@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,98 @@
1
+ import { ClinkrGroup } from "@nseng-ai/clinkr";
2
+ import { defineCli } from "@nseng-ai/foundation/cli-runtime";
3
+ import { optionalEntry } from "@nseng-ai/foundation/primitives";
4
+
5
+ import {
6
+ attachRequestSchema,
7
+ createRealBranchContextCliContext,
8
+ createRequestSchema,
9
+ handleAttach,
10
+ handleCheck,
11
+ handleCreate,
12
+ handleDelete,
13
+ handleList,
14
+ handleLoad,
15
+ keyRequestSchema,
16
+ listRequestSchema,
17
+ loadRequestSchema,
18
+ type BranchContextCliContext,
19
+ type CliDeps,
20
+ } from "./operations.ts";
21
+
22
+ const entry = defineCli<BranchContextCliContext, CliDeps, undefined>({
23
+ metaUrl: import.meta.url,
24
+ runtime: "typescript",
25
+ description: "Branch context operations.",
26
+ prepareRun: ({ deps, cwd }) => {
27
+ const context =
28
+ deps.context === undefined
29
+ ? createRealBranchContextCliContext({
30
+ cwd,
31
+ ...optionalEntry("planStoreRoot", deps.planStoreRoot),
32
+ })
33
+ : {
34
+ context: deps.context,
35
+ cwd,
36
+ ...optionalEntry("planStoreRoot", deps.planStoreRoot),
37
+ };
38
+ return { type: "run", context, buildState: undefined };
39
+ },
40
+ configureCli: ({ root }) => {
41
+ const execGroup = new ClinkrGroup<BranchContextCliContext>({
42
+ name: "exec",
43
+ description: "Run hidden deterministic branch-context operations for agents.",
44
+ isHidden: true,
45
+ });
46
+ execGroup.command({
47
+ name: "from-plan",
48
+ description: "Create a branch context from a saved plan.",
49
+ schema: createRequestSchema,
50
+ handler: handleCreate,
51
+ });
52
+ execGroup.command({
53
+ name: "load",
54
+ description: "Load a branch-context entry and render the implementation prompt.",
55
+ schema: loadRequestSchema,
56
+ positionals: { key: { position: 0 } },
57
+ handler: handleLoad,
58
+ });
59
+ execGroup.command({
60
+ name: "attach",
61
+ description: "Attach a saved plan or file as branch context.",
62
+ schema: attachRequestSchema,
63
+ positionals: { key: { position: 0 } },
64
+ handler: handleAttach,
65
+ });
66
+ execGroup.command({
67
+ name: "list",
68
+ description: "List branch-context entries.",
69
+ schema: listRequestSchema,
70
+ handler: handleList,
71
+ });
72
+ execGroup.command({
73
+ name: "check",
74
+ description: "Check whether a branch-context entry exists.",
75
+ schema: keyRequestSchema,
76
+ positionals: { key: { position: 0 } },
77
+ handler: handleCheck,
78
+ });
79
+ execGroup.command({
80
+ name: "delete",
81
+ description: "Delete a branch-context entry.",
82
+ schema: keyRequestSchema,
83
+ positionals: { key: { position: 0 } },
84
+ handler: handleDelete,
85
+ });
86
+ root.group(execGroup);
87
+ },
88
+ });
89
+
90
+ export const VERSION = entry.version;
91
+
92
+ export function buildCli(): ClinkrGroup<BranchContextCliContext> {
93
+ return entry.buildCli(undefined);
94
+ }
95
+
96
+ export async function runCli(args: readonly string[], deps: CliDeps = {}): Promise<number> {
97
+ return await entry.run(args, deps);
98
+ }
@@ -0,0 +1,19 @@
1
+ import { buildPlanFileName, validatePlanSlug } from "@nseng-ai/plans";
2
+
3
+ export const BRANCH_CONTEXT_NAMESPACE = "branch-context";
4
+ export const UNSUPPORTED_ATTACHED_PLAN_KEY = "plan.md";
5
+
6
+ export function buildBranchContextPlanKey(slug: string): string {
7
+ const slugError = validatePlanSlug(slug);
8
+ if (slugError !== undefined) {
9
+ throw new Error(`Invalid branch context slug: ${slugError}`);
10
+ }
11
+ return buildPlanFileName(slug);
12
+ }
13
+
14
+ export function isSupportedBranchContextPlanKey(key: string): boolean {
15
+ if (key === UNSUPPORTED_ATTACHED_PLAN_KEY) return false;
16
+ if (!key.endsWith(".md")) return false;
17
+ const slug = key.slice(0, -".md".length);
18
+ return validatePlanSlug(slug) === undefined;
19
+ }
@@ -0,0 +1,45 @@
1
+ import { RealGitBrmemGateway, type BrmemGateway } from "@nseng-ai/brmem";
2
+ import { NodeCommandExecApi } from "@nseng-ai/foundation/exec";
3
+ import type { CommandExecApi, StdinCapableCommandExecApi } from "@nseng-ai/foundation/exec";
4
+ import { RealGitGateway } from "@nseng-ai/capability-kit/git";
5
+ import type { GitGateway } from "@nseng-ai/capability-kit/git";
6
+ import {
7
+ RealGraphiteBranchGateway,
8
+ type GraphiteBranchGateway,
9
+ } from "@nseng-ai/capability-kit/graphite/branch";
10
+
11
+ export interface BranchContextContext {
12
+ commands: CommandExecApi;
13
+ git: GitGateway;
14
+ brmem: BrmemGateway;
15
+ graphite: GraphiteBranchGateway;
16
+ }
17
+
18
+ export interface BranchContextContextOptions {
19
+ cwd?: string;
20
+ }
21
+
22
+ export type BranchContextContextFactory<Args extends unknown[]> = (
23
+ ...args: Args
24
+ ) => BranchContextContext;
25
+
26
+ export function createBranchContextContext(
27
+ commands: StdinCapableCommandExecApi,
28
+ options: BranchContextContextOptions = {},
29
+ ): BranchContextContext {
30
+ const cwd = options.cwd ?? process.cwd();
31
+ const git = new RealGitGateway(commands);
32
+ const brmem = new RealGitBrmemGateway({ cwd, commands, git });
33
+ return {
34
+ commands,
35
+ git,
36
+ brmem,
37
+ graphite: new RealGraphiteBranchGateway(commands),
38
+ };
39
+ }
40
+
41
+ export function createRealBranchContextContext(
42
+ options: BranchContextContextOptions = {},
43
+ ): BranchContextContext {
44
+ return createBranchContextContext(new NodeCommandExecApi(), options);
45
+ }
@@ -0,0 +1,230 @@
1
+ import type { CommandExecApi } from "@nseng-ai/foundation/exec";
2
+ import type { BrmemGateway } from "@nseng-ai/brmem";
3
+ import { selectAttachedPlanKey } from "./attached-plan.ts";
4
+ import { checkBranchContextEntryPresence, listBranchContextPlans } from "./branch-memory.ts";
5
+ import type { BranchContextContext } from "./context.ts";
6
+ import { BRANCH_CONTEXT_NAMESPACE } from "./constants.ts";
7
+ import { extractBranchContextEvidenceFromSessionEntry } from "./session-artifact.ts";
8
+
9
+ export type ExistingBranchContextReuseSource =
10
+ | "explicit-branch"
11
+ | "session-output"
12
+ | "current-branch";
13
+
14
+ export interface ExistingBranchContextReuse {
15
+ branch: string;
16
+ key: string;
17
+ source: ExistingBranchContextReuseSource;
18
+ }
19
+
20
+ export interface ExistingBranchContextCandidate {
21
+ branch: string;
22
+ source: ExistingBranchContextReuseSource;
23
+ requestedKey?: string;
24
+ }
25
+
26
+ export interface ResolveExistingBranchContextReuseParams {
27
+ explicitBranch?: string;
28
+ sessionEntries?: readonly unknown[];
29
+ }
30
+
31
+ export interface ResolveExistingBranchContextReuseOptions {
32
+ cwd: string;
33
+ context: BranchContextContext;
34
+ signal?: AbortSignal;
35
+ }
36
+
37
+ type ReuseVerificationFailure =
38
+ | { type: "candidate"; candidate: ExistingBranchContextCandidate; message: string }
39
+ | { type: "current-branch-resolution"; message: string };
40
+
41
+ type CandidateVerification =
42
+ | { type: "verified"; reuse: ExistingBranchContextReuse }
43
+ | { type: "failure"; message: string };
44
+
45
+ export async function resolveExistingBranchContextReuse(
46
+ _pi: CommandExecApi,
47
+ params: ResolveExistingBranchContextReuseParams,
48
+ options: ResolveExistingBranchContextReuseOptions,
49
+ ): Promise<ExistingBranchContextReuse> {
50
+ const brmem = options.context.brmem;
51
+ const failures: ReuseVerificationFailure[] = [];
52
+
53
+ const explicitBranch = params.explicitBranch;
54
+ if (explicitBranch !== undefined) {
55
+ const candidate: ExistingBranchContextCandidate = {
56
+ branch: explicitBranch,
57
+ source: "explicit-branch",
58
+ };
59
+ const result = await verifyCandidate(brmem, candidate);
60
+ if (result.type === "verified") {
61
+ return result.reuse;
62
+ }
63
+ throw buildNoReusableBranchError([{ type: "candidate", candidate, message: result.message }]);
64
+ }
65
+
66
+ const sessionCandidates = collectSessionCandidates(params.sessionEntries ?? []);
67
+ if (sessionCandidates.length > 1) {
68
+ throw new Error(
69
+ [
70
+ "Multiple existing branch-context candidates were found in this session.",
71
+ "Rerun with `--branch <target-branch>` to choose explicitly.",
72
+ "",
73
+ "Candidates:",
74
+ ...sessionCandidates.map(formatCandidateListItem),
75
+ ].join("\n"),
76
+ );
77
+ }
78
+
79
+ const sessionCandidate = sessionCandidates[0];
80
+ if (sessionCandidate !== undefined) {
81
+ const result = await verifyCandidate(brmem, sessionCandidate);
82
+ if (result.type === "verified") {
83
+ return result.reuse;
84
+ }
85
+ failures.push({ type: "candidate", candidate: sessionCandidate, message: result.message });
86
+ }
87
+
88
+ const branch = await options.context.git.currentBranch({
89
+ cwd: options.cwd,
90
+ signal: options.signal,
91
+ });
92
+ if (branch.type === "branch") {
93
+ const candidate: ExistingBranchContextCandidate = {
94
+ branch: branch.branch,
95
+ source: "current-branch",
96
+ };
97
+ const result = await verifyCandidate(brmem, candidate);
98
+ if (result.type === "verified") {
99
+ return result.reuse;
100
+ }
101
+ failures.push({ type: "candidate", candidate, message: result.message });
102
+ } else if (branch.type === "detached") {
103
+ failures.push({ type: "current-branch-resolution", message: "Current HEAD is detached." });
104
+ } else {
105
+ failures.push({ type: "current-branch-resolution", message: branch.error.message });
106
+ }
107
+
108
+ throw buildNoReusableBranchError(failures);
109
+ }
110
+
111
+ async function verifyCandidate(
112
+ brmem: BrmemGateway,
113
+ candidate: ExistingBranchContextCandidate,
114
+ ): Promise<CandidateVerification> {
115
+ if (candidate.requestedKey !== undefined) {
116
+ let key: string;
117
+ try {
118
+ key = selectAttachedPlanKey({
119
+ branch: candidate.branch,
120
+ requestedKey: candidate.requestedKey,
121
+ entries: [
122
+ {
123
+ branch: candidate.branch,
124
+ key: candidate.requestedKey,
125
+ namespace: BRANCH_CONTEXT_NAMESPACE,
126
+ refName: "",
127
+ },
128
+ ],
129
+ });
130
+ } catch (error) {
131
+ return { type: "failure", message: error instanceof Error ? error.message : String(error) };
132
+ }
133
+ const presence = await checkBranchContextEntryPresence(brmem, {
134
+ branch: candidate.branch,
135
+ key,
136
+ });
137
+ if (presence.type === "present") {
138
+ return {
139
+ type: "verified",
140
+ reuse: { branch: candidate.branch, key, source: candidate.source },
141
+ };
142
+ }
143
+ if (presence.type === "absent") {
144
+ return { type: "failure", message: `Branch-context key \`${key}\` is absent.` };
145
+ }
146
+ return { type: "failure", message: presence.error.message };
147
+ }
148
+
149
+ const list = await listBranchContextPlans(brmem, { branch: candidate.branch });
150
+ if (!list.ok) {
151
+ return { type: "failure", message: list.error.message };
152
+ }
153
+ try {
154
+ const key = selectAttachedPlanKey({ branch: candidate.branch, entries: list.value });
155
+ return { type: "verified", reuse: { branch: candidate.branch, key, source: candidate.source } };
156
+ } catch (error) {
157
+ return { type: "failure", message: error instanceof Error ? error.message : String(error) };
158
+ }
159
+ }
160
+
161
+ function collectSessionCandidates(entries: readonly unknown[]): ExistingBranchContextCandidate[] {
162
+ const candidates: ExistingBranchContextCandidate[] = [];
163
+ for (const entry of entries) {
164
+ const evidence = extractBranchContextEvidenceFromSessionEntry(entry);
165
+ if (evidence === undefined || evidence.namespace !== BRANCH_CONTEXT_NAMESPACE) {
166
+ continue;
167
+ }
168
+ const candidate = {
169
+ branch: evidence.branch,
170
+ requestedKey: evidence.key,
171
+ source: "session-output",
172
+ } as const;
173
+ if (
174
+ !candidates.some(
175
+ (existing) =>
176
+ existing.branch === candidate.branch && existing.requestedKey === candidate.requestedKey,
177
+ )
178
+ ) {
179
+ candidates.push(candidate);
180
+ }
181
+ }
182
+ return candidates;
183
+ }
184
+
185
+ function buildNoReusableBranchError(failures: readonly ReuseVerificationFailure[]): Error {
186
+ return new Error(
187
+ [
188
+ "No existing branch context with an attached plan could be reused.",
189
+ "",
190
+ "Verification failures:",
191
+ ...failures.map(formatVerificationFailureListItem),
192
+ ].join("\n"),
193
+ );
194
+ }
195
+
196
+ export function formatExistingBranchContextReuse(reuse: ExistingBranchContextReuse): string {
197
+ return [
198
+ "Existing branch context with attached plan:",
199
+ `Branch: ${reuse.branch}`,
200
+ `Branch Memory namespace: ${BRANCH_CONTEXT_NAMESPACE}`,
201
+ `Branch Memory key: ${reuse.key}`,
202
+ `Reuse source: ${formatExistingReuseSource(reuse.source)}`,
203
+ ].join("\n");
204
+ }
205
+
206
+ function formatExistingReuseSource(source: ExistingBranchContextReuseSource): string {
207
+ switch (source) {
208
+ case "explicit-branch":
209
+ return "explicit --branch";
210
+ case "session-output":
211
+ return "current session branch-context output";
212
+ case "current-branch":
213
+ return "current branch";
214
+ }
215
+ }
216
+
217
+ function formatCandidateListItem(candidate: ExistingBranchContextCandidate): string {
218
+ const keyPart =
219
+ candidate.requestedKey === undefined
220
+ ? "attached key: auto-select"
221
+ : `attached key: ${candidate.requestedKey}`;
222
+ return `- ${candidate.branch} (${keyPart}; source: ${formatExistingReuseSource(candidate.source)})`;
223
+ }
224
+
225
+ function formatVerificationFailureListItem(failure: ReuseVerificationFailure): string {
226
+ if (failure.type === "candidate") {
227
+ return `${formatCandidateListItem(failure.candidate)}\n ${failure.message}`;
228
+ }
229
+ return `- could not resolve current branch:\n ${failure.message}`;
230
+ }
@@ -0,0 +1,47 @@
1
+ export {
2
+ createBranchContextContext,
3
+ createRealBranchContextContext,
4
+ type BranchContextContext,
5
+ type BranchContextContextFactory,
6
+ type BranchContextContextOptions,
7
+ } from "./context.ts";
8
+ export {
9
+ BRANCH_CONTEXT_NAMESPACE,
10
+ buildBranchContextPlanKey,
11
+ buildBranchContextCreateOperation,
12
+ createBranchContextFromFile,
13
+ deriveTargetBranch,
14
+ formatBranchContextCreateFailure,
15
+ formatBranchContextCreatePreview,
16
+ formatBranchContextEvidence,
17
+ describeBranchContextGraphiteCreationSteps,
18
+ resolveBranchContextCreatePreviewContext,
19
+ type BranchContextCreateOperation,
20
+ type BranchContextEvidence,
21
+ type BranchCreationMethod,
22
+ } from "./branch-context-creation.ts";
23
+ export {
24
+ buildImplBranchContextPrompt,
25
+ formatLoadedAttachedPlanEvidence,
26
+ loadBranchContextPlan,
27
+ type LoadedAttachedPlan,
28
+ } from "./attached-plan.ts";
29
+ export {
30
+ BRANCH_CONTEXT_OUTPUT_MESSAGE_TYPE,
31
+ buildBranchContextOutputMessage,
32
+ extractBranchContextEvidence,
33
+ extractBranchContextEvidenceFromSessionEntry,
34
+ findLatestBranchContextEvidence,
35
+ type BranchContextOutputDetails,
36
+ type BranchContextOutputMessage,
37
+ } from "./session-artifact.ts";
38
+ export {
39
+ formatExistingBranchContextReuse,
40
+ resolveExistingBranchContextReuse,
41
+ type ExistingBranchContextReuse,
42
+ } from "./existing-branch-reuse.ts";
43
+ export {
44
+ buildPlanContentSlugPrompt,
45
+ derivePlanContentSlug,
46
+ type PlanContentSlugEvidence,
47
+ } from "./plan-content-slug.ts";