@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 +7 -7
- package/src/api/index.ts +4 -0
- package/src/core/branch-context-creation.ts +214 -25
- package/src/core/context.ts +5 -2
- package/src/core/operations.ts +25 -1
- package/src/core/plan-content-slug.ts +2 -2
- package/src/ns/command.ts +1 -1
- package/src/ns/commands/attach.ts +1 -1
- package/src/ns/commands/check.ts +1 -1
- package/src/ns/commands/delete.ts +1 -1
- package/src/ns/commands/from-plan.ts +1 -1
- package/src/ns/commands/list.ts +1 -1
- package/src/ns/commands/load.ts +1 -1
- package/src/ns/repo-local-ns-extension.ts +1 -1
- package/src/pi/enriched-plan-save.ts +30 -92
- package/src/pi/from-plan-commands.ts +51 -9
- package/src/pi/gt/git-gateway.ts +11 -0
- package/src/pi/gt/upstack-impl-launch.ts +9 -24
- package/src/pi/host-types.ts +1 -0
- package/src/pi/prompts/plans-write-default.md +73 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nseng-ai/branch-context",
|
|
3
|
-
"version": "0.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.
|
|
33
|
-
"@nseng-ai/capability-kit": "0.1.
|
|
34
|
-
"@nseng-ai/clinkr": "0.1.
|
|
35
|
-
"@nseng-ai/foundation": "0.1.
|
|
36
|
-
"@nseng-ai/
|
|
37
|
-
"@nseng-ai/plans": "0.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 {
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
375
|
+
interface SelectedBranchAvailabilityOptions extends BranchContextRepoAccess {
|
|
376
|
+
operation: BranchContextCreateOperation;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function assertSelectedTargetBranchStillAvailable(
|
|
380
|
+
options: SelectedBranchAvailabilityOptions,
|
|
300
381
|
): Promise<void> {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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: ${
|
|
310
|
-
`Ref: ${
|
|
311
|
-
`Command: ${
|
|
395
|
+
`Branch: ${options.operation.branch}`,
|
|
396
|
+
`Ref: ${occupancy.localBranch.refName}`,
|
|
397
|
+
`Command: ${occupancy.localBranch.displayCommand}`,
|
|
312
398
|
].join("\n"),
|
|
313
399
|
);
|
|
314
400
|
}
|
|
315
|
-
|
|
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) {
|
package/src/core/context.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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,
|
package/src/core/operations.ts
CHANGED
|
@@ -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 =
|
|
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:
|
|
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/
|
|
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/
|
|
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";
|
package/src/ns/commands/check.ts
CHANGED
package/src/ns/commands/list.ts
CHANGED
package/src/ns/commands/load.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineExtension } from "@nseng-ai/
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
129
|
+
return await readWritePlanPromptBody(repoRoot.path);
|
|
185
130
|
} catch (error) {
|
|
186
131
|
return fallbackWritePlanPromptBody(
|
|
187
|
-
`
|
|
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
|
|
212
|
-
repoRoot:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
126
|
+
const result = await options.git.checkout({
|
|
131
127
|
cwd: options.cwd,
|
|
132
|
-
|
|
128
|
+
branch: options.targetBranch,
|
|
133
129
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
134
130
|
});
|
|
135
|
-
if (result.
|
|
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:
|
|
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
|
-
}
|
package/src/pi/host-types.ts
CHANGED
|
@@ -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.
|