@nseng-ai/plans 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,29 @@
1
+ {
2
+ "name": "@nseng-ai/plans",
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
+ "bin": {
16
+ "enriched-plan": "src/cli.ts"
17
+ },
18
+ "exports": {
19
+ ".": "./src/index.ts",
20
+ "./api": "./src/api.ts",
21
+ "./testing": "./src/testing.ts"
22
+ },
23
+ "dependencies": {
24
+ "@nseng-ai/capability-kit": "0.1.1",
25
+ "@nseng-ai/clinkr": "0.1.1",
26
+ "@nseng-ai/foundation": "0.1.1",
27
+ "zod": "^4.4.3"
28
+ }
29
+ }
package/src/api.ts ADDED
@@ -0,0 +1,25 @@
1
+ export {
2
+ NoSavedPlanAvailableError,
3
+ buildRepoPlanStoreKey,
4
+ encodeBranchForPlanPath,
5
+ formatSavedPlanFileEvidence,
6
+ normalizeRepoOriginUrl,
7
+ resolvePlanStoreDirectory,
8
+ writeSavedPlanFile,
9
+ type PlanStoreDirectoryEvidence,
10
+ type RepoIdentitySource,
11
+ type SavedPlanFileEvidence,
12
+ } from "./saved-plan-file.ts";
13
+ export {
14
+ buildSavedPlanContentSlugPrompt,
15
+ deriveSavedPlanContentSlug,
16
+ type SavedPlanContentSlugEvidence,
17
+ } from "./saved-plan-content-slug.ts";
18
+ export {
19
+ WRITE_SAVED_PLAN_FILE_TOOL_NAME,
20
+ findLatestSessionSavedPlanFile,
21
+ resolveSelectedSavedPlanFile,
22
+ type SelectedSavedPlanFile,
23
+ type ValidateSessionSavedPlanCandidateOptions,
24
+ type ValidatedSessionSavedPlan,
25
+ } from "./saved-plan-selection.ts";
package/src/cli.ts ADDED
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from "node:path";
4
+
5
+ import { ClinkrGroup, failure, negative, ok, usageError, type ClinkrExit } from "@nseng-ai/clinkr";
6
+ import {
7
+ defineCli,
8
+ readStdin,
9
+ runOperationCommand,
10
+ type CliEntrypointDeps,
11
+ } from "@nseng-ai/foundation/cli-runtime";
12
+ import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
13
+ import { NodeCommandExecApi } from "@nseng-ai/foundation/exec";
14
+ import type { CommandExecApi } from "@nseng-ai/foundation/exec";
15
+ import { RealGitGateway } from "@nseng-ai/capability-kit/git";
16
+ import type { GitGateway } from "@nseng-ai/capability-kit/git";
17
+ import { z } from "zod";
18
+
19
+ import {
20
+ normalizePlanFilePath,
21
+ resolvePlanSourceFile,
22
+ validatePlanSlug,
23
+ } from "./plan-persistence.ts";
24
+ import { createRealPlanStoreGateway, type PlanStoreGateway } from "./plan-store-gateway.ts";
25
+ import {
26
+ buildPlanStoreOptions,
27
+ findLatestSavedPlanFile,
28
+ formatSavedPlanFileEvidence,
29
+ NoSavedPlanAvailableError,
30
+ listSavedPlans,
31
+ writeSavedPlanFile,
32
+ type LatestSavedPlanFileEvidence,
33
+ type PlanStoreOptions,
34
+ type SavedPlanFileEvidence,
35
+ type SavedPlanListItem,
36
+ } from "./saved-plan-file.ts";
37
+
38
+ type PlansOperation = "list" | "save" | "resolve";
39
+
40
+ const listRequestSchema = z.object({
41
+ planStoreRoot: z
42
+ .string()
43
+ .optional()
44
+ .describe("Plan store root directory (relative paths resolve against cwd)."),
45
+ });
46
+
47
+ const saveRequestSchema = z.object({
48
+ slug: z.string().describe("Saved plan slug."),
49
+ summary: z.string().optional().describe("Optional saved-plan summary."),
50
+ stdin: z.boolean().optional().describe("Read plan content from stdin."),
51
+ contentFile: z.string().optional().describe("Read plan content from this file path."),
52
+ });
53
+
54
+ const resolveRequestSchema = z.object({
55
+ path: z.string().optional().describe("Absolute, @-prefixed, or home-relative plan file path."),
56
+ });
57
+
58
+ type ListRequest = z.infer<typeof listRequestSchema>;
59
+ type SaveRequest = z.infer<typeof saveRequestSchema>;
60
+ type ResolveRequest = z.infer<typeof resolveRequestSchema>;
61
+
62
+ interface ExplicitResolvePlanEvidence {
63
+ source: "explicit";
64
+ filePath: string;
65
+ }
66
+
67
+ type LatestResolvePlanEvidence = LatestSavedPlanFileEvidence & { source: "latest" };
68
+
69
+ type ResolvePlanEvidence = ExplicitResolvePlanEvidence | LatestResolvePlanEvidence;
70
+
71
+ type SavedPlanListData = ReturnType<typeof savedPlanListJson>;
72
+ type SavedPlanFileData = ReturnType<typeof savedPlanFileJson>;
73
+ interface NoSavedPlanData {
74
+ code: NoSavedPlanAvailableError["reason"];
75
+ directoryPath: string;
76
+ }
77
+
78
+ type ResolvePlanData = ReturnType<typeof resolvePlanJson> | NoSavedPlanData;
79
+
80
+ export interface CliDeps extends Pick<CliEntrypointDeps, "cwd" | "stdout" | "stderr"> {
81
+ commands?: CommandExecApi;
82
+ git?: GitGateway;
83
+ stdin?: () => Promise<string>;
84
+ planStoreRoot?: string;
85
+ planStoreGateway?: PlanStoreGateway;
86
+ }
87
+
88
+ export interface PlansCliContext {
89
+ commands: CommandExecApi;
90
+ git: GitGateway;
91
+ cwd: string;
92
+ stdin: () => Promise<string>;
93
+ planStoreRoot?: string;
94
+ planStoreGateway: PlanStoreGateway;
95
+ }
96
+
97
+ const entry = defineCli<PlansCliContext, CliDeps, undefined>({
98
+ metaUrl: import.meta.url,
99
+ runtime: "typescript",
100
+ description: "Enriched-plan operations. An enriched plan is any plan saved into ns.",
101
+ prepareRun: ({ deps, cwd }) => {
102
+ const commands = deps.commands ?? new NodeCommandExecApi();
103
+ const context: PlansCliContext = {
104
+ commands,
105
+ git: deps.git ?? new RealGitGateway(commands),
106
+ cwd,
107
+ stdin: deps.stdin ?? readStdin,
108
+ planStoreGateway: deps.planStoreGateway ?? createRealPlanStoreGateway(),
109
+ ...(deps.planStoreRoot === undefined ? {} : { planStoreRoot: deps.planStoreRoot }),
110
+ };
111
+ return { type: "run", context, buildState: undefined };
112
+ },
113
+ configureCli: ({ root }) => {
114
+ root.command({
115
+ name: "list",
116
+ description: "List saved plans for the current repository across all branch keys.",
117
+ schema: listRequestSchema,
118
+ handler: handleList,
119
+ renderHuman: formatSavedPlanListData,
120
+ });
121
+
122
+ const execGroup = new ClinkrGroup<PlansCliContext>({
123
+ name: "exec",
124
+ description: "Run hidden deterministic saved-plan operations for agents.",
125
+ isHidden: true,
126
+ });
127
+ execGroup.command({
128
+ name: "save",
129
+ description: "Save a source-branch plan file in the local store.",
130
+ schema: saveRequestSchema,
131
+ handler: handleSave,
132
+ });
133
+ execGroup.command({
134
+ name: "resolve",
135
+ description: "Resolve an explicit or latest source-branch plan file.",
136
+ schema: resolveRequestSchema,
137
+ positionals: { path: { position: 0 } },
138
+ handler: handleResolve,
139
+ renderHuman: renderResolvePlanData,
140
+ });
141
+ root.group(execGroup);
142
+ },
143
+ });
144
+
145
+ export const VERSION = entry.version;
146
+
147
+ export function buildCli(): ClinkrGroup<PlansCliContext> {
148
+ return entry.buildCli(undefined);
149
+ }
150
+
151
+ export async function runCli(args: readonly string[], deps: CliDeps = {}): Promise<number> {
152
+ return await entry.run(args, deps);
153
+ }
154
+
155
+ async function handleList(
156
+ ctx: PlansCliContext,
157
+ request: ListRequest,
158
+ ): Promise<ClinkrExit<SavedPlanListData>> {
159
+ return await runOperationCommand({
160
+ operation: "list",
161
+ action: async () => {
162
+ const cliPlanStoreRoot =
163
+ request.planStoreRoot === undefined
164
+ ? undefined
165
+ : normalizeRootPath(request.planStoreRoot, ctx.cwd);
166
+ const planStoreRoot = cliPlanStoreRoot ?? ctx.planStoreRoot;
167
+ const plans = await listSavedPlans(ctx.commands, planStoreOptions(ctx, planStoreRoot));
168
+ return ok(savedPlanListJson(plans));
169
+ },
170
+ failureFromError: plansFailureFromError,
171
+ });
172
+ }
173
+
174
+ async function handleSave(
175
+ ctx: PlansCliContext,
176
+ request: SaveRequest,
177
+ ): Promise<ClinkrExit<SavedPlanFileData>> {
178
+ return await runOperationCommand({
179
+ operation: "save",
180
+ action: async () => {
181
+ const slugError = validatePlanSlug(request.slug);
182
+ if (slugError !== undefined) {
183
+ return usageError(`Invalid saved plan slug: ${slugError}`, {
184
+ code: "invalid-slug",
185
+ argument: "slug",
186
+ reason: slugError,
187
+ });
188
+ }
189
+ if (Boolean(request.stdin) === (request.contentFile !== undefined)) {
190
+ return usageError("Pass exactly one of --stdin or --content-file <path>.", {
191
+ code: "invalid-save-input",
192
+ requiredExactlyOneOf: ["--stdin", "--content-file"],
193
+ });
194
+ }
195
+
196
+ const contentFile = request.contentFile;
197
+ let content: string;
198
+ if (request.stdin === true) {
199
+ content = await ctx.stdin();
200
+ } else {
201
+ if (contentFile === undefined) {
202
+ throw new Error("Save input validation invariant failed.");
203
+ }
204
+ content = await ctx.planStoreGateway.readTextFile(normalizePlanFilePath(contentFile));
205
+ }
206
+ const evidence = await writeSavedPlanFile(
207
+ ctx.commands,
208
+ {
209
+ slug: request.slug,
210
+ content,
211
+ ...(request.summary === undefined ? {} : { summary: request.summary }),
212
+ },
213
+ planStoreOptions(ctx),
214
+ );
215
+ return ok(savedPlanFileJson(evidence), { human: formatSavedPlanFileEvidence(evidence) });
216
+ },
217
+ failureFromError: plansFailureFromError,
218
+ });
219
+ }
220
+
221
+ async function handleResolve(
222
+ ctx: PlansCliContext,
223
+ request: ResolveRequest,
224
+ ): Promise<ClinkrExit<ResolvePlanData>> {
225
+ return await runOperationCommand<PlansOperation, ResolvePlanData>({
226
+ operation: "resolve",
227
+ action: async () => {
228
+ try {
229
+ return ok(resolvePlanJson(await resolvePlanEvidence(request, ctx)));
230
+ } catch (error) {
231
+ if (error instanceof NoSavedPlanAvailableError) {
232
+ return negative(error.message, {
233
+ data: { code: error.reason, directoryPath: error.directoryPath },
234
+ });
235
+ }
236
+ throw error;
237
+ }
238
+ },
239
+ failureFromError: plansFailureFromError,
240
+ });
241
+ }
242
+
243
+ function plansFailureFromError(operation: PlansOperation, error: unknown): ClinkrExit<never> {
244
+ return failure(plansErrorType(operation), formatErrorMessage(error), {
245
+ code: "unexpected-error",
246
+ });
247
+ }
248
+
249
+ function planStoreOptions(
250
+ ctx: PlansCliContext,
251
+ planStoreRoot: string | undefined = ctx.planStoreRoot,
252
+ ): PlanStoreOptions {
253
+ return buildPlanStoreOptions({
254
+ cwd: ctx.cwd,
255
+ git: ctx.git,
256
+ planStoreGateway: ctx.planStoreGateway,
257
+ planStoreRoot,
258
+ });
259
+ }
260
+
261
+ function plansErrorType(operation: PlansOperation): string {
262
+ switch (operation) {
263
+ case "list":
264
+ return "saved-plan-list-failed";
265
+ case "save":
266
+ return "saved-plan-write-failed";
267
+ case "resolve":
268
+ return "saved-plan-resolution-failed";
269
+ }
270
+ }
271
+
272
+ function normalizeRootPath(rawPath: string, cwd: string): string {
273
+ const normalized = normalizePlanFilePath(rawPath);
274
+ return resolve(cwd, normalized);
275
+ }
276
+
277
+ async function resolvePlanEvidence(
278
+ args: ResolveRequest,
279
+ ctx: PlansCliContext,
280
+ ): Promise<ResolvePlanEvidence> {
281
+ if (args.path !== undefined) {
282
+ const filePath = await resolvePlanSourceFile(ctx.commands, {
283
+ cwd: ctx.cwd,
284
+ rawFilePath: args.path,
285
+ git: ctx.git,
286
+ planStoreGateway: ctx.planStoreGateway,
287
+ });
288
+ return { source: "explicit", filePath };
289
+ }
290
+ const latest = await findLatestSavedPlanFile(ctx.commands, planStoreOptions(ctx));
291
+ return { source: "latest", ...latest };
292
+ }
293
+
294
+ function formatSavedPlanListData(data: SavedPlanListData): string {
295
+ if (data.plans.length === 0) {
296
+ return "No saved plans found for the current repository.";
297
+ }
298
+
299
+ const lines = ["Saved plans:"];
300
+ for (const plan of data.plans) {
301
+ lines.push(
302
+ [
303
+ `- ${plan.slug}`,
304
+ ` Branch key: ${plan.branchKey}`,
305
+ ` Modified: ${new Date(plan.modifiedTimeMs).toISOString()}`,
306
+ ` Path: ${plan.path}`,
307
+ ].join("\n"),
308
+ );
309
+ }
310
+ return lines.join("\n");
311
+ }
312
+
313
+ function renderResolvePlanData(data: ResolvePlanData): string {
314
+ if (!("source" in data)) {
315
+ return `No saved plan available: ${data.code}\nDirectory: ${data.directoryPath}`;
316
+ }
317
+ if (data.source === "explicit") {
318
+ return [`Resolved explicit plan file.`, `Path: ${data.filePath}`].join("\n");
319
+ }
320
+ return [
321
+ "Resolved latest saved plan file in local plan store.",
322
+ `Path: ${data.filePath}`,
323
+ `Repo key: ${data.repoKey}`,
324
+ `Repo root: ${data.repoRoot}`,
325
+ `Repo identity source: ${data.repoIdentitySource}`,
326
+ `Source branch: ${data.sourceBranch}`,
327
+ `Branch path segment: ${data.branchKey}`,
328
+ `Slug: ${data.slug}`,
329
+ `Modified time ms: ${data.modifiedTimeMs}`,
330
+ ].join("\n");
331
+ }
332
+
333
+ function savedPlanListJson(plans: readonly SavedPlanListItem[]): {
334
+ plans: ReturnType<typeof savedPlanListItemJson>[];
335
+ } {
336
+ return { plans: plans.map(savedPlanListItemJson) };
337
+ }
338
+
339
+ function savedPlanListItemJson(plan: SavedPlanListItem): {
340
+ slug: string;
341
+ branchKey: string;
342
+ modifiedTimeMs: number;
343
+ path: string;
344
+ fileName: string;
345
+ repo: {
346
+ root: string;
347
+ key: string;
348
+ identitySource: string;
349
+ planStorePath: string;
350
+ };
351
+ } {
352
+ return {
353
+ slug: plan.slug,
354
+ branchKey: plan.branchKey,
355
+ modifiedTimeMs: plan.modifiedTimeMs,
356
+ path: plan.filePath,
357
+ fileName: plan.fileName,
358
+ repo: {
359
+ root: plan.repoRoot,
360
+ key: plan.repoKey,
361
+ identitySource: plan.repoIdentitySource,
362
+ planStorePath: plan.repoDirectoryPath,
363
+ },
364
+ };
365
+ }
366
+
367
+ function savedPlanFileJson(evidence: SavedPlanFileEvidence): {
368
+ slug: string;
369
+ filePath: string;
370
+ repoRoot: string;
371
+ repoKey: string;
372
+ repoIdentitySource: SavedPlanFileEvidence["repoIdentitySource"];
373
+ sourceBranch: string;
374
+ branchKey: string;
375
+ summary?: string;
376
+ } {
377
+ return {
378
+ slug: evidence.slug,
379
+ filePath: evidence.filePath,
380
+ repoRoot: evidence.repoRoot,
381
+ repoKey: evidence.repoKey,
382
+ repoIdentitySource: evidence.repoIdentitySource,
383
+ sourceBranch: evidence.sourceBranch,
384
+ branchKey: evidence.branchKey,
385
+ ...(evidence.summary === undefined ? {} : { summary: evidence.summary }),
386
+ };
387
+ }
388
+
389
+ function resolvePlanJson(evidence: ResolvePlanEvidence):
390
+ | {
391
+ source: "explicit";
392
+ filePath: string;
393
+ }
394
+ | {
395
+ source: "latest";
396
+ filePath: string;
397
+ slug: string;
398
+ fileName: string;
399
+ modifiedTimeMs: number;
400
+ repoRoot: string;
401
+ repoKey: string;
402
+ repoIdentitySource: LatestSavedPlanFileEvidence["repoIdentitySource"];
403
+ sourceBranch: string;
404
+ branchKey: string;
405
+ directoryPath: string;
406
+ } {
407
+ switch (evidence.source) {
408
+ case "explicit":
409
+ return {
410
+ source: evidence.source,
411
+ filePath: evidence.filePath,
412
+ };
413
+ case "latest":
414
+ return {
415
+ source: evidence.source,
416
+ filePath: evidence.filePath,
417
+ slug: evidence.slug,
418
+ fileName: evidence.fileName,
419
+ modifiedTimeMs: evidence.modifiedTimeMs,
420
+ repoRoot: evidence.repoRoot,
421
+ repoKey: evidence.repoKey,
422
+ repoIdentitySource: evidence.repoIdentitySource,
423
+ sourceBranch: evidence.sourceBranch,
424
+ branchKey: evidence.branchKey,
425
+ directoryPath: evidence.directoryPath,
426
+ };
427
+ }
428
+ }
429
+
430
+ await entry.runIfMain({ isImportMetaMain: import.meta.main });
@@ -0,0 +1,127 @@
1
+ import { type CommandExecApi, formatOutputSection } from "@nseng-ai/foundation/exec";
2
+ import { deriveSlugWithModel, type SlugModelEvidence } from "@nseng-ai/capability-kit/model-slug";
3
+ import { MAX_PLAN_SLUG_WORDS, MIN_PLAN_SLUG_WORDS, validatePlanSlug } from "./plan-persistence.ts";
4
+
5
+ const MAX_ERROR_CHARS = 4_000;
6
+
7
+ export const MAX_PLAN_CONTENT_CHARS = 32_000;
8
+
9
+ export interface ContentSlugDerivationVariant {
10
+ slugKind: string;
11
+ promptIntroLines: readonly string[];
12
+ invalidSlugMessage: string;
13
+ failureHeader: string;
14
+ noFallbackLine: string;
15
+ }
16
+
17
+ export interface DeriveContentSlugInput {
18
+ content: string;
19
+ cwd: string;
20
+ signal?: AbortSignal;
21
+ }
22
+
23
+ export type ContentSlugEvidence = SlugModelEvidence;
24
+
25
+ export async function deriveContentSlug(
26
+ pi: CommandExecApi,
27
+ input: DeriveContentSlugInput,
28
+ variant: ContentSlugDerivationVariant,
29
+ ): Promise<ContentSlugEvidence> {
30
+ const prompt = buildContentSlugPrompt(input.content, variant);
31
+ const result = await deriveSlugWithModel({
32
+ cwd: input.cwd,
33
+ prompt,
34
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
35
+ slugKind: variant.slugKind,
36
+ normalizeOutput: normalizePlanContentSlugOutput,
37
+ exec: (command, args, options) => pi.exec(command, args, options),
38
+ });
39
+ if (!result.ok) {
40
+ throw slugDerivationFailed(variant, result.failure.lines);
41
+ }
42
+
43
+ const { slug, rawOutput } = result.evidence;
44
+ const slugError = validatePlanSlug(slug);
45
+ if (slugError !== undefined) {
46
+ throw slugDerivationFailed(variant, [
47
+ variant.invalidSlugMessage,
48
+ `Normalized slug: ${slug}`,
49
+ `Reason: ${slugError}`,
50
+ formatOutputSection("stdout", rawOutput, { maxChars: MAX_ERROR_CHARS, maxLines: 80 }),
51
+ ]);
52
+ }
53
+
54
+ return result.evidence;
55
+ }
56
+
57
+ export function buildContentSlugPrompt(
58
+ content: string,
59
+ variant: ContentSlugDerivationVariant,
60
+ ): string {
61
+ return [
62
+ ...variant.promptIntroLines,
63
+ "Return exactly one slug and no prose.",
64
+ "Rules:",
65
+ "- Use lowercase ASCII kebab-case words separated by single hyphens.",
66
+ `- Use ${MIN_PLAN_SLUG_WORDS}–${MAX_PLAN_SLUG_WORDS} words.`,
67
+ "- Make the slug specific to the implementation described by the plan.",
68
+ "- Prefer concrete deliverables and nouns from the plan body.",
69
+ "- Do not use dates, random IDs, generic-only slugs, or the saved-plan filename.",
70
+ "",
71
+ "## Plan content",
72
+ truncatePlanContentForSlug(displayPlanContentForSlug(content)),
73
+ ].join("\n");
74
+ }
75
+
76
+ function displayPlanContentForSlug(content: string): string {
77
+ const trimmed = content.trim();
78
+ return trimmed.length > 0 ? trimmed : "(empty plan content)";
79
+ }
80
+
81
+ export function normalizePlanContentSlugOutput(value: string): string | undefined {
82
+ const firstLine = firstNonEmptyModelOutputLine(value);
83
+ if (firstLine === undefined) {
84
+ return undefined;
85
+ }
86
+
87
+ const slug = firstLine
88
+ .toLowerCase()
89
+ .normalize("NFKD")
90
+ .replace(/[\u0300-\u036f]/g, "")
91
+ .replace(/[^a-z0-9]+/g, "-")
92
+ .replace(/-+/g, "-")
93
+ .replace(/^-|-$/g, "");
94
+ const withoutPlanSuffix = slug.replace(/(?:-plan)+$/g, "").replace(/^-|-$/g, "");
95
+ if (withoutPlanSuffix.length === 0) {
96
+ return undefined;
97
+ }
98
+
99
+ const repaired = withoutPlanSuffix
100
+ .split("-")
101
+ .filter(Boolean)
102
+ .slice(0, MAX_PLAN_SLUG_WORDS)
103
+ .join("-");
104
+ return repaired.length > 0 ? repaired : undefined;
105
+ }
106
+
107
+ export function truncatePlanContentForSlug(content: string): string {
108
+ if (content.length <= MAX_PLAN_CONTENT_CHARS) {
109
+ return content;
110
+ }
111
+ return `${content.slice(0, MAX_PLAN_CONTENT_CHARS)}\n\n[Plan content truncated for slug generation]`;
112
+ }
113
+
114
+ function firstNonEmptyModelOutputLine(value: string): string | undefined {
115
+ return value
116
+ .replace(/```[\s\S]*?```/g, (match) => match.replace(/```[a-zA-Z]*\n?|```/g, ""))
117
+ .split("\n")
118
+ .map((line) => line.trim())
119
+ .find((line) => line.length > 0);
120
+ }
121
+
122
+ function slugDerivationFailed(
123
+ variant: ContentSlugDerivationVariant,
124
+ lines: readonly string[],
125
+ ): Error {
126
+ return new Error([variant.failureHeader, ...lines, variant.noFallbackLine].join("\n"));
127
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ export { buildCli, runCli, type CliDeps, type PlansCliContext } from "./cli.ts";
2
+ export {
3
+ buildContentSlugPrompt,
4
+ deriveContentSlug,
5
+ MAX_PLAN_CONTENT_CHARS,
6
+ normalizePlanContentSlugOutput,
7
+ truncatePlanContentForSlug,
8
+ type ContentSlugDerivationVariant,
9
+ type ContentSlugEvidence,
10
+ type DeriveContentSlugInput,
11
+ } from "./content-slug-derivation.ts";
12
+ export {
13
+ createRealPlanStoreGateway,
14
+ RealPlanStoreGateway,
15
+ type PlanStoreDirectoryEntry,
16
+ type PlanStoreDirectoryRead,
17
+ type PlanStoreGateway,
18
+ type PlanStorePathStat,
19
+ type PlanStorePathType,
20
+ } from "./plan-store-gateway.ts";
21
+ export {
22
+ isPathInside,
23
+ normalizePlanFilePath,
24
+ normalizeSummary,
25
+ resolveGitRepoRoot,
26
+ resolvePlanSourceFile,
27
+ validatePlanSlug,
28
+ } from "./plan-persistence.ts";
29
+ export {
30
+ NoSavedPlanAvailableError,
31
+ buildPlanStoreOptions,
32
+ buildPlanFileName,
33
+ buildPlanStoreBranchDirectoryPath,
34
+ buildRepoPlanStoreKey,
35
+ defaultPlanStoreRoot,
36
+ encodeBranchForPlanPath,
37
+ findLatestSavedPlanFile,
38
+ formatSavedPlanFileEvidence,
39
+ listSavedPlans,
40
+ normalizeRepoOriginUrl,
41
+ resolvePlanStoreDirectory,
42
+ resolvePlanStoreRepoDirectory,
43
+ sanitizePlanPathSegment,
44
+ writeSavedPlanFile,
45
+ type LatestSavedPlanFileEvidence,
46
+ type NoSavedPlanAvailableReason,
47
+ type PlanStoreDirectoryEvidence,
48
+ type PlanStoreOptions,
49
+ type PlanStoreRepoEvidence,
50
+ type RepoIdentitySource,
51
+ type SavedPlanFileEvidence,
52
+ type SavedPlanFileParams,
53
+ type SavedPlanListItem,
54
+ } from "./saved-plan-file.ts";
55
+ export {
56
+ buildSavedPlanContentSlugPrompt,
57
+ deriveSavedPlanContentSlug,
58
+ type SavedPlanContentSlugEvidence,
59
+ } from "./saved-plan-content-slug.ts";
60
+ export {
61
+ WRITE_SAVED_PLAN_FILE_TOOL_NAME,
62
+ extractSavedPlanFileEvidenceFromSessionEntry,
63
+ findLatestSessionSavedPlanFile,
64
+ resolveSelectedSavedPlanFile,
65
+ validateSessionSavedPlanCandidate,
66
+ type LatestSessionSavedPlanResult,
67
+ type ResolveSelectedSavedPlanFileOptions,
68
+ type SelectedSavedPlanFile,
69
+ type SessionSavedPlanValidation,
70
+ type ValidateSessionSavedPlanCandidateOptions,
71
+ type ValidatedSessionSavedPlan,
72
+ } from "./saved-plan-selection.ts";