@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.
@@ -0,0 +1,590 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+
4
+ import { failure, ok, usageError, type ClinkrExit } from "@nseng-ai/clinkr";
5
+ import { runOperationCommand, type CliEntrypointDeps } from "@nseng-ai/foundation/cli-runtime";
6
+ import {
7
+ formatErrorMessage,
8
+ optionalEntries,
9
+ optionalEntry,
10
+ } from "@nseng-ai/foundation/primitives";
11
+ import { normalizePlanFilePath, validatePlanSlug } from "@nseng-ai/plans";
12
+ import { z } from "zod";
13
+
14
+ import {
15
+ AttachBranchContextError,
16
+ AttachBranchContextUsageError,
17
+ BranchContextNamespaceInvalidError,
18
+ attachBranchContextEntry,
19
+ checkBranchContextEntry,
20
+ deleteBranchContextEntry,
21
+ formatAttachEvidence,
22
+ formatCheckEvidence,
23
+ formatDeleteEvidence,
24
+ formatListEvidence,
25
+ listBranchContextEntries,
26
+ type BranchContextAttachEvidence,
27
+ type BranchContextCheckEvidence,
28
+ type BranchContextDeleteEvidence,
29
+ type BranchContextListEvidence,
30
+ } from "./attach.ts";
31
+ import {
32
+ AmbiguousBranchContextPlanEntryError,
33
+ NoAttachedBranchContextEntriesError,
34
+ NoSupportedBranchContextPlanEntriesError,
35
+ RequestedBranchContextPlanKeyNotFoundError,
36
+ SavedPlanFallbackLoadError,
37
+ UnsupportedBranchContextPlanKeyError,
38
+ buildImplBranchContextPrompt,
39
+ formatLoadedAttachedPlanEvidence,
40
+ loadBranchContextPlan,
41
+ type LoadedAttachedPlan,
42
+ } from "./attached-plan.ts";
43
+ import {
44
+ BRANCH_CREATION_METHODS,
45
+ createBranchContextFromFile,
46
+ DEFAULT_BRANCH_CREATION_METHOD,
47
+ formatBranchContextEvidence,
48
+ type BranchContextEvidence,
49
+ } from "./branch-context-creation.ts";
50
+ import { createRealBranchContextContext, type BranchContextContext } from "./context.ts";
51
+
52
+ type BranchContextOperation = "create" | "load" | "attach" | "list" | "check" | "delete";
53
+
54
+ export const createRequestSchema = z.object({
55
+ slug: z.string().describe("Branch context slug."),
56
+ planFile: z.string().describe("Plan file path (must live outside the repository)."),
57
+ branch: z.string().optional().describe("Branch name (defaults to the slug)."),
58
+ branchCreation: z
59
+ .enum(BRANCH_CREATION_METHODS)
60
+ .default(DEFAULT_BRANCH_CREATION_METHOD)
61
+ .describe("Branch creation method."),
62
+ summary: z.string().optional().describe("Optional plan summary."),
63
+ });
64
+
65
+ export const loadRequestSchema = z.object({
66
+ key: z.string().optional().describe("Branch-context key (defaults to the only attached entry)."),
67
+ promptFile: z.string().optional().describe("Write the implementation prompt to this file."),
68
+ includeContent: z
69
+ .boolean()
70
+ .optional()
71
+ .describe("Include the branch-context entry content in JSON output."),
72
+ includePrompt: z
73
+ .boolean()
74
+ .optional()
75
+ .describe("Include the implementation prompt in JSON output."),
76
+ });
77
+
78
+ export const attachRequestSchema = z.object({
79
+ key: z.string().optional().describe("Entry key for --file form."),
80
+ file: z.string().optional().describe("File to attach for arbitrary-key form."),
81
+ plan: z.string().optional().describe("Saved plan slug to attach as <slug>.md."),
82
+ branch: z.string().optional().describe("Target branch (defaults to current branch)."),
83
+ });
84
+
85
+ export const listRequestSchema = z.object({
86
+ branch: z.string().optional().describe("Branch to list (defaults to current branch)."),
87
+ });
88
+
89
+ export const keyRequestSchema = z.object({
90
+ key: z.string().describe("Branch-context entry key."),
91
+ branch: z.string().optional().describe("Target branch (defaults to current branch)."),
92
+ });
93
+
94
+ export type CreateRequest = z.infer<typeof createRequestSchema>;
95
+ export type LoadRequest = z.infer<typeof loadRequestSchema>;
96
+ export type AttachRequest = z.infer<typeof attachRequestSchema>;
97
+ export type ListRequest = z.infer<typeof listRequestSchema>;
98
+ export type KeyRequest = z.infer<typeof keyRequestSchema>;
99
+
100
+ export type BranchContextData = ReturnType<typeof branchContextJson>;
101
+ export type LoadPlanData = ReturnType<typeof loadedPlanJson>;
102
+ export type AttachData = ReturnType<typeof attachJson>;
103
+ export type ListData = ReturnType<typeof listJson>;
104
+ export type CheckData = ReturnType<typeof checkJson>;
105
+ export type DeleteData = ReturnType<typeof deleteJson>;
106
+
107
+ export const branchContextResultSchema = z.object({
108
+ slug: z.string(),
109
+ branch: z.string(),
110
+ branchCreation: z.enum(BRANCH_CREATION_METHODS),
111
+ startPoint: z.string(),
112
+ namespace: z.string(),
113
+ key: z.string(),
114
+ refName: z.string(),
115
+ commit: z.string(),
116
+ sourceFile: z.string(),
117
+ summary: z.string().optional(),
118
+ });
119
+
120
+ export const loadPlanResultSchema = z.object({
121
+ branch: z.string(),
122
+ namespace: z.string(),
123
+ selectedKey: z.string(),
124
+ refName: z.string(),
125
+ byteCount: z.number(),
126
+ availableKeys: z.array(z.string()),
127
+ source: z.union([z.literal("attached"), z.literal("saved")]),
128
+ sourceFile: z.string().optional(),
129
+ implementationPromptFile: z.string().optional(),
130
+ attachedPlanContent: z.string().optional(),
131
+ implementationPrompt: z.string().optional(),
132
+ });
133
+
134
+ export const attachResultSchema = z.object({
135
+ branch: z.string(),
136
+ namespace: z.string(),
137
+ key: z.string(),
138
+ refName: z.string(),
139
+ commit: z.string(),
140
+ sourceFile: z.string(),
141
+ planSlug: z.string().optional(),
142
+ });
143
+
144
+ export const listResultSchema = z.object({
145
+ branch: z.string(),
146
+ entries: z.array(
147
+ z.object({
148
+ namespace: z.string(),
149
+ key: z.string(),
150
+ branch: z.string(),
151
+ refName: z.string(),
152
+ }),
153
+ ),
154
+ });
155
+
156
+ export const checkResultSchema = z.object({
157
+ branch: z.string(),
158
+ namespace: z.string(),
159
+ key: z.string(),
160
+ present: z.boolean(),
161
+ });
162
+
163
+ export const deleteResultSchema = z.object({
164
+ branch: z.string(),
165
+ namespace: z.string(),
166
+ key: z.string(),
167
+ deleted: z.boolean(),
168
+ });
169
+
170
+ export function createRealBranchContextCliContext(options: {
171
+ cwd: string;
172
+ env?: NodeJS.ProcessEnv;
173
+ stderr?: (text: string) => void;
174
+ planStoreRoot?: string;
175
+ }): BranchContextCliContext {
176
+ return {
177
+ context: createRealBranchContextContext({
178
+ cwd: options.cwd,
179
+ ...(options.env === undefined ? {} : { env: options.env }),
180
+ ...(options.stderr === undefined ? {} : { stderr: options.stderr }),
181
+ }),
182
+ cwd: options.cwd,
183
+ ...(options.planStoreRoot === undefined ? {} : { planStoreRoot: options.planStoreRoot }),
184
+ };
185
+ }
186
+
187
+ export interface CliDeps extends Pick<CliEntrypointDeps, "cwd" | "stdout" | "stderr"> {
188
+ context?: BranchContextContext;
189
+ planStoreRoot?: string;
190
+ }
191
+
192
+ export interface BranchContextCliContext {
193
+ context: BranchContextContext;
194
+ cwd: string;
195
+ planStoreRoot?: string;
196
+ }
197
+
198
+ export async function handleCreate(
199
+ ctx: BranchContextCliContext,
200
+ request: CreateRequest,
201
+ ): Promise<ClinkrExit<BranchContextData>> {
202
+ return await runOperationCommand({
203
+ operation: "create",
204
+ action: async () => {
205
+ const slugError = validatePlanSlug(request.slug);
206
+ if (slugError !== undefined) {
207
+ return usageError(`Invalid branch context slug: ${slugError}`, {
208
+ code: "invalid-slug",
209
+ argument: "slug",
210
+ reason: slugError,
211
+ });
212
+ }
213
+ const evidence = await createBranchContextFromFile(
214
+ ctx.context.commands,
215
+ {
216
+ slug: request.slug,
217
+ filePath: request.planFile,
218
+ ...(request.branch === undefined ? {} : { branchName: request.branch }),
219
+ branchCreation: request.branchCreation,
220
+ ...(request.summary === undefined ? {} : { summary: request.summary }),
221
+ },
222
+ operationOptions(ctx),
223
+ );
224
+ return ok(branchContextJson(evidence), { human: formatBranchContextEvidence(evidence) });
225
+ },
226
+ failureFromError: branchContextExitFromError,
227
+ });
228
+ }
229
+
230
+ export async function handleLoad(
231
+ ctx: BranchContextCliContext,
232
+ request: LoadRequest,
233
+ ): Promise<ClinkrExit<LoadPlanData>> {
234
+ return await runOperationCommand({
235
+ operation: "load",
236
+ action: async () => {
237
+ const requestedKey = request.key;
238
+ const plan = await loadBranchContextPlan(
239
+ ctx.context.commands,
240
+ requestedKey === undefined ? {} : { requestedKey },
241
+ operationOptions(ctx),
242
+ );
243
+ const promptFile =
244
+ request.promptFile === undefined ? undefined : normalizePlanFilePath(request.promptFile);
245
+ const implementationPrompt = buildImplBranchContextPrompt(plan);
246
+ if (promptFile !== undefined) {
247
+ await mkdir(dirname(promptFile), { recursive: true });
248
+ await writeFile(promptFile, implementationPrompt, "utf8");
249
+ }
250
+ const data = loadedPlanJson(plan, {
251
+ ...optionalEntry("promptFile", promptFile),
252
+ ...(request.includeContent === true ? { attachedPlanContent: plan.content } : {}),
253
+ ...(request.includePrompt === true ? { implementationPrompt } : {}),
254
+ });
255
+ return ok(data, { human: formatLoadPlanHuman(plan, promptFile, implementationPrompt) });
256
+ },
257
+ failureFromError: branchContextExitFromError,
258
+ });
259
+ }
260
+
261
+ export async function handleAttach(
262
+ ctx: BranchContextCliContext,
263
+ request: AttachRequest,
264
+ ): Promise<ClinkrExit<AttachData>> {
265
+ return await runOperationCommand({
266
+ operation: "attach",
267
+ action: async () => {
268
+ const evidence = await attachBranchContextEntry(
269
+ ctx.context.commands,
270
+ {
271
+ ...optionalEntries({
272
+ key: request.key,
273
+ filePath: request.file,
274
+ planSlug: request.plan,
275
+ branch: request.branch,
276
+ }),
277
+ },
278
+ operationOptions(ctx),
279
+ );
280
+ return ok(attachJson(evidence), { human: formatAttachEvidence(evidence) });
281
+ },
282
+ failureFromError: branchContextExitFromError,
283
+ });
284
+ }
285
+
286
+ export async function handleList(
287
+ ctx: BranchContextCliContext,
288
+ request: ListRequest,
289
+ ): Promise<ClinkrExit<ListData>> {
290
+ return await runOperationCommand({
291
+ operation: "list",
292
+ action: async () => {
293
+ const list = await listBranchContextEntries(
294
+ request.branch === undefined ? {} : { branch: request.branch },
295
+ operationOptions(ctx),
296
+ );
297
+ return ok(listJson(list), { human: formatListEvidence(list.branch, list.entries) });
298
+ },
299
+ failureFromError: branchContextExitFromError,
300
+ });
301
+ }
302
+
303
+ export async function handleCheck(
304
+ ctx: BranchContextCliContext,
305
+ request: KeyRequest,
306
+ ): Promise<ClinkrExit<CheckData>> {
307
+ return await runOperationCommand({
308
+ operation: "check",
309
+ action: async () => {
310
+ const evidence = await checkBranchContextEntry(
311
+ {
312
+ key: request.key,
313
+ ...optionalEntry("branch", request.branch),
314
+ },
315
+ operationOptions(ctx),
316
+ );
317
+ return ok(checkJson(evidence), { human: formatCheckEvidence(evidence) });
318
+ },
319
+ failureFromError: branchContextExitFromError,
320
+ });
321
+ }
322
+
323
+ export async function handleDelete(
324
+ ctx: BranchContextCliContext,
325
+ request: KeyRequest,
326
+ ): Promise<ClinkrExit<DeleteData>> {
327
+ return await runOperationCommand({
328
+ operation: "delete",
329
+ action: async () => {
330
+ const evidence = await deleteBranchContextEntry(
331
+ {
332
+ key: request.key,
333
+ ...optionalEntry("branch", request.branch),
334
+ },
335
+ operationOptions(ctx),
336
+ );
337
+ return ok(deleteJson(evidence), { human: formatDeleteEvidence(evidence) });
338
+ },
339
+ failureFromError: branchContextExitFromError,
340
+ });
341
+ }
342
+
343
+ function branchContextExitFromError(
344
+ operation: BranchContextOperation,
345
+ error: unknown,
346
+ ): ClinkrExit<never> {
347
+ if (error instanceof AttachBranchContextUsageError) {
348
+ return usageError(error.message, { code: error.code });
349
+ }
350
+ const classification = classifyBranchContextError(error);
351
+ return failure(branchContextErrorType(operation), formatErrorMessage(error), {
352
+ code: classification.code,
353
+ ...classification.data,
354
+ });
355
+ }
356
+
357
+ function branchContextErrorType(operation: BranchContextOperation): string {
358
+ switch (operation) {
359
+ case "create":
360
+ return "branch-context-create-failed";
361
+ case "load":
362
+ return "branch-context-load-failed";
363
+ case "attach":
364
+ return "branch-context-attach-failed";
365
+ case "list":
366
+ return "branch-context-list-failed";
367
+ case "check":
368
+ return "branch-context-check-failed";
369
+ case "delete":
370
+ return "branch-context-delete-failed";
371
+ }
372
+ }
373
+
374
+ interface BranchContextErrorClassification {
375
+ code: string;
376
+ data: Record<string, unknown>;
377
+ }
378
+
379
+ function classifyBranchContextError(error: unknown): BranchContextErrorClassification {
380
+ if (error instanceof NoAttachedBranchContextEntriesError) {
381
+ return { code: "no-attached-entries", data: { branch: error.branch } };
382
+ }
383
+ if (error instanceof AmbiguousBranchContextPlanEntryError) {
384
+ return {
385
+ code: "ambiguous-attached-plan",
386
+ data: { branch: error.branch, availableKeys: error.availableKeys },
387
+ };
388
+ }
389
+ if (error instanceof UnsupportedBranchContextPlanKeyError) {
390
+ return {
391
+ code: "unsupported-attached-plan-key",
392
+ data: { branch: error.branch, key: error.key },
393
+ };
394
+ }
395
+ if (error instanceof NoSupportedBranchContextPlanEntriesError) {
396
+ return {
397
+ code: "no-supported-attached-plans",
398
+ data: { branch: error.branch, availableKeys: error.availableKeys },
399
+ };
400
+ }
401
+ if (error instanceof RequestedBranchContextPlanKeyNotFoundError) {
402
+ return {
403
+ code: "attached-plan-key-not-found",
404
+ data: {
405
+ branch: error.branch,
406
+ key: error.key,
407
+ availableKeys: error.availableKeys,
408
+ supportedKeys: error.supportedKeys,
409
+ },
410
+ };
411
+ }
412
+ if (error instanceof SavedPlanFallbackLoadError) {
413
+ return {
414
+ code: "fallback-resolution-failed",
415
+ data: {
416
+ branch: error.branch,
417
+ attachedMessage: error.attachedMessage,
418
+ fallbackMessage: error.fallbackMessage,
419
+ },
420
+ };
421
+ }
422
+ if (error instanceof BranchContextNamespaceInvalidError) {
423
+ return {
424
+ code: error.code,
425
+ data: { branch: error.branch, unsupportedKeys: error.unsupportedKeys },
426
+ };
427
+ }
428
+ if (error instanceof AttachBranchContextError) {
429
+ return { code: normalizeErrorCode(error.code), data: {} };
430
+ }
431
+ return { code: "unexpected-error", data: {} };
432
+ }
433
+
434
+ function normalizeErrorCode(code: string): string {
435
+ return code.replaceAll("_", "-");
436
+ }
437
+
438
+ function operationOptions(ctx: BranchContextCliContext) {
439
+ return {
440
+ cwd: ctx.cwd,
441
+ context: ctx.context,
442
+ ...(ctx.planStoreRoot === undefined ? {} : { planStoreRoot: ctx.planStoreRoot }),
443
+ };
444
+ }
445
+
446
+ function formatLoadPlanHuman(
447
+ plan: LoadedAttachedPlan,
448
+ promptFile: string | undefined,
449
+ implementationPrompt: string,
450
+ ): string {
451
+ if (promptFile !== undefined) {
452
+ return `${formatLoadedAttachedPlanEvidence(plan)}\nImplementation prompt file: ${promptFile}`;
453
+ }
454
+ return `${formatLoadedAttachedPlanEvidence(plan)}\n\n${implementationPrompt}`;
455
+ }
456
+
457
+ function branchContextJson(evidence: BranchContextEvidence): {
458
+ slug: string;
459
+ branch: string;
460
+ branchCreation: BranchContextEvidence["branchCreation"];
461
+ startPoint: string;
462
+ namespace: string;
463
+ key: string;
464
+ refName: string;
465
+ commit: string;
466
+ sourceFile: string;
467
+ summary?: string;
468
+ } {
469
+ return {
470
+ slug: evidence.slug,
471
+ branch: evidence.branch,
472
+ branchCreation: evidence.branchCreation,
473
+ startPoint: evidence.startPoint,
474
+ namespace: evidence.namespace,
475
+ key: evidence.key,
476
+ refName: evidence.refName,
477
+ commit: evidence.commit,
478
+ sourceFile: evidence.sourceFile,
479
+ ...(evidence.summary === undefined ? {} : { summary: evidence.summary }),
480
+ };
481
+ }
482
+
483
+ interface LoadedPlanJsonOptions {
484
+ promptFile?: string;
485
+ attachedPlanContent?: string;
486
+ implementationPrompt?: string;
487
+ }
488
+
489
+ function listJson(list: BranchContextListEvidence): {
490
+ branch: string;
491
+ entries: {
492
+ namespace: string;
493
+ key: string;
494
+ branch: string;
495
+ refName: string;
496
+ }[];
497
+ } {
498
+ return {
499
+ branch: list.branch,
500
+ entries: list.entries.map((entry) => ({
501
+ namespace: entry.namespace,
502
+ key: entry.key,
503
+ branch: entry.branch,
504
+ refName: entry.refName,
505
+ })),
506
+ };
507
+ }
508
+
509
+ function attachJson(evidence: BranchContextAttachEvidence): {
510
+ branch: string;
511
+ namespace: string;
512
+ key: string;
513
+ refName: string;
514
+ commit: string;
515
+ sourceFile: string;
516
+ planSlug?: string;
517
+ } {
518
+ return {
519
+ branch: evidence.branch,
520
+ namespace: evidence.namespace,
521
+ key: evidence.key,
522
+ refName: evidence.refName,
523
+ commit: evidence.commit,
524
+ sourceFile: evidence.sourceFile,
525
+ ...(evidence.planSlug === undefined ? {} : { planSlug: evidence.planSlug }),
526
+ };
527
+ }
528
+
529
+ function checkJson(evidence: BranchContextCheckEvidence): {
530
+ branch: string;
531
+ namespace: string;
532
+ key: string;
533
+ present: boolean;
534
+ } {
535
+ return {
536
+ branch: evidence.branch,
537
+ namespace: evidence.namespace,
538
+ key: evidence.key,
539
+ present: evidence.present,
540
+ };
541
+ }
542
+
543
+ function deleteJson(evidence: BranchContextDeleteEvidence): {
544
+ branch: string;
545
+ namespace: string;
546
+ key: string;
547
+ deleted: boolean;
548
+ } {
549
+ return {
550
+ branch: evidence.branch,
551
+ namespace: evidence.namespace,
552
+ key: evidence.key,
553
+ deleted: evidence.deleted,
554
+ };
555
+ }
556
+
557
+ function loadedPlanJson(
558
+ plan: LoadedAttachedPlan,
559
+ options: LoadedPlanJsonOptions = {},
560
+ ): {
561
+ branch: string;
562
+ namespace: string;
563
+ selectedKey: string;
564
+ refName: string;
565
+ byteCount: number;
566
+ availableKeys: string[];
567
+ source: LoadedAttachedPlan["source"];
568
+ sourceFile?: string;
569
+ implementationPromptFile?: string;
570
+ attachedPlanContent?: string;
571
+ implementationPrompt?: string;
572
+ } {
573
+ return {
574
+ branch: plan.branch,
575
+ namespace: plan.namespace,
576
+ selectedKey: plan.selectedKey,
577
+ refName: plan.refName,
578
+ byteCount: plan.byteCount,
579
+ availableKeys: plan.availableKeys,
580
+ source: plan.source,
581
+ ...(plan.sourceFile === undefined ? {} : { sourceFile: plan.sourceFile }),
582
+ ...(options.promptFile === undefined ? {} : { implementationPromptFile: options.promptFile }),
583
+ ...(options.attachedPlanContent === undefined
584
+ ? {}
585
+ : { attachedPlanContent: options.attachedPlanContent }),
586
+ ...(options.implementationPrompt === undefined
587
+ ? {}
588
+ : { implementationPrompt: options.implementationPrompt }),
589
+ };
590
+ }
@@ -0,0 +1,50 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ import type { CommandExecApi } from "@nseng-ai/foundation/exec";
4
+ import {
5
+ buildContentSlugPrompt,
6
+ deriveContentSlug,
7
+ type ContentSlugDerivationVariant,
8
+ type ContentSlugEvidence,
9
+ } from "@nseng-ai/plans";
10
+
11
+ export type PlanContentSlugEvidence = ContentSlugEvidence;
12
+
13
+ export interface DerivePlanContentSlugInput {
14
+ filePath: string;
15
+ cwd: string;
16
+ signal?: AbortSignal;
17
+ readTextFile?: (path: string) => Promise<string>;
18
+ }
19
+
20
+ const PLAN_CONTENT_SLUG_VARIANT: ContentSlugDerivationVariant = {
21
+ slugKind: "branch-context slug",
22
+ promptIntroLines: [
23
+ "Generate the branch-context slug for the Markdown implementation plan content below.",
24
+ "Use only the plan content. Do not use any saved-plan filename or path.",
25
+ ],
26
+ invalidSlugMessage: "Pi slug model output normalized to an invalid branch-context slug.",
27
+ failureHeader: "Failed to derive branch-context slug from plan content.",
28
+ noFallbackLine: "No filename or deterministic fallback was attempted.",
29
+ };
30
+
31
+ export async function derivePlanContentSlug(
32
+ pi: CommandExecApi,
33
+ input: DerivePlanContentSlugInput,
34
+ ): Promise<PlanContentSlugEvidence> {
35
+ const readTextFile = input.readTextFile ?? defaultReadTextFile;
36
+ const content = await readTextFile(input.filePath);
37
+ return deriveContentSlug(
38
+ pi,
39
+ { content, cwd: input.cwd, ...(input.signal === undefined ? {} : { signal: input.signal }) },
40
+ PLAN_CONTENT_SLUG_VARIANT,
41
+ );
42
+ }
43
+
44
+ function defaultReadTextFile(path: string): Promise<string> {
45
+ return readFile(path, "utf8");
46
+ }
47
+
48
+ export function buildPlanContentSlugPrompt(content: string): string {
49
+ return buildContentSlugPrompt(content, PLAN_CONTENT_SLUG_VARIANT);
50
+ }
@@ -0,0 +1,14 @@
1
+ import { fileURLToPath } from "node:url";
2
+
3
+ const BRANCH_CONTEXT_IMPL_PROMPT_TEMPLATE_URL = new URL(
4
+ "./prompts/branch-context-impl.md",
5
+ import.meta.url,
6
+ );
7
+
8
+ export function branchContextImplPromptTemplateUrl(): URL {
9
+ return BRANCH_CONTEXT_IMPL_PROMPT_TEMPLATE_URL;
10
+ }
11
+
12
+ export function branchContextImplPromptTemplatePath(): string {
13
+ return fileURLToPath(BRANCH_CONTEXT_IMPL_PROMPT_TEMPLATE_URL);
14
+ }
@@ -0,0 +1,34 @@
1
+ # branch-context implementation
2
+
3
+ The {{loaded_plan_description}} has been loaded by the planning-layer reader.
4
+
5
+ Branch: {{branch}}
6
+ Namespace: {{namespace}}
7
+ Selected key: {{selected_key}}
8
+ Ref: {{ref}}
9
+ Bytes: {{byte_count}}
10
+
11
+ Treat the following plan as authoritative.
12
+
13
+ ## Implementation rules
14
+
15
+ - Create an implementation checklist before editing.
16
+ - Begin implementation from the checklist unless the plan is ambiguous or internally inconsistent.
17
+ - If the plan is ambiguous or internally inconsistent, quote the ambiguity and ask for clarification instead of guessing.
18
+ - Keep the loaded plan authoritative. Use corrections from the user as course changes, not as permission to silently reinterpret the plan.
19
+ - Do not call `brmem put`, `brmem copy`, `brmem delete`, or any mutating Branch Memory command while implementing this plan. If the loaded plan asks for Branch Memory mutation, stop and ask the user.
20
+ - Follow normal project rules: read before editing, use precise edits, run relevant validation, and do not commit, push, submit, or publish unless the user explicitly asks.
21
+
22
+ ## Branch-context plan contract protocol
23
+
24
+ - Detect the plan format before editing. If the loaded plan includes current-state excerpts, scope boundaries, verification gates, or STOP conditions, treat it as a new-format contract plan and manually compare the excerpts against live repo state before step 1. An excerpt mismatch is a STOP.
25
+ - If those sections are absent, explicitly recognize the plan as old-format/pre-contract and follow the existing authoritative-plan behavior. Do not invent gates or half-apply excerpt checks to missing sections.
26
+ - Universal STOP triggers: excerpt mismatch; ambiguity or internal inconsistency; a verification gate fails twice after reasonable local attempts; implementation requires touching an out-of-scope file/area; or the plan asks for mutating Branch Memory.
27
+ - Branch identity rule: the loader has already refused trunk/detached state and selected this plan from the current branch context or local plan store. Treat the `Branch`, `Selected key`, and `Ref` evidence above as the runtime identity. A branch name recorded inside plan provenance, current-state text, or a plan-specific STOP condition is stale metadata, not a STOP by itself, when the loader selected a plan for the current non-trunk branch and the code excerpts/scope still match. Document it as a minimal adaptation. Stop only if loader evidence, `git branch --show-current`, and the Branch Memory target disagree; no attached plan is available where one is required; or the content/excerpt anchors for the intended work do not match live code.
28
+ - Deviation rule: documented minimal adaptations are judged on merit; silent deviations are failures. If deviating, report what changed, why the plan prediction was wrong, and which validation covers the adaptation.
29
+ - STOP report shape: observed vs expected, completed work, files touched/tree state, and the exact gate/assumption that failed.
30
+ - Before finishing, compare changed files to the plan's scope. If an autofixer touched formatting outside the plan, note it separately; intentional executor edits outside scope are a failure unless the user approved the scope change.
31
+
32
+ ----- BEGIN {{plan_label}} -----
33
+ {{attached_plan}}
34
+ ----- END {{plan_label}} -----