@nseng-ai/branch-context 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@nseng-ai/branch-context",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "files": [
6
+ "src"
7
+ ],
8
+ "engines": {
9
+ "node": ">=24.12.0",
10
+ "pnpm": ">=11.8.0"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "exports": {
16
+ ".": "./src/core/index.ts",
17
+ "./api": "./src/api/index.ts",
18
+ "./api/prompt-assets": "./src/api/prompt-assets.ts",
19
+ "./ns/commands/attach": "./src/ns/commands/attach.ts",
20
+ "./ns/commands/check": "./src/ns/commands/check.ts",
21
+ "./ns/commands/delete": "./src/ns/commands/delete.ts",
22
+ "./ns/commands/from-plan": "./src/ns/commands/from-plan.ts",
23
+ "./ns/commands/list": "./src/ns/commands/list.ts",
24
+ "./ns/commands/load": "./src/ns/commands/load.ts",
25
+ "./testing": "./src/testing/index.ts",
26
+ "./repo-local-ns-extension": "./src/ns/repo-local-ns-extension.ts",
27
+ "./pi": "./src/pi/index.ts",
28
+ "./pi/extension": "./src/pi/extension.ts"
29
+ },
30
+ "dependencies": {
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",
38
+ "zod": "^4.4.3"
39
+ },
40
+ "peerDependencies": {
41
+ "@nseng-ai/pi": "*"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@nseng-ai/pi": {
45
+ "optional": true
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,43 @@
1
+ export {
2
+ BRANCH_CONTEXT_NAMESPACE,
3
+ buildBranchContextCreateOperation,
4
+ buildBranchContextPlanKey,
5
+ createBranchContextFromFile,
6
+ deriveTargetBranch,
7
+ formatBranchContextEvidence,
8
+ formatBranchContextCreateFailure,
9
+ formatBranchContextCreatePreview,
10
+ describeBranchContextGraphiteCreationSteps,
11
+ resolveBranchContextCreatePreviewContext,
12
+ type BranchContextCreateOperation,
13
+ type BranchContextEvidence,
14
+ type BranchCreationMethod,
15
+ } from "../core/branch-context-creation.ts";
16
+ export {
17
+ buildImplBranchContextPrompt,
18
+ formatLoadedAttachedPlanEvidence,
19
+ loadBranchContextPlan,
20
+ type LoadedAttachedPlan,
21
+ } from "../core/attached-plan.ts";
22
+ export {
23
+ createBranchContextContext,
24
+ createRealBranchContextContext,
25
+ type BranchContextContext,
26
+ type BranchContextContextFactory,
27
+ type BranchContextContextOptions,
28
+ } from "../core/context.ts";
29
+ export {
30
+ formatExistingBranchContextReuse,
31
+ resolveExistingBranchContextReuse,
32
+ type ExistingBranchContextReuse,
33
+ } from "../core/existing-branch-reuse.ts";
34
+ export {
35
+ buildPlanContentSlugPrompt,
36
+ derivePlanContentSlug,
37
+ type PlanContentSlugEvidence,
38
+ } from "../core/plan-content-slug.ts";
39
+ export {
40
+ buildBranchContextOutputMessage,
41
+ findLatestBranchContextEvidence,
42
+ type BranchContextOutputDetails,
43
+ } from "../core/session-artifact.ts";
@@ -0,0 +1,4 @@
1
+ export {
2
+ branchContextImplPromptTemplatePath,
3
+ branchContextImplPromptTemplateUrl,
4
+ } from "../core/prompt-assets.ts";
@@ -0,0 +1,455 @@
1
+ import { optionalEntry } from "@nseng-ai/foundation/primitives";
2
+
3
+ import {
4
+ BRANCH_CONTEXT_NAMESPACE,
5
+ UNSUPPORTED_ATTACHED_PLAN_KEY,
6
+ buildBranchContextPlanKey,
7
+ isSupportedBranchContextPlanKey,
8
+ } from "./constants.ts";
9
+ import { formatAvailableKeys, normalizeRequestedBranchContextKey } from "./attached-plan.ts";
10
+ import {
11
+ attachBranchContextPlan,
12
+ checkBranchContextEntryPresence,
13
+ deleteBranchContextPlan,
14
+ listBranchContextPlans,
15
+ throwBranchContextBrmemError,
16
+ type AttachedPlanEntry,
17
+ type BranchContextAttachData,
18
+ unwrapBranchContextBrmemResult,
19
+ } from "./branch-memory.ts";
20
+ import type { BranchContextContext } from "./context.ts";
21
+ import type { BrmemGateway } from "@nseng-ai/brmem";
22
+ import type { CommandExecApi } from "@nseng-ai/foundation/exec";
23
+ import type { GitGateway } from "@nseng-ai/capability-kit/git";
24
+ import {
25
+ buildPlanStoreOptions,
26
+ listSavedPlans,
27
+ resolvePlanSourceFile,
28
+ type PlanStoreOptions,
29
+ } from "@nseng-ai/plans";
30
+
31
+ export interface BranchContextPrimitiveOptions {
32
+ cwd: string;
33
+ context: BranchContextContext;
34
+ signal?: AbortSignal;
35
+ planStoreRoot?: string;
36
+ }
37
+
38
+ export interface AttachBranchContextParams {
39
+ key?: string;
40
+ filePath?: string;
41
+ planSlug?: string;
42
+ branch?: string;
43
+ }
44
+
45
+ export type BranchContextAttachEvidence = BranchContextAttachData & { planSlug?: string };
46
+
47
+ export interface BranchContextListEvidence {
48
+ branch: string;
49
+ entries: AttachedPlanEntry[];
50
+ }
51
+
52
+ export interface BranchContextCheckEvidence {
53
+ branch: string;
54
+ namespace: string;
55
+ key: string;
56
+ present: boolean;
57
+ }
58
+
59
+ export interface BranchContextDeleteEvidence {
60
+ branch: string;
61
+ namespace: string;
62
+ key: string;
63
+ deleted: boolean;
64
+ }
65
+
66
+ export interface AttachBranchContextOptions {
67
+ brmem: BrmemGateway;
68
+ cwd: string;
69
+ branch: string;
70
+ key: string;
71
+ sourceFile: string;
72
+ }
73
+
74
+ interface BrmemEntryAbsentOptions {
75
+ formatPresentMessage?: (context: { targetBranch: string; key: string }) => string;
76
+ }
77
+
78
+ interface BranchContextPrimitiveResolution {
79
+ git: GitGateway;
80
+ brmem: BrmemGateway;
81
+ branch: string;
82
+ }
83
+
84
+ export async function attachBranchContextEntry(
85
+ pi: CommandExecApi,
86
+ params: AttachBranchContextParams,
87
+ options: BranchContextPrimitiveOptions,
88
+ ): Promise<BranchContextAttachEvidence> {
89
+ const context = await resolveBranchContextPrimitiveResolution(options, params.branch);
90
+ const source = await resolveAttachSource(pi, params, options);
91
+ const before = await listBranchContextNamespace(context.brmem, context.branch);
92
+ assertBranchContextNamespaceSupported(context.branch, before);
93
+ await assertBrmemEntryAbsent(context.brmem, context.branch, source.key);
94
+ const data = await attachBranchContext({
95
+ brmem: context.brmem,
96
+ cwd: options.cwd,
97
+ branch: context.branch,
98
+ key: source.key,
99
+ sourceFile: source.sourceFile,
100
+ });
101
+ const after = await listBranchContextNamespace(context.brmem, context.branch);
102
+ assertAttachChangedNamespaceByOneKey({
103
+ branch: context.branch,
104
+ before,
105
+ after,
106
+ attachedKey: source.key,
107
+ });
108
+ return attachEvidence(data, source.planSlug);
109
+ }
110
+
111
+ export async function listBranchContextEntries(
112
+ params: { branch?: string },
113
+ options: BranchContextPrimitiveOptions,
114
+ ): Promise<BranchContextListEvidence> {
115
+ const context = await resolveBranchContextPrimitiveResolution(options, params.branch);
116
+ const entries = unwrapBranchContextBrmemResult(
117
+ await listBranchContextPlans(context.brmem, { branch: context.branch }),
118
+ );
119
+ return { branch: context.branch, entries };
120
+ }
121
+
122
+ export async function checkBranchContextEntry(
123
+ params: { key: string; branch?: string },
124
+ options: BranchContextPrimitiveOptions,
125
+ ): Promise<BranchContextCheckEvidence> {
126
+ const context = await resolveBranchContextPrimitiveResolution(options, params.branch);
127
+ const key = normalizeRequestedBranchContextKey(params.key);
128
+ const presence = await checkBranchContextEntryPresence(context.brmem, {
129
+ branch: context.branch,
130
+ key,
131
+ });
132
+ if (presence.type === "error") throwBranchContextBrmemError(presence.error);
133
+ return {
134
+ branch: context.branch,
135
+ namespace: BRANCH_CONTEXT_NAMESPACE,
136
+ key,
137
+ present: presence.type === "present",
138
+ };
139
+ }
140
+
141
+ export async function deleteBranchContextEntry(
142
+ params: { key: string; branch?: string },
143
+ options: BranchContextPrimitiveOptions,
144
+ ): Promise<BranchContextDeleteEvidence> {
145
+ const context = await resolveBranchContextPrimitiveResolution(options, params.branch);
146
+ const key = normalizeRequestedBranchContextKey(params.key);
147
+ unwrapBranchContextBrmemResult(
148
+ await deleteBranchContextPlan(context.brmem, {
149
+ branch: context.branch,
150
+ key,
151
+ }),
152
+ );
153
+ return { branch: context.branch, namespace: BRANCH_CONTEXT_NAMESPACE, key, deleted: true };
154
+ }
155
+
156
+ export async function assertBrmemEntryAbsent(
157
+ brmem: BrmemGateway,
158
+ targetBranch: string,
159
+ key: string,
160
+ options: BrmemEntryAbsentOptions = {},
161
+ ): Promise<void> {
162
+ const check = await checkBranchContextEntryPresence(brmem, { branch: targetBranch, key });
163
+ if (check.type === "absent") {
164
+ return;
165
+ }
166
+ if (check.type === "present") {
167
+ throw new Error(
168
+ options.formatPresentMessage?.({ targetBranch, key }) ??
169
+ formatDefaultBrmemEntryPresentMessage(targetBranch, key),
170
+ );
171
+ }
172
+ throwBranchContextBrmemError(check.error);
173
+ }
174
+
175
+ function formatDefaultBrmemEntryPresentMessage(targetBranch: string, key: string): string {
176
+ return [
177
+ "Attached plan already exists on target branch; refusing to overwrite.",
178
+ `Namespace: ${BRANCH_CONTEXT_NAMESPACE}`,
179
+ `Branch: ${targetBranch}`,
180
+ `Key: ${key}`,
181
+ ].join("\n");
182
+ }
183
+
184
+ export async function attachBranchContext(
185
+ options: AttachBranchContextOptions,
186
+ ): Promise<BranchContextAttachData> {
187
+ const attach = await attachBranchContextPlan(options.brmem, {
188
+ cwd: options.cwd,
189
+ branch: options.branch,
190
+ key: options.key,
191
+ sourceFile: options.sourceFile,
192
+ });
193
+ if (attach.ok) {
194
+ return attach.value;
195
+ }
196
+ throw new AttachBranchContextError(attach.error.code, attach.error.message);
197
+ }
198
+
199
+ export class AttachBranchContextError extends Error {
200
+ readonly code: string;
201
+
202
+ constructor(code: string, message: string) {
203
+ super(message);
204
+ this.name = "AttachBranchContextError";
205
+ this.code = code;
206
+ }
207
+ }
208
+
209
+ export class AttachBranchContextUsageError extends Error {
210
+ readonly code: string;
211
+
212
+ constructor(code: string, message: string) {
213
+ super(message);
214
+ this.name = "AttachBranchContextUsageError";
215
+ this.code = code;
216
+ }
217
+ }
218
+
219
+ export class BranchContextNamespaceInvalidError extends Error {
220
+ readonly code = "branch-context-namespace-invalid";
221
+ readonly branch: string;
222
+ readonly unsupportedKeys: readonly string[];
223
+
224
+ constructor(branch: string, unsupportedKeys: readonly string[]) {
225
+ super(
226
+ [
227
+ `Branch-context namespace on branch ${JSON.stringify(branch)} contains unsupported Entries; refusing to attach into an invalid namespace.`,
228
+ "",
229
+ "Unsupported keys:",
230
+ formatAvailableKeys(unsupportedKeys),
231
+ ].join("\n"),
232
+ );
233
+ this.name = "BranchContextNamespaceInvalidError";
234
+ this.branch = branch;
235
+ this.unsupportedKeys = [...unsupportedKeys];
236
+ }
237
+ }
238
+
239
+ export function formatAttachEvidence(evidence: BranchContextAttachEvidence): string {
240
+ return [
241
+ "Attached branch-context entry.",
242
+ `Branch: ${evidence.branch}`,
243
+ `Namespace: ${evidence.namespace}`,
244
+ `Key: ${evidence.key}`,
245
+ `Ref: ${evidence.refName}`,
246
+ `Commit: ${evidence.commit}`,
247
+ `Source file: ${evidence.sourceFile}`,
248
+ ...(evidence.planSlug === undefined ? [] : [`Plan slug: ${evidence.planSlug}`]),
249
+ ].join("\n");
250
+ }
251
+
252
+ export function formatListEvidence(branch: string, entries: readonly AttachedPlanEntry[]): string {
253
+ const lines = [`Branch context entries on ${branch}:`];
254
+ if (entries.length === 0) {
255
+ lines.push("(none)");
256
+ return lines.join("\n");
257
+ }
258
+ for (const entry of entries) {
259
+ const label =
260
+ entry.key === UNSUPPORTED_ATTACHED_PLAN_KEY
261
+ ? " (unsupported legacy plan key)"
262
+ : isSupportedBranchContextPlanKey(entry.key)
263
+ ? " (plan)"
264
+ : " (unsupported)";
265
+ lines.push(`- ${entry.key}${label}`);
266
+ }
267
+ return lines.join("\n");
268
+ }
269
+
270
+ export function formatCheckEvidence(evidence: BranchContextCheckEvidence): string {
271
+ return [
272
+ `Branch: ${evidence.branch}`,
273
+ `Namespace: ${evidence.namespace}`,
274
+ `Key: ${evidence.key}`,
275
+ `Present: ${evidence.present}`,
276
+ ].join("\n");
277
+ }
278
+
279
+ export function formatDeleteEvidence(evidence: BranchContextDeleteEvidence): string {
280
+ return [
281
+ `Deleted branch-context entry.`,
282
+ `Branch: ${evidence.branch}`,
283
+ `Namespace: ${evidence.namespace}`,
284
+ `Key: ${evidence.key}`,
285
+ ].join("\n");
286
+ }
287
+
288
+ async function resolveAttachSource(
289
+ pi: CommandExecApi,
290
+ params: AttachBranchContextParams,
291
+ options: BranchContextPrimitiveOptions,
292
+ ): Promise<{ key: string; sourceFile: string; planSlug?: string }> {
293
+ if (params.planSlug !== undefined) {
294
+ if (params.key !== undefined || params.filePath !== undefined) {
295
+ throw new AttachBranchContextUsageError(
296
+ "invalid-attach-source",
297
+ "Pass either --plan <slug> or <key> --file <path>, not both.",
298
+ );
299
+ }
300
+ const available = await listSavedPlans(pi, planStoreOptions(options));
301
+ const matches = available.filter((plan) => plan.slug === params.planSlug);
302
+ if (matches.length === 0) {
303
+ throw new Error(
304
+ [
305
+ `No saved plan found for slug \`${params.planSlug}\`.`,
306
+ "",
307
+ "Available slugs:",
308
+ ...available.map((plan) => `- ${plan.slug}`),
309
+ ].join("\n"),
310
+ );
311
+ }
312
+ if (matches.length > 1) {
313
+ throw new Error(
314
+ [
315
+ `Multiple saved plans found for slug \`${params.planSlug}\`; choose a file explicitly.`,
316
+ "",
317
+ ...matches.map((plan) => `- ${plan.branchKey}: ${plan.filePath}`),
318
+ ].join("\n"),
319
+ );
320
+ }
321
+ const match = matches[0]!;
322
+ return {
323
+ key: buildBranchContextPlanKey(match.slug),
324
+ sourceFile: match.filePath,
325
+ planSlug: match.slug,
326
+ };
327
+ }
328
+ if (params.key === undefined || params.filePath === undefined) {
329
+ throw new AttachBranchContextUsageError(
330
+ "missing-attach-source",
331
+ "Attach requires either --plan <slug> or <key> --file <path>.",
332
+ );
333
+ }
334
+ const key = normalizeRequestedBranchContextKey(params.key);
335
+ if (key === UNSUPPORTED_ATTACHED_PLAN_KEY) {
336
+ throw new AttachBranchContextUsageError(
337
+ "unsupported-attach-key",
338
+ "Legacy branch-context key plan.md is no longer supported; attach the plan under a named Markdown key such as <slug>.md.",
339
+ );
340
+ }
341
+ if (!isSupportedBranchContextPlanKey(key)) {
342
+ throw new AttachBranchContextUsageError(
343
+ "unsupported-attach-key",
344
+ "Branch-context attach keys must be named Markdown plan keys derived from a valid plan slug, such as <slug>.md.",
345
+ );
346
+ }
347
+ return {
348
+ key,
349
+ sourceFile: await resolvePlanSourceFile(pi, {
350
+ cwd: options.cwd,
351
+ rawFilePath: params.filePath,
352
+ ...optionalEntry("signal", options.signal),
353
+ git: options.context.git,
354
+ }),
355
+ };
356
+ }
357
+
358
+ async function resolveBranchContextPrimitiveResolution(
359
+ options: BranchContextPrimitiveOptions,
360
+ requestedBranch: string | undefined,
361
+ ): Promise<BranchContextPrimitiveResolution> {
362
+ const branch = await resolveAttachBranch(options.context.git, options, requestedBranch);
363
+ return { git: options.context.git, brmem: options.context.brmem, branch };
364
+ }
365
+
366
+ async function resolveAttachBranch(
367
+ git: GitGateway,
368
+ options: BranchContextPrimitiveOptions,
369
+ requestedBranch: string | undefined,
370
+ ): Promise<string> {
371
+ const branch = requestedBranch?.trim();
372
+ if (branch !== undefined && branch.length > 0) return branch;
373
+ const current = await git.currentBranch({ cwd: options.cwd, signal: options.signal });
374
+ if (current.type === "branch") return current.branch;
375
+ if (current.type === "detached") {
376
+ throw new Error(
377
+ "Cannot default branch-context operation from detached HEAD. Pass --branch explicitly.",
378
+ );
379
+ }
380
+ throw new Error(
381
+ [
382
+ "Cannot default branch-context operation because the current branch could not be resolved. Pass --branch explicitly.",
383
+ "",
384
+ current.error.message,
385
+ ].join("\n"),
386
+ );
387
+ }
388
+
389
+ async function listBranchContextNamespace(
390
+ brmem: BrmemGateway,
391
+ branch: string,
392
+ ): Promise<AttachedPlanEntry[]> {
393
+ return unwrapBranchContextBrmemResult(await listBranchContextPlans(brmem, { branch }));
394
+ }
395
+
396
+ function assertBranchContextNamespaceSupported(
397
+ branch: string,
398
+ entries: readonly AttachedPlanEntry[],
399
+ ): void {
400
+ const unsupportedKeys = entries
401
+ .map((entry) => entry.key)
402
+ .filter((key) => !isSupportedBranchContextPlanKey(key))
403
+ .sort();
404
+ if (unsupportedKeys.length === 0) return;
405
+ throw new BranchContextNamespaceInvalidError(branch, unsupportedKeys);
406
+ }
407
+
408
+ interface AssertAttachChangedNamespaceByOneKeyOptions {
409
+ branch: string;
410
+ before: readonly AttachedPlanEntry[];
411
+ after: readonly AttachedPlanEntry[];
412
+ attachedKey: string;
413
+ }
414
+
415
+ function assertAttachChangedNamespaceByOneKey(
416
+ options: AssertAttachChangedNamespaceByOneKeyOptions,
417
+ ): void {
418
+ assertBranchContextNamespaceSupported(options.branch, options.after);
419
+ const expectedKeys = new Set(options.before.map((entry) => entry.key));
420
+ expectedKeys.add(options.attachedKey);
421
+ const actualKeys = new Set(options.after.map((entry) => entry.key));
422
+ if (
423
+ expectedKeys.size === actualKeys.size &&
424
+ [...expectedKeys].every((key) => actualKeys.has(key))
425
+ ) {
426
+ return;
427
+ }
428
+ throw new AttachBranchContextError(
429
+ "branch-context-namespace-invalid",
430
+ [
431
+ `Branch-context attach did not change namespace by exactly the intended key on branch ${JSON.stringify(options.branch)}.`,
432
+ `Expected keys: ${formatAvailableKeys([...expectedKeys].sort())}`,
433
+ `Actual keys: ${formatAvailableKeys([...actualKeys].sort())}`,
434
+ ].join("\n"),
435
+ );
436
+ }
437
+
438
+ function planStoreOptions(options: BranchContextPrimitiveOptions): PlanStoreOptions {
439
+ return buildPlanStoreOptions({
440
+ cwd: options.cwd,
441
+ git: options.context.git,
442
+ planStoreRoot: options.planStoreRoot,
443
+ signal: options.signal,
444
+ });
445
+ }
446
+
447
+ function attachEvidence(
448
+ data: BranchContextAttachData,
449
+ planSlug: string | undefined,
450
+ ): BranchContextAttachEvidence {
451
+ return {
452
+ ...data,
453
+ ...optionalEntry("planSlug", planSlug),
454
+ };
455
+ }