@nseng-ai/branch-context 0.1.1 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nseng-ai/branch-context",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@earendil-works/pi-tui": "0.79.1",
32
- "@nseng-ai/brmem": "0.1.1",
33
- "@nseng-ai/capability-kit": "0.1.1",
34
- "@nseng-ai/clinkr": "0.1.1",
35
- "@nseng-ai/foundation": "0.1.1",
36
- "@nseng-ai/ns": "0.1.1",
37
- "@nseng-ai/plans": "0.1.1",
32
+ "@nseng-ai/brmem": "0.1.2",
33
+ "@nseng-ai/capability-kit": "0.1.2",
34
+ "@nseng-ai/clinkr": "0.1.2",
35
+ "@nseng-ai/foundation": "0.1.2",
36
+ "@nseng-ai/kernel": "0.1.2",
37
+ "@nseng-ai/plans": "0.1.2",
38
38
  "zod": "^4.4.3"
39
39
  },
40
40
  "peerDependencies": {
package/src/api/index.ts CHANGED
@@ -4,11 +4,15 @@ export {
4
4
  buildBranchContextPlanKey,
5
5
  createBranchContextFromFile,
6
6
  deriveTargetBranch,
7
+ selectBranchContextCreateOperationTarget,
7
8
  formatBranchContextEvidence,
8
9
  formatBranchContextCreateFailure,
9
10
  formatBranchContextCreatePreview,
11
+ formatBranchSelectionLines,
10
12
  describeBranchContextGraphiteCreationSteps,
11
13
  resolveBranchContextCreatePreviewContext,
14
+ type BranchContextBranchSelection,
15
+ type BranchContextBranchSelectionCollision,
12
16
  type BranchContextCreateOperation,
13
17
  type BranchContextEvidence,
14
18
  type BranchCreationMethod,
@@ -1,5 +1,6 @@
1
1
  import type { BranchContextAttachData } from "./branch-memory.ts";
2
- import { assertBrmemEntryAbsent, attachBranchContext, AttachBranchContextError } from "./attach.ts";
2
+ import { attachBranchContext, AttachBranchContextError } from "./attach.ts";
3
+ import { checkBranchContextEntryPresence, throwBranchContextBrmemError } from "./branch-memory.ts";
3
4
  import { BRANCH_CONTEXT_NAMESPACE, buildBranchContextPlanKey } from "./constants.ts";
4
5
  import type { BrmemGateway } from "@nseng-ai/brmem";
5
6
  import type { GraphiteBranchGateway } from "@nseng-ai/capability-kit/graphite/branch";
@@ -35,6 +36,22 @@ export interface CreateBranchContextFromFileOptions {
35
36
  signal?: AbortSignal;
36
37
  }
37
38
 
39
+ export interface BranchContextBranchSelectionCollision {
40
+ branch: string;
41
+ isLocalBranch: boolean;
42
+ hasAttachedPlan: boolean;
43
+ }
44
+
45
+ interface BranchContextBranchSelectionBase {
46
+ requestedBranch: string;
47
+ selectedBranch: string;
48
+ collisions: BranchContextBranchSelectionCollision[];
49
+ }
50
+
51
+ export type BranchContextBranchSelection =
52
+ | (BranchContextBranchSelectionBase & { type: "exact" })
53
+ | (BranchContextBranchSelectionBase & { type: "auto-suffixed" });
54
+
38
55
  export interface BranchContextEvidence {
39
56
  slug: string;
40
57
  branch: string;
@@ -45,6 +62,7 @@ export interface BranchContextEvidence {
45
62
  refName: string;
46
63
  commit: string;
47
64
  sourceFile: string;
65
+ branchSelection?: BranchContextBranchSelection;
48
66
  summary?: string;
49
67
  }
50
68
 
@@ -56,6 +74,7 @@ export interface BranchContextCreateOperation {
56
74
  namespace: string;
57
75
  key: string;
58
76
  params: CreateBranchContextFromFileParams;
77
+ branchSelection: BranchContextBranchSelection;
59
78
  summary?: string;
60
79
  }
61
80
 
@@ -64,16 +83,19 @@ export interface BranchContextCreatePreviewContext {
64
83
  graphiteParentBranch?: string;
65
84
  }
66
85
 
67
- export interface CreateBranchContextFromResolvedSourceOptions {
86
+ interface BranchContextRepoAccess {
68
87
  cwd: string;
69
- operation: BranchContextCreateOperation;
70
- sourceFile: string;
71
88
  git: GitGateway;
72
89
  brmem: BrmemGateway;
73
- graphite: GraphiteBranchGateway;
74
90
  signal?: AbortSignal;
75
91
  }
76
92
 
93
+ export interface CreateBranchContextFromResolvedSourceOptions extends BranchContextRepoAccess {
94
+ operation: BranchContextCreateOperation;
95
+ sourceFile: string;
96
+ graphite: GraphiteBranchGateway;
97
+ }
98
+
77
99
  export async function createBranchContextFromFile(
78
100
  pi: CommandExecApi,
79
101
  params: CreateBranchContextFromFileParams,
@@ -82,15 +104,23 @@ export async function createBranchContextFromFile(
82
104
  const operation = buildBranchContextCreateOperation(params);
83
105
  const { git, brmem, graphite } = options.context;
84
106
  await checkBranchRefFormat(git, options.cwd, operation.branch, options.signal);
107
+ const selectedOperation = await selectBranchContextCreateOperationTarget({
108
+ cwd: options.cwd,
109
+ operation,
110
+ git,
111
+ brmem,
112
+ isExplicitTargetBranch: params.branchName !== undefined,
113
+ ...optionalEntry("signal", options.signal),
114
+ });
85
115
  const sourceFile = await resolvePlanSourceFile(pi, {
86
116
  cwd: options.cwd,
87
- rawFilePath: operation.filePath,
117
+ rawFilePath: selectedOperation.filePath,
88
118
  ...optionalEntry("signal", options.signal),
89
119
  git,
90
120
  });
91
121
  return createBranchContextFromResolvedSource({
92
122
  cwd: options.cwd,
93
- operation,
123
+ operation: selectedOperation,
94
124
  sourceFile,
95
125
  git,
96
126
  brmem,
@@ -103,10 +133,7 @@ export async function createBranchContextFromResolvedSource(
103
133
  options: CreateBranchContextFromResolvedSourceOptions,
104
134
  ): Promise<BranchContextEvidence> {
105
135
  const startPoint = await resolveStartPoint(options.git, options.cwd, options.signal);
106
- await assertLocalBranchAbsent(options.git, options.cwd, options.operation.branch, options.signal);
107
- await assertBrmemEntryAbsent(options.brmem, options.operation.branch, options.operation.key, {
108
- formatPresentMessage: formatStaleTargetBranchMemoryMessage,
109
- });
136
+ await assertSelectedTargetBranchStillAvailable(options);
110
137
  await createBranchContext(options.git, options.graphite, {
111
138
  cwd: options.cwd,
112
139
  method: options.operation.branchCreation,
@@ -141,6 +168,7 @@ export async function createBranchContextFromResolvedSource(
141
168
  slug: options.operation.slug,
142
169
  branchCreation: options.operation.branchCreation,
143
170
  startPoint,
171
+ branchSelection: options.operation.branchSelection,
144
172
  summary: options.operation.summary,
145
173
  });
146
174
  }
@@ -172,6 +200,7 @@ export function buildBranchContextCreateOperation(
172
200
  namespace: BRANCH_CONTEXT_NAMESPACE,
173
201
  key: buildBranchContextPlanKey(slug),
174
202
  params: operationParams,
203
+ branchSelection: exactBranchSelection(branch),
175
204
  };
176
205
  if (summary === undefined) {
177
206
  return operation;
@@ -195,6 +224,7 @@ export function formatBranchContextCreatePreview(
195
224
  "Target:",
196
225
  `Branch: ${operation.branch}`,
197
226
  `Branch creation: ${operation.branchCreation}`,
227
+ ...formatBranchSelectionLines(operation.branchSelection),
198
228
  `Start point: ${context.startPoint}`,
199
229
  `Branch Memory namespace: ${operation.namespace}`,
200
230
  `Branch Memory key: ${operation.key}`,
@@ -233,6 +263,7 @@ export function formatBranchContextEvidence(evidence: BranchContextEvidence): st
233
263
  "Created branch context and attached plan.",
234
264
  `Branch: ${evidence.branch}`,
235
265
  `Branch creation: ${evidence.branchCreation}`,
266
+ ...formatBranchSelectionLines(evidence.branchSelection),
236
267
  `Start point: ${evidence.startPoint}`,
237
268
  `Namespace: ${evidence.namespace}`,
238
269
  `Key: ${evidence.key}`,
@@ -254,6 +285,7 @@ export function formatBranchContextCreateFailure(
254
285
  return [
255
286
  "Failed to create branch context and attach plan.",
256
287
  `Branch: ${operation.branch}`,
288
+ ...formatBranchSelectionLines(operation.branchSelection),
257
289
  `Branch creation: ${operation.branchCreation}`,
258
290
  `Namespace: ${operation.namespace}`,
259
291
  `Key: ${operation.key}`,
@@ -268,6 +300,54 @@ export function deriveTargetBranch(branchName: string | undefined, slug: string)
268
300
  return trimmedBranchName && trimmedBranchName.length > 0 ? trimmedBranchName : slug;
269
301
  }
270
302
 
303
+ export async function selectBranchContextCreateOperationTarget(
304
+ options: BranchContextRepoAccess & {
305
+ operation: BranchContextCreateOperation;
306
+ isExplicitTargetBranch: boolean;
307
+ },
308
+ ): Promise<BranchContextCreateOperation> {
309
+ const maxAttempts = 100;
310
+ const requestedBranch = options.operation.branch;
311
+ if (options.isExplicitTargetBranch) {
312
+ return withBranchSelection(options.operation, exactBranchSelection(requestedBranch));
313
+ }
314
+
315
+ const collisions: BranchContextBranchSelectionCollision[] = [];
316
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
317
+ const candidate = attempt === 1 ? requestedBranch : `${requestedBranch}-${attempt}`;
318
+ const occupancy = await checkTargetBranchOccupancy({
319
+ cwd: options.cwd,
320
+ git: options.git,
321
+ brmem: options.brmem,
322
+ branch: candidate,
323
+ key: options.operation.key,
324
+ ...optionalEntry("signal", options.signal),
325
+ });
326
+ if (!occupancy.collision.isLocalBranch && !occupancy.collision.hasAttachedPlan) {
327
+ const selection =
328
+ collisions.length === 0
329
+ ? exactBranchSelection(candidate)
330
+ : {
331
+ type: "auto-suffixed" as const,
332
+ requestedBranch,
333
+ selectedBranch: candidate,
334
+ collisions,
335
+ };
336
+ return withBranchSelection(options.operation, selection);
337
+ }
338
+ collisions.push({ branch: candidate, ...occupancy.collision });
339
+ }
340
+
341
+ throw new Error(
342
+ [
343
+ "Could not find an available default target branch for branch context creation.",
344
+ `Base branch: ${requestedBranch}`,
345
+ `Attempts: ${maxAttempts}`,
346
+ `Last attempted branch: ${requestedBranch}-${maxAttempts}`,
347
+ ].join("\n"),
348
+ );
349
+ }
350
+
271
351
  async function checkBranchRefFormat(
272
352
  git: GitGateway,
273
353
  cwd: string,
@@ -292,27 +372,134 @@ async function resolveStartPoint(
292
372
  return head.value;
293
373
  }
294
374
 
295
- async function assertLocalBranchAbsent(
296
- git: GitGateway,
297
- cwd: string,
298
- targetBranch: string,
299
- signal: AbortSignal | undefined,
375
+ interface SelectedBranchAvailabilityOptions extends BranchContextRepoAccess {
376
+ operation: BranchContextCreateOperation;
377
+ }
378
+
379
+ async function assertSelectedTargetBranchStillAvailable(
380
+ options: SelectedBranchAvailabilityOptions,
300
381
  ): Promise<void> {
301
- const check = await git.localBranchPresence({ cwd, branch: targetBranch, signal });
302
- if (check.type === "absent") {
303
- return;
304
- }
305
- if (check.type === "present") {
382
+ const occupancy = await checkTargetBranchOccupancy({
383
+ cwd: options.cwd,
384
+ git: options.git,
385
+ brmem: options.brmem,
386
+ branch: options.operation.branch,
387
+ key: options.operation.key,
388
+ stopAfterLocalBranch: true,
389
+ ...optionalEntry("signal", options.signal),
390
+ });
391
+ if (occupancy.localBranch !== undefined) {
306
392
  throw new Error(
307
393
  [
308
394
  "Target branch already exists; refusing to overwrite.",
309
- `Branch: ${targetBranch}`,
310
- `Ref: ${check.refName}`,
311
- `Command: ${check.displayCommand}`,
395
+ `Branch: ${options.operation.branch}`,
396
+ `Ref: ${occupancy.localBranch.refName}`,
397
+ `Command: ${occupancy.localBranch.displayCommand}`,
312
398
  ].join("\n"),
313
399
  );
314
400
  }
315
- throw new Error(check.error.message);
401
+ if (occupancy.collision.hasAttachedPlan) {
402
+ throw new Error(
403
+ formatStaleTargetBranchMemoryMessage({
404
+ targetBranch: options.operation.branch,
405
+ key: options.operation.key,
406
+ }),
407
+ );
408
+ }
409
+ }
410
+
411
+ interface CheckTargetBranchOccupancyOptions extends BranchContextRepoAccess {
412
+ branch: string;
413
+ key: string;
414
+ stopAfterLocalBranch?: boolean;
415
+ }
416
+
417
+ interface TargetBranchOccupancy {
418
+ collision: Omit<BranchContextBranchSelectionCollision, "branch">;
419
+ localBranch?: LocalBranchPresence;
420
+ }
421
+
422
+ interface LocalBranchPresence {
423
+ refName: string;
424
+ displayCommand: string;
425
+ }
426
+
427
+ async function checkTargetBranchOccupancy(
428
+ options: CheckTargetBranchOccupancyOptions,
429
+ ): Promise<TargetBranchOccupancy> {
430
+ const localBranch = await checkLocalBranchPresence(options);
431
+ if (localBranch !== undefined && options.stopAfterLocalBranch === true) {
432
+ return {
433
+ collision: { isLocalBranch: true, hasAttachedPlan: false },
434
+ localBranch,
435
+ };
436
+ }
437
+ const hasAttachedPlan = await checkAttachedPlanPresence(
438
+ options.brmem,
439
+ options.branch,
440
+ options.key,
441
+ );
442
+ return {
443
+ collision: {
444
+ isLocalBranch: localBranch !== undefined,
445
+ hasAttachedPlan,
446
+ },
447
+ ...optionalEntry("localBranch", localBranch),
448
+ };
449
+ }
450
+
451
+ async function checkLocalBranchPresence(
452
+ options: Pick<CheckTargetBranchOccupancyOptions, "cwd" | "git" | "branch" | "signal">,
453
+ ): Promise<LocalBranchPresence | undefined> {
454
+ const localBranch = await options.git.localBranchPresence({
455
+ cwd: options.cwd,
456
+ branch: options.branch,
457
+ ...optionalEntry("signal", options.signal),
458
+ });
459
+ if (localBranch.type === "error") {
460
+ throw new Error(localBranch.error.message);
461
+ }
462
+ if (localBranch.type === "absent") {
463
+ return undefined;
464
+ }
465
+ return { refName: localBranch.refName, displayCommand: localBranch.displayCommand };
466
+ }
467
+
468
+ async function checkAttachedPlanPresence(
469
+ brmem: BrmemGateway,
470
+ branch: string,
471
+ key: string,
472
+ ): Promise<boolean> {
473
+ const attachedPlan = await checkBranchContextEntryPresence(brmem, { branch, key });
474
+ if (attachedPlan.type === "error") {
475
+ throwBranchContextBrmemError(attachedPlan.error);
476
+ }
477
+ return attachedPlan.type === "present";
478
+ }
479
+
480
+ function exactBranchSelection(branch: string): BranchContextBranchSelection {
481
+ return { type: "exact", requestedBranch: branch, selectedBranch: branch, collisions: [] };
482
+ }
483
+
484
+ function withBranchSelection(
485
+ operation: BranchContextCreateOperation,
486
+ branchSelection: BranchContextBranchSelection,
487
+ ): BranchContextCreateOperation {
488
+ const branch = branchSelection.selectedBranch;
489
+ return { ...operation, branch, branchSelection };
490
+ }
491
+
492
+ export function formatBranchSelectionLines(
493
+ selection: BranchContextBranchSelection | undefined,
494
+ ): string[] {
495
+ if (selection === undefined || selection.type === "exact") {
496
+ return [];
497
+ }
498
+ return [
499
+ `Default branch: ${selection.requestedBranch}`,
500
+ `Selected target branch: ${selection.selectedBranch}`,
501
+ `Default branch ${selection.requestedBranch} already exists or has an attached plan; selected ${selection.selectedBranch}.`,
502
+ ];
316
503
  }
317
504
 
318
505
  function formatStaleTargetBranchMemoryMessage(context: {
@@ -437,6 +624,7 @@ function buildEvidence(input: {
437
624
  slug: string;
438
625
  branchCreation: BranchCreationMethod;
439
626
  startPoint: string;
627
+ branchSelection: BranchContextBranchSelection;
440
628
  summary: string | undefined;
441
629
  }): BranchContextEvidence {
442
630
  const evidence = {
@@ -449,6 +637,7 @@ function buildEvidence(input: {
449
637
  refName: input.data.refName,
450
638
  commit: input.data.commit,
451
639
  sourceFile: input.data.sourceFile,
640
+ branchSelection: input.branchSelection,
452
641
  };
453
642
 
454
643
  if (input.summary === undefined) {
@@ -17,6 +17,7 @@ export interface BranchContextContext {
17
17
 
18
18
  export interface BranchContextContextOptions {
19
19
  cwd?: string;
20
+ brmemCommands?: StdinCapableCommandExecApi;
20
21
  }
21
22
 
22
23
  export type BranchContextContextFactory<Args extends unknown[]> = (
@@ -24,12 +25,14 @@ export type BranchContextContextFactory<Args extends unknown[]> = (
24
25
  ) => BranchContextContext;
25
26
 
26
27
  export function createBranchContextContext(
27
- commands: StdinCapableCommandExecApi,
28
+ commands: CommandExecApi,
28
29
  options: BranchContextContextOptions = {},
29
30
  ): BranchContextContext {
30
31
  const cwd = options.cwd ?? process.cwd();
31
32
  const git = new RealGitGateway(commands);
32
- const brmem = new RealGitBrmemGateway({ cwd, commands, git });
33
+ const brmemCommands = options.brmemCommands ?? new NodeCommandExecApi();
34
+ const brmemGit = brmemCommands === commands ? git : new RealGitGateway(brmemCommands);
35
+ const brmem = new RealGitBrmemGateway({ cwd, commands: brmemCommands, git: brmemGit });
33
36
  return {
34
37
  commands,
35
38
  git,
@@ -97,13 +97,25 @@ export type AttachRequest = z.infer<typeof attachRequestSchema>;
97
97
  export type ListRequest = z.infer<typeof listRequestSchema>;
98
98
  export type KeyRequest = z.infer<typeof keyRequestSchema>;
99
99
 
100
- export type BranchContextData = ReturnType<typeof branchContextJson>;
100
+ export type BranchContextData = z.infer<typeof branchContextResultSchema>;
101
101
  export type LoadPlanData = ReturnType<typeof loadedPlanJson>;
102
102
  export type AttachData = ReturnType<typeof attachJson>;
103
103
  export type ListData = ReturnType<typeof listJson>;
104
104
  export type CheckData = ReturnType<typeof checkJson>;
105
105
  export type DeleteData = ReturnType<typeof deleteJson>;
106
106
 
107
+ const branchSelectionCollisionSchema = z.object({
108
+ branch: z.string(),
109
+ isLocalBranch: z.boolean(),
110
+ hasAttachedPlan: z.boolean(),
111
+ });
112
+
113
+ const branchSelectionBaseSchema = z.object({
114
+ requestedBranch: z.string(),
115
+ selectedBranch: z.string(),
116
+ collisions: z.array(branchSelectionCollisionSchema),
117
+ });
118
+
107
119
  export const branchContextResultSchema = z.object({
108
120
  slug: z.string(),
109
121
  branch: z.string(),
@@ -114,6 +126,10 @@ export const branchContextResultSchema = z.object({
114
126
  refName: z.string(),
115
127
  commit: z.string(),
116
128
  sourceFile: z.string(),
129
+ branchSelection: z.union([
130
+ branchSelectionBaseSchema.extend({ type: z.literal("exact") }),
131
+ branchSelectionBaseSchema.extend({ type: z.literal("auto-suffixed") }),
132
+ ]),
117
133
  summary: z.string().optional(),
118
134
  });
119
135
 
@@ -464,8 +480,15 @@ function branchContextJson(evidence: BranchContextEvidence): {
464
480
  refName: string;
465
481
  commit: string;
466
482
  sourceFile: string;
483
+ branchSelection: NonNullable<BranchContextEvidence["branchSelection"]>;
467
484
  summary?: string;
468
485
  } {
486
+ const branchSelection = evidence.branchSelection ?? {
487
+ type: "exact" as const,
488
+ requestedBranch: evidence.branch,
489
+ selectedBranch: evidence.branch,
490
+ collisions: [],
491
+ };
469
492
  return {
470
493
  slug: evidence.slug,
471
494
  branch: evidence.branch,
@@ -476,6 +499,7 @@ function branchContextJson(evidence: BranchContextEvidence): {
476
499
  refName: evidence.refName,
477
500
  commit: evidence.commit,
478
501
  sourceFile: evidence.sourceFile,
502
+ branchSelection,
479
503
  ...(evidence.summary === undefined ? {} : { summary: evidence.summary }),
480
504
  };
481
505
  }
@@ -4,8 +4,8 @@ import type { CommandExecApi } from "@nseng-ai/foundation/exec";
4
4
  import {
5
5
  buildContentSlugPrompt,
6
6
  deriveContentSlug,
7
- type ContentSlugDerivationVariant,
8
7
  type ContentSlugEvidence,
8
+ type PlanContentSlugVariantSeed,
9
9
  } from "@nseng-ai/plans";
10
10
 
11
11
  export type PlanContentSlugEvidence = ContentSlugEvidence;
@@ -17,7 +17,7 @@ export interface DerivePlanContentSlugInput {
17
17
  readTextFile?: (path: string) => Promise<string>;
18
18
  }
19
19
 
20
- const PLAN_CONTENT_SLUG_VARIANT: ContentSlugDerivationVariant = {
20
+ const PLAN_CONTENT_SLUG_VARIANT: PlanContentSlugVariantSeed = {
21
21
  slugKind: "branch-context slug",
22
22
  promptIntroLines: [
23
23
  "Generate the branch-context slug for the Markdown implementation plan content below.",
package/src/ns/command.ts CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  type NsDomainCommandOptions,
4
4
  } from "@nseng-ai/capability-kit/ns-command";
5
5
  import { optionalEntry } from "@nseng-ai/foundation/primitives";
6
- import type { NsCommand, NsCommandSchema, NsExtensionApi } from "@nseng-ai/ns/kernel/sdk";
6
+ import type { NsCommand, NsCommandSchema, NsExtensionApi } from "@nseng-ai/kernel/sdk";
7
7
 
8
8
  import {
9
9
  createRealBranchContextCliContext,
@@ -1,4 +1,4 @@
1
- import { defineExtension } from "@nseng-ai/ns/kernel/sdk";
1
+ import { defineExtension } from "@nseng-ai/kernel/sdk";
2
2
 
3
3
  import { branchContextCommand } from "../command.ts";
4
4
  import { attachRequestSchema, attachResultSchema, handleAttach } from "../../core/operations.ts";
@@ -1,4 +1,4 @@
1
- import { defineExtension } from "@nseng-ai/ns/kernel/sdk";
1
+ import { defineExtension } from "@nseng-ai/kernel/sdk";
2
2
 
3
3
  import { branchContextCommand } from "../command.ts";
4
4
  import { checkResultSchema, handleCheck, keyRequestSchema } from "../../core/operations.ts";
@@ -1,4 +1,4 @@
1
- import { defineExtension } from "@nseng-ai/ns/kernel/sdk";
1
+ import { defineExtension } from "@nseng-ai/kernel/sdk";
2
2
 
3
3
  import { branchContextCommand } from "../command.ts";
4
4
  import { deleteResultSchema, handleDelete, keyRequestSchema } from "../../core/operations.ts";
@@ -1,4 +1,4 @@
1
- import { defineExtension } from "@nseng-ai/ns/kernel/sdk";
1
+ import { defineExtension } from "@nseng-ai/kernel/sdk";
2
2
 
3
3
  import { branchContextCommand } from "../command.ts";
4
4
  import {
@@ -1,4 +1,4 @@
1
- import { defineExtension } from "@nseng-ai/ns/kernel/sdk";
1
+ import { defineExtension } from "@nseng-ai/kernel/sdk";
2
2
 
3
3
  import { branchContextCommand } from "../command.ts";
4
4
  import { handleList, listRequestSchema, listResultSchema } from "../../core/operations.ts";
@@ -1,4 +1,4 @@
1
- import { defineExtension } from "@nseng-ai/ns/kernel/sdk";
1
+ import { defineExtension } from "@nseng-ai/kernel/sdk";
2
2
 
3
3
  import { branchContextCommand } from "../command.ts";
4
4
  import { handleLoad, loadPlanResultSchema, loadRequestSchema } from "../../core/operations.ts";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  defineRepoLocalNsExtensionDescriptor,
3
3
  repoLocalNsCommandDescriptor,
4
- } from "@nseng-ai/ns/kernel/sdk";
4
+ } from "@nseng-ai/kernel/sdk";
5
5
 
6
6
  import { branchContextAttachNsCommand } from "./commands/attach.ts";
7
7
  import { branchContextCheckNsCommand } from "./commands/check.ts";
@@ -1,8 +1,6 @@
1
1
  import { registerCommandWithImmediateAck } from "@nseng-ai/pi/commands/ack";
2
- import type { Stats } from "node:fs";
2
+ import { readFileSync, type Stats } from "node:fs";
3
3
  import { lstat, readFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
-
6
4
  import { Text } from "@earendil-works/pi-tui";
7
5
  import { piExecApiToCommandExecApi } from "@nseng-ai/foundation/command";
8
6
  import { RealGitGateway } from "@nseng-ai/capability-kit/git";
@@ -13,6 +11,12 @@ import {
13
11
  optionalEntry,
14
12
  } from "@nseng-ai/foundation/primitives";
15
13
  import type { ScheduledTimer } from "@nseng-ai/foundation/timers";
14
+ import {
15
+ loadPointCatalog,
16
+ nodeProjectConfigGateway,
17
+ resolvePromptPointPath,
18
+ resolvePromptPointSource,
19
+ } from "@nseng-ai/kernel/project-config/points";
16
20
  import { systemTimerScheduler } from "@nseng-ai/foundation/time";
17
21
  import { WRITE_GRILLED_PLAN_COMMAND_NAME, WRITE_PLAN_COMMAND_NAME } from "./surfaces.ts";
18
22
  import { sendCommandProgressOrNotify } from "@nseng-ai/pi/commands/ack";
@@ -56,71 +60,12 @@ interface WriteSavedPlanFileProgressDetails {
56
60
  elapsedSeconds?: number;
57
61
  }
58
62
 
59
- const WRITE_PLAN_PROMPT_NAME = "plans-write";
60
-
61
- export const DEFAULT_WRITE_PLAN_PROMPT_BODY = `Plan audience and context contract:
62
- - Treat the saved Markdown plan as the only planning context available to a completely fresh downstream implementation session.
63
- - Make the plan self-contained. Do not rely on this conversation, hidden context, tool transcripts, or "as discussed" references.
64
- - Embed all relevant context discovered during planning, including user goals, constraints, current behavior, important files/symbols/tests/docs, decisions made, rationale, rejected alternatives, assumptions, risks, and proportional validation guidance.
65
- - Prefer concrete file paths, symbol names, command names, expected outcomes, and implementation order over vague instructions.
66
- - If you inspected evidence during planning, summarize the discovered facts in the plan so the downstream agent does not need to rediscover them unless verification is required.
63
+ const WRITE_PLAN_POINT_ID = "branch-context.plans-write";
67
64
 
68
- External research/context contract:
69
- - If planning used anything outside the repository — web searches, external docs, GitHub issues/PRs, API docs, CLIs hitting remote services, local files outside the repo, or other non-repo resources — include the relevant findings inline in the saved plan.
70
- - Do not merely link to external resources. Summarize the concrete facts, constraints, examples, decisions, and caveats the downstream agent needs.
71
- - Include source/provenance where useful: URL, command, document name, issue/PR number, accessed date/time if known, and why it mattered.
72
- - If external findings may become stale, mark what should be revalidated during implementation.
73
- - Do not include secrets, credentials, private tokens, or unnecessary sensitive data.
74
-
75
- <!-- PLAN-VERIFICATION-WORKSTREAM:START refactor-execution-strategy-guidance -->
76
- Refactor execution strategy:
77
- - If the implementation includes same-shape edits across multiple files, explicitly choose an execution mode in the plan.
78
- - Apply the canonical guidance in \`skills/enriched-plan-save/references/refactor-execution-strategy.md\`, including the final stale-terminology grep/equivalent check when changing names or concepts.
79
- <!-- PLAN-VERIFICATION-WORKSTREAM:END refactor-execution-strategy-guidance -->
80
-
81
- Recommended saved plan sections:
82
- - Goal and user-visible outcome.
83
- - Planning context and discovered facts, including relevant repository state.
84
- - External/off-repo research context, or a note that none was used when that helps remove ambiguity.
85
- - Files, symbols, commands, and tests likely to change.
86
- - Step-by-step implementation approach.
87
- - Validation guidance and expected results. Do not over-specify routine test/check scope as a planning decision; leave ordinary validation coverage to the implementing agent's project policy and changed-file judgment.
88
- - Risks, assumptions, edge cases, and open questions.
89
-
90
- Workflow:
91
- 1. Inspect the repository, documentation, and current conversation context as needed for the requested work.
92
- 2. Produce a detailed Markdown implementation plan.
93
- 3. Review the final Markdown plan content for completeness.
94
- 4. Call write_saved_plan_file with the full Markdown content and optional one-sentence summary; do not generate or pass a slug.
95
- 5. Report the saved plan evidence: file path, repo key, repo root, repo identity source, source branch, branch path segment, slug, slug model, and summary when present.
96
- 6. Stop after reporting the saved plan evidence. Do not create a branch, write Branch Memory, or call any branch-context command/tool.
97
-
98
- Local plan store contract:
99
- - Canonical path convention: $XDG_STATE_HOME/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md, defaulting to $HOME/.local/state/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md. No fallback path is read or written; only $HOME/.local/state/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md is used.
100
- - <repo>: for github.com origins, gh--<owner>--<repo> from sanitized GitHub owner and repo path segments; for non-GitHub or origin-less repos, one sanitized path segment from the normalized remote.origin.url or real repo root path
101
- - <encoded-source-branch>: current branch at plan-file creation time encoded as one filesystem-safe path segment; branch slashes become --- (for example, branch-contexts/add-widget becomes branch-contexts---add-widget)
102
- - <slug>: semantic kebab-case saved-plan filename slug without .md; this is a local plan-store locator, not necessarily the later implementation branch slug
103
- - Existing saved plan file: write_saved_plan_file refuses to overwrite it; do not manually choose a replacement slug.
104
- - Working-tree behavior: no checked-in plan file is created.
105
-
106
- Saved-plan filename slug rules:
107
- - write_saved_plan_file derives the final saved-plan filename slug from the final plan content through the Codex-backed slug model.
108
- - Do not generate, guess, or pass a slug yourself.
109
- - The derived slug is kebab-case, 3–7 words, specific to the work described by the final plan, and rejects dates, random IDs, and generic-only slugs.
110
-
111
- When the plan is ready, call write_saved_plan_file with:
112
- - content: the complete reviewed Markdown plan content
113
- - summary: optional one-sentence summary of the plan
114
-
115
- Exact tool call shape:
116
- \`\`\`json
117
- {
118
- "content": "# Plan\\n...",
119
- "summary": "One-sentence summary of the plan."
120
- }
121
- \`\`\`
122
-
123
- If summary is not useful, omit it from the tool call rather than passing an empty string. Do not create target branches or write Branch Memory in this workflow.`;
65
+ export const DEFAULT_WRITE_PLAN_PROMPT_BODY = readFileSync(
66
+ new URL("./prompts/plans-write-default.md", import.meta.url),
67
+ "utf8",
68
+ ).trimEnd();
124
69
 
125
70
  type WritePlanPromptBodyResolution =
126
71
  | { type: "resolved"; body: string }
@@ -181,10 +126,10 @@ async function resolveWritePlanPromptBody(
181
126
  }
182
127
 
183
128
  try {
184
- return await readRepoWritePlanPromptBody(repoRoot.path);
129
+ return await readWritePlanPromptBody(repoRoot.path);
185
130
  } catch (error) {
186
131
  return fallbackWritePlanPromptBody(
187
- `repo prompt ${repoPromptPath(repoRoot.path)} could not be read: ${formatErrorMessage(error)}`,
132
+ `prompt point ${WRITE_PLAN_POINT_ID} could not be read: ${formatErrorMessage(error)}`,
188
133
  );
189
134
  }
190
135
  }
@@ -208,28 +153,25 @@ async function resolveGitRoot(
208
153
  return { type: "resolved", path: result.value };
209
154
  }
210
155
 
211
- async function readRepoWritePlanPromptBody(
212
- repoRoot: string,
213
- ): Promise<WritePlanPromptBodyResolution> {
214
- const nsPath = join(repoRoot, ".ns");
215
- const promptDir = join(nsPath, "prompts");
216
- const promptPath = repoPromptPath(repoRoot);
217
- await assertSafeDirectory(nsPath, ".ns");
218
- await assertSafeDirectory(promptDir, ".ns/prompts");
219
- await assertSafeFile(promptPath, `.ns/prompts/${WRITE_PLAN_PROMPT_NAME}.md`);
220
-
221
- const content = await readFile(promptPath, "utf8");
222
- if (content.trim().length === 0) {
223
- return fallbackWritePlanPromptBody(`repo prompt ${promptPath} is empty`);
156
+ async function readWritePlanPromptBody(repoRoot: string): Promise<WritePlanPromptBodyResolution> {
157
+ const catalog = loadPointCatalog({ repoRoot, gateway: nodeProjectConfigGateway });
158
+ const source = resolvePromptPointSource(catalog, WRITE_PLAN_POINT_ID);
159
+ if (source.type === "env") {
160
+ return fallbackWritePlanPromptBody(
161
+ `prompt point ${WRITE_PLAN_POINT_ID} has unsupported env source`,
162
+ );
224
163
  }
225
- return { type: "resolved", body: content };
226
- }
164
+ const promptPath = resolvePromptPointPath(repoRoot, source);
165
+ if (promptPath === undefined) {
166
+ return fallbackWritePlanPromptBody(`prompt point ${WRITE_PLAN_POINT_ID} has no default`);
167
+ }
168
+ await assertSafeFile(promptPath.path, promptPath.label);
227
169
 
228
- async function assertSafeDirectory(targetPath: string, label: string): Promise<void> {
229
- const stats = await assertNotSymlink(targetPath, label);
230
- if (!stats.isDirectory()) {
231
- throw new Error(`${label} is not a directory`);
170
+ const content = await readFile(promptPath.path, "utf8");
171
+ if (content.trim().length === 0) {
172
+ return fallbackWritePlanPromptBody(`${promptPath.label} is empty`);
232
173
  }
174
+ return { type: "resolved", body: content };
233
175
  }
234
176
 
235
177
  async function assertSafeFile(targetPath: string, label: string): Promise<void> {
@@ -247,10 +189,6 @@ async function assertNotSymlink(targetPath: string, label: string): Promise<Stat
247
189
  return stats;
248
190
  }
249
191
 
250
- function repoPromptPath(repoRoot: string): string {
251
- return join(repoRoot, ".ns", "prompts", `${WRITE_PLAN_PROMPT_NAME}.md`);
252
- }
253
-
254
192
  export async function handleWritePlanCommand(
255
193
  pi: ExtensionAPI,
256
194
  args: string,
@@ -9,6 +9,7 @@ import {
9
9
  formatBranchContextGtUpstackImplFollowUpFlow,
10
10
  runBranchContextGtUpstackImplLaunch,
11
11
  } from "./gt/upstack-impl-launch.ts";
12
+ import { createGtUpstackImplGitGateway } from "./gt/git-gateway.ts";
12
13
  import {
13
14
  BRANCH_CONTEXT_FROM_PLAN_COMMAND_NAME,
14
15
  BRANCH_CONTEXT_UPSTACK_IMPL_FROM_PLAN_COMMAND_NAME,
@@ -21,14 +22,19 @@ import {
21
22
  buildBranchContextOutputMessage,
22
23
  buildBranchContextPlanKey,
23
24
  buildImplBranchContextPrompt,
25
+ createBranchContextContext,
24
26
  createRealBranchContextContext,
25
27
  derivePlanContentSlug,
26
28
  deriveTargetBranch,
27
29
  formatBranchContextEvidence,
28
30
  describeBranchContextGraphiteCreationSteps,
31
+ formatBranchSelectionLines,
32
+ selectBranchContextCreateOperationTarget,
33
+ buildBranchContextCreateOperation,
29
34
  formatExistingBranchContextReuse,
30
35
  formatLoadedAttachedPlanEvidence,
31
36
  resolveExistingBranchContextReuse,
37
+ type BranchContextBranchSelection,
32
38
  type BranchContextEvidence,
33
39
  type BranchContextOutputDetails,
34
40
  type BranchCreationMethod,
@@ -71,14 +77,14 @@ const GRAPHITE_BRANCH_CREATION_HELP =
71
77
 
72
78
  export const CREATE_BRANCH_CONTEXT_USAGE = `Usage: /${CREATE_BRANCH_CONTEXT_COMMAND_NAME} [options] [absolute-or-home-plan-file.md]
73
79
 
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.
80
+ Create a branch context from a saved plan. The branch slug is derived from the plan content by a tiny Pi model, default target branches auto-suffix on collisions, then the plan is attached to the branch in Branch Memory as <content-derived-slug>.md.
75
81
 
76
82
  Options:
77
83
  --dry-run Show the selected plan and target branch without mutating.
78
84
  --yes, -y Compatibility no-op; resolved branch contexts create without confirmation.
79
85
  --graphite Create with the branch-context Graphite method.
80
86
  --plain-git Create with plain Git only; no Graphite tracking.
81
- --branch <name> Use an explicit target branch name.
87
+ --branch <name> Use an explicit target branch name; explicit branches do not auto-suffix.
82
88
  --help, -h Show this help.
83
89
 
84
90
  ${GRAPHITE_BRANCH_CREATION_HELP}
@@ -96,7 +102,7 @@ Options:
96
102
  --yes, -y Compatibility no-op; resolved branch contexts create without confirmation.
97
103
  --graphite Default: create with the branch-context Graphite method.
98
104
  --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.
105
+ --branch <name> Use an explicit target branch name; explicit branches do not auto-suffix.
100
106
  --help, -h Show this help.
101
107
 
102
108
  ${GRAPHITE_BRANCH_CREATION_HELP}
@@ -166,9 +172,12 @@ interface CreateBranchContextPreviewBase {
166
172
  filePath: string;
167
173
  fileName: string;
168
174
  planKey: string;
175
+ requestedBranch?: string;
169
176
  targetBranch: string;
170
177
  branchNameForCreation?: string;
171
178
  isExplicitTargetBranch: boolean;
179
+ /** Display-only preview evidence; creation re-runs core target selection authoritatively. */
180
+ branchSelection?: BranchContextBranchSelection;
172
181
  slugEvidence: PlanContentSlugEvidence;
173
182
  branchCreation: BranchCreationMethod;
174
183
  summary?: string;
@@ -238,12 +247,27 @@ class CreateBranchContextUsageError extends Error {
238
247
  }
239
248
  }
240
249
 
250
+ function shouldResolveTargetBranchInPreview(options: BranchContextExtensionOptions): boolean {
251
+ return (
252
+ options.shouldResolveTargetBranchInPreview ?? options.branchContextOperations === undefined
253
+ );
254
+ }
255
+
241
256
  function resolveBranchContextContext(
242
257
  pi: ExtensionAPI,
243
258
  cwd: string,
244
259
  options: BranchContextExtensionOptions,
245
260
  ) {
246
- return options.createBranchContextContext?.(pi, cwd) ?? createRealBranchContextContext({ cwd });
261
+ if (options.createBranchContextContext !== undefined) {
262
+ return options.createBranchContextContext(pi, cwd);
263
+ }
264
+ if (pi.exec !== undefined) {
265
+ return createBranchContextContext(
266
+ { exec: (command, args, execOptions) => pi.exec(command, args, execOptions) },
267
+ { cwd },
268
+ );
269
+ }
270
+ return createRealBranchContextContext({ cwd });
247
271
  }
248
272
 
249
273
  export function parseCreateBranchContextArgs(rawArgs: string): CreateBranchContextArgs {
@@ -394,17 +418,34 @@ export async function deriveCreateBranchContextPreview(
394
418
  const branchCreation = args.branchCreation ?? resolveBranchContextDefaultCreation(options);
395
419
  const target = deriveBranchContextTargetBranch(args, slugEvidence.slug, options);
396
420
  const planKey = buildBranchContextPlanKey(slugEvidence.slug);
421
+ const requestedOperation = buildBranchContextCreateOperation({
422
+ slug: slugEvidence.slug,
423
+ filePath: selectedFile.filePath,
424
+ branchCreation,
425
+ ...optionalEntry("branchName", target.branchNameForCreation),
426
+ });
427
+ let selectedOperation = requestedOperation;
428
+ if (shouldResolveTargetBranchInPreview(options)) {
429
+ const context = resolveBranchContextContext(pi, ctx.cwd, options);
430
+ selectedOperation = await selectBranchContextCreateOperationTarget({
431
+ cwd: ctx.cwd,
432
+ operation: requestedOperation,
433
+ git: context.git,
434
+ brmem: context.brmem,
435
+ isExplicitTargetBranch: target.isExplicitTargetBranch,
436
+ });
437
+ }
397
438
  const base = {
398
439
  slug: slugEvidence.slug,
399
440
  savedPlanFileStem: selected.savedPlanFileStem,
400
441
  filePath: selectedFile.filePath,
401
442
  fileName: selectedFile.fileName,
402
443
  planKey,
403
- targetBranch: target.targetBranch,
404
- ...(target.branchNameForCreation === undefined
405
- ? {}
406
- : { branchNameForCreation: target.branchNameForCreation }),
444
+ requestedBranch: target.targetBranch,
445
+ targetBranch: selectedOperation.branch,
446
+ ...optionalEntry("branchNameForCreation", target.branchNameForCreation),
407
447
  isExplicitTargetBranch: target.isExplicitTargetBranch,
448
+ branchSelection: selectedOperation.branchSelection,
408
449
  branchCreation,
409
450
  slugEvidence,
410
451
  };
@@ -448,6 +489,7 @@ export function formatCreateBranchContextPreview(preview: CreateBranchContextPre
448
489
  lines.push("Target:");
449
490
  lines.push(`Branch: ${preview.targetBranch}`);
450
491
  lines.push(`Branch creation: ${preview.branchCreation}`);
492
+ lines.push(...formatBranchSelectionLines(preview.branchSelection));
451
493
  lines.push("Attach plan as:");
452
494
  lines.push(`Branch Memory namespace: ${BRANCH_CONTEXT_NAMESPACE}`);
453
495
  lines.push(`Branch Memory key: ${preview.planKey}`);
@@ -991,7 +1033,7 @@ async function runGtUpstackImplLaunchTail(options: GtUpstackImplLaunchTailOption
991
1033
  presentBranchContextMessage(pi, ctx, options.successBody, options.outputDetails, "info");
992
1034
 
993
1035
  const launchResult = await runBranchContextGtUpstackImplLaunch({
994
- host: pi,
1036
+ git: createGtUpstackImplGitGateway(pi),
995
1037
  ctx,
996
1038
  statusKey: GT_UPSTACK_IMPL_STATUS_KEY,
997
1039
  target,
@@ -0,0 +1,11 @@
1
+ import { piExecApiToCommandExecApi } from "@nseng-ai/foundation/command";
2
+ import { RealGitGateway, type GitGateway } from "@nseng-ai/capability-kit/git";
3
+ import type { ExtensionAPI } from "../host-types.ts";
4
+
5
+ export const GT_UPSTACK_IMPL_CHECKOUT_TIMEOUT_MS = 30_000;
6
+
7
+ export function createGtUpstackImplGitGateway(pi: ExtensionAPI): GitGateway {
8
+ return new RealGitGateway(piExecApiToCommandExecApi(pi), {
9
+ timeoutMs: GT_UPSTACK_IMPL_CHECKOUT_TIMEOUT_MS,
10
+ });
11
+ }
@@ -1,10 +1,8 @@
1
1
  import { type BranchContextEvidence } from "@nseng-ai/branch-context/api";
2
2
  import { formatImplBranchContextCommand } from "../surfaces.ts";
3
- import type { ExecResult } from "@nseng-ai/foundation/command";
4
3
  import { setRuntimeStatus } from "@nseng-ai/pi/runtime/status";
5
- import type { ExtensionAPI, NewSessionOptions, NewSessionResult } from "../host-types.ts";
6
-
7
- export type BranchContextGtUpstackImplHost = Pick<ExtensionAPI, "exec">;
4
+ import type { GitGateway } from "@nseng-ai/capability-kit/git";
5
+ import type { NewSessionOptions, NewSessionResult } from "../host-types.ts";
8
6
 
9
7
  export type BranchContextGtUpstackImplNewSessionOptions = NewSessionOptions;
10
8
  export type BranchContextGtUpstackImplNewSessionResult = NewSessionResult;
@@ -26,7 +24,7 @@ export interface BranchContextGtUpstackImplContext {
26
24
  }
27
25
 
28
26
  export interface BranchContextGtUpstackImplLaunchOptions {
29
- host: BranchContextGtUpstackImplHost;
27
+ git: Pick<GitGateway, "checkout">;
30
28
  ctx: BranchContextGtUpstackImplContext;
31
29
  statusKey: string;
32
30
  target: Pick<BranchContextEvidence, "branch" | "key">;
@@ -47,8 +45,6 @@ export type BranchContextGtUpstackImplLaunchResult =
47
45
  parentSession?: string;
48
46
  };
49
47
 
50
- const CHECKOUT_TIMEOUT_MS = 30_000;
51
-
52
48
  export async function runBranchContextGtUpstackImplLaunch(
53
49
  options: BranchContextGtUpstackImplLaunchOptions,
54
50
  ): Promise<BranchContextGtUpstackImplLaunchResult> {
@@ -60,7 +56,7 @@ export async function runBranchContextGtUpstackImplLaunch(
60
56
  try {
61
57
  setRuntimeStatus(options.ctx, options.statusKey, "checking out branch context…");
62
58
  const checkout = await checkoutBranchContext({
63
- host: options.host,
59
+ git: options.git,
64
60
  cwd: options.ctx.cwd,
65
61
  targetBranch: branch,
66
62
  signal: options.signal,
@@ -118,7 +114,7 @@ export function formatBranchContextGtUpstackImplFollowUpFlow(
118
114
  type CheckoutResult = { type: "ok" } | { type: "failed"; message: string };
119
115
 
120
116
  interface CheckoutBranchContextOptions {
121
- host: BranchContextGtUpstackImplHost;
117
+ git: Pick<GitGateway, "checkout">;
122
118
  cwd: string;
123
119
  targetBranch: string;
124
120
  signal: AbortSignal | undefined;
@@ -127,28 +123,17 @@ interface CheckoutBranchContextOptions {
127
123
  async function checkoutBranchContext(
128
124
  options: CheckoutBranchContextOptions,
129
125
  ): Promise<CheckoutResult> {
130
- const result = await options.host.exec("git", ["checkout", options.targetBranch], {
126
+ const result = await options.git.checkout({
131
127
  cwd: options.cwd,
132
- timeout: CHECKOUT_TIMEOUT_MS,
128
+ branch: options.targetBranch,
133
129
  ...(options.signal === undefined ? {} : { signal: options.signal }),
134
130
  });
135
- if (result.code === 0) {
131
+ if (result.ok) {
136
132
  return { type: "ok" };
137
133
  }
138
134
 
139
- const output = formatCheckoutFailureOutput(result);
140
135
  return {
141
136
  type: "failed",
142
- message: `git checkout ${options.targetBranch} failed with exit code ${result.code}: ${output}`,
137
+ message: result.error.message,
143
138
  };
144
139
  }
145
-
146
- function formatCheckoutFailureOutput(result: ExecResult): string {
147
- if (result.stderr.length > 0) {
148
- return result.stderr;
149
- }
150
- if (result.stdout.length > 0) {
151
- return result.stdout;
152
- }
153
- return "(no output)";
154
- }
@@ -69,6 +69,7 @@ export interface BranchContextExtensionOptions {
69
69
  branchContextPrefix?: string;
70
70
  planStoreRoot?: string;
71
71
  branchContextOperations?: BranchContextOperations;
72
+ shouldResolveTargetBranchInPreview?: boolean;
72
73
  createBranchContextContext?: BranchContextContextFactory<[pi: ExtensionAPI, cwd: string]>;
73
74
  }
74
75
 
@@ -0,0 +1,73 @@
1
+ Plan audience and context contract:
2
+
3
+ - Treat the saved Markdown plan as the only planning context available to a completely fresh downstream implementation session.
4
+ - Make the plan self-contained. Do not rely on this conversation, hidden context, tool transcripts, or "as discussed" references.
5
+ - Embed all relevant context discovered during planning, including user goals, constraints, current behavior, important files/symbols/tests/docs, decisions made, rationale, rejected alternatives, assumptions, risks, and proportional validation guidance.
6
+ - Prefer concrete file paths, symbol names, command names, expected outcomes, and implementation order over vague instructions.
7
+ - If you inspected evidence during planning, summarize the discovered facts in the plan so the downstream agent does not need to rediscover them unless verification is required.
8
+
9
+ External research/context contract:
10
+
11
+ - If planning used anything outside the repository — web searches, external docs, GitHub issues/PRs, API docs, CLIs hitting remote services, local files outside the repo, or other non-repo resources — include the relevant findings inline in the saved plan.
12
+ - Do not merely link to external resources. Summarize the concrete facts, constraints, examples, decisions, and caveats the downstream agent needs.
13
+ - Include source/provenance where useful: URL, command, document name, issue/PR number, accessed date/time if known, and why it mattered.
14
+ - If external findings may become stale, mark what should be revalidated during implementation.
15
+ - Do not include secrets, credentials, private tokens, or unnecessary sensitive data.
16
+
17
+ <!-- PLAN-VERIFICATION-WORKSTREAM:START refactor-execution-strategy-guidance -->
18
+
19
+ Refactor execution strategy:
20
+
21
+ - If the implementation includes same-shape edits across multiple files, explicitly choose an execution mode in the plan.
22
+ - Apply the canonical guidance in \`skills/enriched-plan-save/references/refactor-execution-strategy.md\`, including the final stale-terminology grep/equivalent check when changing names or concepts.
23
+
24
+ <!-- PLAN-VERIFICATION-WORKSTREAM:END refactor-execution-strategy-guidance -->
25
+
26
+ Recommended saved plan sections:
27
+
28
+ - Goal and user-visible outcome.
29
+ - Planning context and discovered facts, including relevant repository state.
30
+ - External/off-repo research context, or a note that none was used when that helps remove ambiguity.
31
+ - Files, symbols, commands, and tests likely to change.
32
+ - Step-by-step implementation approach.
33
+ - Validation guidance and expected results. Do not over-specify routine test/check scope as a planning decision; leave ordinary validation coverage to the implementing agent's project policy and changed-file judgment.
34
+ - Risks, assumptions, edge cases, and open questions.
35
+
36
+ Workflow:
37
+
38
+ 1. Inspect the repository, documentation, and current conversation context as needed for the requested work.
39
+ 2. Produce a detailed Markdown implementation plan.
40
+ 3. Review the final Markdown plan content for completeness.
41
+ 4. Call write_saved_plan_file with the full Markdown content and optional one-sentence summary; do not generate or pass a slug.
42
+ 5. Report the saved plan evidence: file path, repo key, repo root, repo identity source, source branch, branch path segment, slug, slug model, and summary when present.
43
+ 6. Stop after reporting the saved plan evidence. Do not create a branch, write Branch Memory, or call any branch-context command/tool.
44
+
45
+ Local plan store contract:
46
+
47
+ - Canonical path convention: $XDG_STATE_HOME/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md, defaulting to $HOME/.local/state/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md. No fallback path is read or written; only $HOME/.local/state/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md is used.
48
+ - <repo>: for github.com origins, gh--<owner>--<repo> from sanitized GitHub owner and repo path segments; for non-GitHub or origin-less repos, one sanitized path segment from the normalized remote.origin.url or real repo root path
49
+ - <encoded-source-branch>: current branch at plan-file creation time encoded as one filesystem-safe path segment; branch slashes become --- (for example, branch-contexts/add-widget becomes branch-contexts---add-widget)
50
+ - <slug>: semantic kebab-case saved-plan filename slug without .md; this is a local plan-store locator, not necessarily the later implementation branch slug
51
+ - Existing saved plan file: write_saved_plan_file refuses to overwrite it; do not manually choose a replacement slug.
52
+ - Working-tree behavior: no checked-in plan file is created.
53
+
54
+ Saved-plan filename slug rules:
55
+
56
+ - write_saved_plan_file derives the final saved-plan filename slug from the final plan content through the Codex-backed slug model.
57
+ - Do not generate, guess, or pass a slug yourself.
58
+ - The derived slug is kebab-case, 3–7 words, specific to the work described by the final plan, and rejects dates, random IDs, and generic-only slugs.
59
+
60
+ When the plan is ready, call write_saved_plan_file with:
61
+
62
+ - content: the complete reviewed Markdown plan content
63
+ - summary: optional one-sentence summary of the plan
64
+
65
+ Exact tool call shape:
66
+ \`\`\`json
67
+ {
68
+ "content": "# Plan\\n...",
69
+ "summary": "One-sentence summary of the plan."
70
+ }
71
+ \`\`\`
72
+
73
+ If summary is not useful, omit it from the tool call rather than passing an empty string. Do not create target branches or write Branch Memory in this workflow.