@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,551 @@
1
+ import { registerCommandWithImmediateAck } from "@nseng-ai/pi/commands/ack";
2
+ import type { Stats } from "node:fs";
3
+ import { lstat, readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+
6
+ import { Text } from "@earendil-works/pi-tui";
7
+ import { piExecApiToCommandExecApi } from "@nseng-ai/foundation/command";
8
+ import { RealGitGateway } from "@nseng-ai/capability-kit/git";
9
+ import type { GitGateway } from "@nseng-ai/capability-kit/git";
10
+ import {
11
+ formatErrorMessage,
12
+ optionalEntries,
13
+ optionalEntry,
14
+ } from "@nseng-ai/foundation/primitives";
15
+ import type { ScheduledTimer } from "@nseng-ai/foundation/timers";
16
+ import { systemTimerScheduler } from "@nseng-ai/foundation/time";
17
+ import { WRITE_GRILLED_PLAN_COMMAND_NAME, WRITE_PLAN_COMMAND_NAME } from "./surfaces.ts";
18
+ import { sendCommandProgressOrNotify } from "@nseng-ai/pi/commands/ack";
19
+ import {
20
+ WRITE_SAVED_PLAN_FILE_TOOL_NAME,
21
+ deriveSavedPlanContentSlug,
22
+ formatSavedPlanFileEvidence,
23
+ type SavedPlanContentSlugEvidence,
24
+ type SavedPlanFileEvidence,
25
+ } from "@nseng-ai/plans/api";
26
+ import { isRecord } from "@nseng-ai/pi/runtime/primitives";
27
+ import { GRILL_ASK_TOOL_NAME } from "@nseng-ai/pi/grill/surfaces";
28
+ import { resolveBranchContextOperations, resolvePlanStoreRootOption } from "./options.ts";
29
+ import type {
30
+ BranchContextExtensionOptions,
31
+ CommandContext,
32
+ ExtensionAPI,
33
+ ToolContext,
34
+ ToolDefinition,
35
+ ToolResult,
36
+ ToolUpdateHandler,
37
+ } from "./host-types.ts";
38
+
39
+ export { WRITE_GRILLED_PLAN_COMMAND_NAME, WRITE_PLAN_COMMAND_NAME } from "./surfaces.ts";
40
+ const WRITE_PLAN_TOOL_STATUS_KEY = WRITE_PLAN_COMMAND_NAME;
41
+
42
+ interface WriteSavedPlanFileToolParams {
43
+ content: string;
44
+ summary?: string;
45
+ }
46
+
47
+ interface WriteSavedPlanFileToolDetails extends SavedPlanFileEvidence {
48
+ slugEvidence: SavedPlanContentSlugEvidence;
49
+ }
50
+
51
+ type WriteSavedPlanFilePhase = "validating" | "deriving-slug" | "writing-file";
52
+
53
+ interface WriteSavedPlanFileProgressDetails {
54
+ phase: WriteSavedPlanFilePhase;
55
+ slug?: string;
56
+ elapsedSeconds?: number;
57
+ }
58
+
59
+ const WRITE_PLAN_PROMPT_NAME = "plans-write";
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.
67
+
68
+ External research/context contract:
69
+ - 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.
70
+ - Do not merely link to external resources. Summarize the concrete facts, constraints, examples, decisions, and caveats the downstream agent needs.
71
+ - Include source/provenance where useful: URL, command, document name, issue/PR number, accessed date/time if known, and why it mattered.
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.`;
124
+
125
+ type WritePlanPromptBodyResolution =
126
+ | { type: "resolved"; body: string }
127
+ | { type: "fallback"; body: string; warning: string };
128
+
129
+ export function buildWritePlanPrompt(
130
+ steering: string,
131
+ promptBody = DEFAULT_WRITE_PLAN_PROMPT_BODY,
132
+ ): string {
133
+ return `This is a /${WRITE_PLAN_COMMAND_NAME} request. Write a detailed implementation plan and save it in the local plan store.
134
+
135
+ ${formatSteeringBlock(steering)}
136
+
137
+ ${promptBody}`;
138
+ }
139
+
140
+ export function buildWriteGrilledPlanPrompt(steering: string): string {
141
+ return `This is a /${WRITE_GRILLED_PLAN_COMMAND_NAME} request. Write a detailed implementation plan and save it in the local plan store after structured requirements grilling.
142
+
143
+ ${formatSteeringBlock(steering)}
144
+
145
+ Plan audience and target inference:
146
+ - Treat the saved Markdown plan as self-contained context for a completely fresh downstream implementation session.
147
+ - User steering may be empty. Infer the planning target from explicit steering, nearby conversation/session context, and repository evidence, such as a just-produced objective summary or prototype plan.
148
+ - Inspect repository evidence before asking. Do not ask questions answerable from local files, docs, or commands.
149
+
150
+ Structured grilling contract:
151
+ - Use ${GRILL_ASK_TOOL_NAME} for every user-facing grilling question.
152
+ - Ask exactly one question per ${GRILL_ASK_TOOL_NAME} call.
153
+ - Each question must include 2–5 affirmative, mutually exclusive options and a recommendation with concise rationale.
154
+ - Use up to 12 high-leverage questions. Some plans are simple and may not require any user-facing questions; stop early when requirements are resolved, and exceed that budget only if the user explicitly asks to continue.
155
+ - If ${GRILL_ASK_TOOL_NAME} is unavailable or returns ui_unavailable, stop, explain that structured grill UI is required, summarize current status, and do not call write_saved_plan_file.
156
+ - If ${GRILL_ASK_TOOL_NAME} returns status_request, provide a compact status report and re-ask the same pending question; do not count it as an answer.
157
+ - If ${GRILL_ASK_TOOL_NAME} returns end_grill, stop, summarize resolved decisions, unresolved branches, and final recommendation, and do not call write_saved_plan_file.
158
+
159
+ Save/no-save decision:
160
+ - If material requirements remain unresolved after the budget, stop, report blockers, and do not save. Material requirements include command surface, storage behavior, user-visible semantics, compatibility expectations, and irreversible migration or data-safety choices.
161
+ - Do not ask routine validation-scope or test-coverage questions. Ordinary validation coverage is the downstream implementation agent's responsibility, guided by project policy and changed-file judgment.
162
+ - If only non-blocking assumptions remain, fold them into the normal saved plan sections and proceed.
163
+ - Do not include a full Q&A transcript or special Q&A section in the saved plan.
164
+
165
+ Final plan requirements:
166
+ - Produce final Markdown with normal sections: goal/outcome, context/discovered facts, files/symbols/tests/docs, implementation steps, validation guidance, risks/assumptions/open questions, and review/remediation.
167
+ <!-- PLAN-VERIFICATION-WORKSTREAM:START refactor-execution-strategy-guidance -->
168
+ - If the plan includes same-shape edits across multiple files, explicitly choose an execution strategy and apply \`skills/enriched-plan-save/references/refactor-execution-strategy.md\`.
169
+ <!-- PLAN-VERIFICATION-WORKSTREAM:END refactor-execution-strategy-guidance -->
170
+ - Review the final Markdown plan for completeness, then call write_saved_plan_file with the complete content and optional one-sentence summary; do not generate or pass a slug.
171
+ - Report saved plan evidence and stop. Do not create a branch or write Branch Memory.`;
172
+ }
173
+
174
+ async function resolveWritePlanPromptBody(
175
+ pi: ExtensionAPI,
176
+ cwd: string,
177
+ ): Promise<WritePlanPromptBodyResolution> {
178
+ const repoRoot = await resolveGitRoot(new RealGitGateway(piExecApiToCommandExecApi(pi)), cwd);
179
+ if (repoRoot.type === "failed") {
180
+ return fallbackWritePlanPromptBody(repoRoot.reason);
181
+ }
182
+
183
+ try {
184
+ return await readRepoWritePlanPromptBody(repoRoot.path);
185
+ } catch (error) {
186
+ return fallbackWritePlanPromptBody(
187
+ `repo prompt ${repoPromptPath(repoRoot.path)} could not be read: ${formatErrorMessage(error)}`,
188
+ );
189
+ }
190
+ }
191
+
192
+ function fallbackWritePlanPromptBody(reason: string): WritePlanPromptBodyResolution {
193
+ return {
194
+ type: "fallback",
195
+ body: DEFAULT_WRITE_PLAN_PROMPT_BODY,
196
+ warning: `Falling back to built-in /${WRITE_PLAN_COMMAND_NAME} prompt body because ${reason}`,
197
+ };
198
+ }
199
+
200
+ async function resolveGitRoot(
201
+ git: GitGateway,
202
+ cwd: string,
203
+ ): Promise<{ type: "resolved"; path: string } | { type: "failed"; reason: string }> {
204
+ const result = await git.repoRoot({ cwd });
205
+ if (!result.ok) {
206
+ return { type: "failed", reason: result.error.message };
207
+ }
208
+ return { type: "resolved", path: result.value };
209
+ }
210
+
211
+ async function readRepoWritePlanPromptBody(
212
+ repoRoot: string,
213
+ ): Promise<WritePlanPromptBodyResolution> {
214
+ const nsPath = join(repoRoot, ".ns");
215
+ const promptDir = join(nsPath, "prompts");
216
+ const promptPath = repoPromptPath(repoRoot);
217
+ await assertSafeDirectory(nsPath, ".ns");
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`);
224
+ }
225
+ return { type: "resolved", body: content };
226
+ }
227
+
228
+ async function assertSafeDirectory(targetPath: string, label: string): Promise<void> {
229
+ const stats = await assertNotSymlink(targetPath, label);
230
+ if (!stats.isDirectory()) {
231
+ throw new Error(`${label} is not a directory`);
232
+ }
233
+ }
234
+
235
+ async function assertSafeFile(targetPath: string, label: string): Promise<void> {
236
+ const stats = await assertNotSymlink(targetPath, label);
237
+ if (!stats.isFile()) {
238
+ throw new Error(`${label} is not a file`);
239
+ }
240
+ }
241
+
242
+ async function assertNotSymlink(targetPath: string, label: string): Promise<Stats> {
243
+ const stats = await lstat(targetPath);
244
+ if (stats.isSymbolicLink()) {
245
+ throw new Error(`${label} is a symlink`);
246
+ }
247
+ return stats;
248
+ }
249
+
250
+ function repoPromptPath(repoRoot: string): string {
251
+ return join(repoRoot, ".ns", "prompts", `${WRITE_PLAN_PROMPT_NAME}.md`);
252
+ }
253
+
254
+ export async function handleWritePlanCommand(
255
+ pi: ExtensionAPI,
256
+ args: string,
257
+ ctx: CommandContext,
258
+ ): Promise<void> {
259
+ const steering = args.trim();
260
+ sendCommandProgressOrNotify({
261
+ host: pi,
262
+ ctx,
263
+ message: `Starting /${WRITE_PLAN_COMMAND_NAME} planning turn…`,
264
+ });
265
+ await ctx.waitForIdle();
266
+ const promptBody = await resolveWritePlanPromptBody(pi, ctx.cwd);
267
+ if (promptBody.type === "fallback" && ctx.hasUI) {
268
+ ctx.ui.notify(promptBody.warning, "warning");
269
+ }
270
+ pi.sendUserMessage(buildWritePlanPrompt(steering, promptBody.body));
271
+ }
272
+
273
+ export async function handleWriteGrilledPlanCommand(
274
+ pi: ExtensionAPI,
275
+ args: string,
276
+ ctx: CommandContext,
277
+ ): Promise<void> {
278
+ const steering = args.trim();
279
+ sendCommandProgressOrNotify({
280
+ host: pi,
281
+ ctx,
282
+ message: `Starting /${WRITE_GRILLED_PLAN_COMMAND_NAME} planning grill…`,
283
+ });
284
+ await ctx.waitForIdle();
285
+ pi.sendUserMessage(buildWriteGrilledPlanPrompt(steering));
286
+ }
287
+
288
+ export function buildWriteSavedPlanFileTool(
289
+ pi: ExtensionAPI,
290
+ options: BranchContextExtensionOptions,
291
+ ): ToolDefinition {
292
+ return {
293
+ name: WRITE_SAVED_PLAN_FILE_TOOL_NAME,
294
+ label: "Write Saved Plan File",
295
+ description:
296
+ "Create a reviewed, self-contained Markdown implementation plan file for a fresh downstream implementation session in the XDG local plan store at `$XDG_STATE_HOME/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md` (default `$HOME/.local/state/ns/enriched-plan/...`). The tool derives the saved-plan filename slug from the content through the Codex-backed slug model, derives repo and current branch from git, validates the slug, creates parent directories, refuses to overwrite an existing file, writes the full Markdown content, and returns path evidence. It does not create branches or write Branch Memory.",
297
+ promptSnippet:
298
+ "Create a reviewed, self-contained Markdown implementation plan file in the XDG local plan store under `$XDG_STATE_HOME/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md` (default `$HOME/.local/state/ns/enriched-plan/...`).",
299
+ promptGuidelines: [
300
+ `Use write_saved_plan_file for \`/${WRITE_PLAN_COMMAND_NAME}\` and \`/${WRITE_GRILLED_PLAN_COMMAND_NAME}\` after producing a reviewed final Markdown plan.`,
301
+ "Do not generate or pass a saved-plan filename slug; write_saved_plan_file derives it from content through the Codex-backed slug model.",
302
+ "write_saved_plan_file writes the XDG local plan store under `$XDG_STATE_HOME/ns/enriched-plan/<repo>/<encoded-source-branch>/<slug>.md` (default `$HOME/.local/state/ns/enriched-plan/...`); it does not create branches or write Branch Memory.",
303
+ "write_saved_plan_file content should be self-contained for a completely fresh downstream implementation session, including relevant context discovered during planning.",
304
+ "If planning used external/off-repo research, write_saved_plan_file content should include the concrete findings and provenance inline instead of relying on links or hidden conversation context.",
305
+ "If write_saved_plan_file reports that the saved plan file already exists, stop and report the collision; never overwrite the existing file.",
306
+ ],
307
+ parameters: {
308
+ type: "object",
309
+ additionalProperties: false,
310
+ properties: {
311
+ content: {
312
+ type: "string",
313
+ description:
314
+ "Complete reviewed, self-contained Markdown plan content to write, including relevant planning context and external research findings.",
315
+ },
316
+ summary: {
317
+ type: "string",
318
+ description: "Optional one-sentence summary of the plan.",
319
+ },
320
+ },
321
+ required: ["content"],
322
+ },
323
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
324
+ const operations = resolveBranchContextOperations(options);
325
+ try {
326
+ emitWriteSavedPlanProgress(onUpdate, ctx, "Validating saved plan input…", {
327
+ phase: "validating",
328
+ });
329
+ const toolParams = parseWriteSavedPlanFileToolParams(params);
330
+ emitWriteSavedPlanProgress(onUpdate, ctx, "Deriving saved-plan filename slug with Codex…", {
331
+ phase: "deriving-slug",
332
+ });
333
+ const slugStartedAt = Date.now();
334
+ const slugProgressInterval: ScheduledTimer | undefined =
335
+ onUpdate === undefined && !canSetWriteSavedPlanStatus(ctx)
336
+ ? undefined
337
+ : systemTimerScheduler.setInterval(() => {
338
+ const elapsedSeconds = Math.round((Date.now() - slugStartedAt) / 1_000);
339
+ emitWriteSavedPlanProgress(
340
+ onUpdate,
341
+ ctx,
342
+ `Deriving saved-plan filename slug with Codex… ${elapsedSeconds}s elapsed`,
343
+ {
344
+ phase: "deriving-slug",
345
+ elapsedSeconds,
346
+ },
347
+ );
348
+ }, 5_000);
349
+ let slugEvidence: SavedPlanContentSlugEvidence;
350
+ try {
351
+ slugEvidence = await deriveSavedPlanContentSlug(pi, {
352
+ content: toolParams.content,
353
+ cwd: ctx.cwd,
354
+ ...optionalEntry("signal", signal),
355
+ });
356
+ } finally {
357
+ if (slugProgressInterval !== undefined) {
358
+ slugProgressInterval.cancel();
359
+ }
360
+ }
361
+ emitWriteSavedPlanProgress(
362
+ onUpdate,
363
+ ctx,
364
+ `Derived slug ${slugEvidence.slug}; resolving repo/branch and writing plan file…`,
365
+ { phase: "writing-file", slug: slugEvidence.slug },
366
+ );
367
+ emitWriteSavedPlanProgress(onUpdate, ctx, "Writing plan file…", {
368
+ phase: "writing-file",
369
+ slug: slugEvidence.slug,
370
+ });
371
+ const planStoreRoot = resolvePlanStoreRootOption(options);
372
+ const evidence = await operations.writeSavedPlanFile(
373
+ pi,
374
+ buildSavedPlanFileParams(toolParams, slugEvidence.slug),
375
+ {
376
+ cwd: ctx.cwd,
377
+ ...optionalEntries({ signal, planStoreRoot }),
378
+ },
379
+ );
380
+ const details: WriteSavedPlanFileToolDetails = { ...evidence, slugEvidence };
381
+ return {
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: formatSavedPlanFileEvidenceWithSlugModel(evidence, slugEvidence),
386
+ },
387
+ ],
388
+ details,
389
+ };
390
+ } finally {
391
+ setWriteSavedPlanStatus(ctx, undefined);
392
+ }
393
+ },
394
+ renderCall(args, _theme, context) {
395
+ return new Text(formatWriteSavedPlanFileCall(args, context), 0, 0);
396
+ },
397
+ renderResult(result, { isPartial }) {
398
+ const text = formatToolResultText(result);
399
+ if (isPartial) {
400
+ return new Text(`Saving branch-context plan…\n${text}`, 0, 0);
401
+ }
402
+ return new Text(text, 0, 0);
403
+ },
404
+ };
405
+ }
406
+
407
+ function emitWriteSavedPlanProgress(
408
+ onUpdate: ToolUpdateHandler | undefined,
409
+ ctx: ToolContext,
410
+ text: string,
411
+ details: WriteSavedPlanFileProgressDetails,
412
+ ): void {
413
+ onUpdate?.({ content: [{ type: "text", text }], details });
414
+ setWriteSavedPlanStatus(ctx, text);
415
+ }
416
+
417
+ function setWriteSavedPlanStatus(ctx: ToolContext, value: string | undefined): void {
418
+ if (ctx.hasUI === false) {
419
+ return;
420
+ }
421
+ ctx.ui?.setStatus?.(WRITE_PLAN_TOOL_STATUS_KEY, value);
422
+ }
423
+
424
+ function canSetWriteSavedPlanStatus(ctx: ToolContext): boolean {
425
+ if (ctx.hasUI === false) {
426
+ return false;
427
+ }
428
+ return ctx.ui?.setStatus !== undefined;
429
+ }
430
+
431
+ function formatToolResultText(result: ToolResult): string {
432
+ return result.content.map((item) => item.text).join("\n");
433
+ }
434
+
435
+ function formatWriteSavedPlanFileCall(args: unknown, context: unknown): string {
436
+ const content = isRecord(args) && typeof args.content === "string" ? args.content : undefined;
437
+ const tokenEstimate = content === undefined ? "" : ` ${formatEstimatedTokenCount(content)}`;
438
+ if (isToolExecutionStarted(context)) {
439
+ return `${WRITE_SAVED_PLAN_FILE_TOOL_NAME} — saving reviewed plan…${tokenEstimate}`;
440
+ }
441
+
442
+ return `${WRITE_SAVED_PLAN_FILE_TOOL_NAME} — receiving saved-plan content from model…${tokenEstimate}`;
443
+ }
444
+
445
+ function isToolExecutionStarted(context: unknown): boolean {
446
+ return isRecord(context) && context.executionStarted === true;
447
+ }
448
+
449
+ const ESTIMATED_CHARS_PER_TOKEN = 4;
450
+
451
+ function formatEstimatedTokenCount(text: string): string {
452
+ return `${formatCount(Math.ceil(text.length / ESTIMATED_CHARS_PER_TOKEN))} tokens (est.)`;
453
+ }
454
+
455
+ function formatCount(count: number): string {
456
+ if (count < 1_000) {
457
+ return `${count}`;
458
+ }
459
+ if (count < 1_000_000) {
460
+ return `${formatCompactNumber(count / 1_000)}k`;
461
+ }
462
+ return `${formatCompactNumber(count / 1_000_000)}m`;
463
+ }
464
+
465
+ function formatCompactNumber(value: number): string {
466
+ const formatted = value >= 10 ? value.toFixed(0) : value.toFixed(1);
467
+ return formatted.replace(/\.0$/, "");
468
+ }
469
+
470
+ function parseWriteSavedPlanFileToolParams(params: unknown): WriteSavedPlanFileToolParams {
471
+ return parseWriteSavedPlanFileToolParamsForName(params, WRITE_SAVED_PLAN_FILE_TOOL_NAME);
472
+ }
473
+
474
+ function parseWriteSavedPlanFileToolParamsForName(
475
+ params: unknown,
476
+ toolName: string,
477
+ ): WriteSavedPlanFileToolParams {
478
+ if (!isRecord(params)) {
479
+ throw new Error(`${toolName} parameters must be an object.`);
480
+ }
481
+ if ("slug" in params) {
482
+ throw new Error(
483
+ `${toolName} derives \`slug\` from content through Codex; do not pass \`slug\`.`,
484
+ );
485
+ }
486
+
487
+ const content = params.content;
488
+ const summary = params.summary;
489
+ if (typeof content !== "string") {
490
+ throw new Error(`${toolName} requires string parameter \`content\`.`);
491
+ }
492
+ if (summary !== undefined && typeof summary !== "string") {
493
+ throw new Error(`${toolName} parameter \`summary\` must be a string when provided.`);
494
+ }
495
+
496
+ if (summary === undefined) {
497
+ return { content };
498
+ }
499
+ return { content, summary };
500
+ }
501
+
502
+ function buildSavedPlanFileParams(
503
+ params: WriteSavedPlanFileToolParams,
504
+ slug: string,
505
+ ): { slug: string; content: string; summary?: string } {
506
+ if (params.summary === undefined) {
507
+ return { slug, content: params.content };
508
+ }
509
+ return { slug, content: params.content, summary: params.summary };
510
+ }
511
+
512
+ function formatSavedPlanFileEvidenceWithSlugModel(
513
+ evidence: SavedPlanFileEvidence,
514
+ slugEvidence: SavedPlanContentSlugEvidence,
515
+ ): string {
516
+ return `${formatSavedPlanFileEvidence(evidence)}\nSlug model: ${slugEvidence.provider}/${slugEvidence.model}`;
517
+ }
518
+
519
+ function formatSteeringBlock(steering: string): string {
520
+ const trimmedSteering = steering.trim();
521
+ if (!trimmedSteering) {
522
+ return "User steering for this planning request: (none)";
523
+ }
524
+
525
+ return `User steering for this planning request:\n\n\`\`\`text\n${trimmedSteering}\n\`\`\``;
526
+ }
527
+
528
+ export function registerEnrichedPlanCommandsAndTools(
529
+ pi: ExtensionAPI,
530
+ options: BranchContextExtensionOptions = {},
531
+ ): void {
532
+ registerCommandWithImmediateAck({
533
+ host: pi,
534
+ commandName: WRITE_PLAN_COMMAND_NAME,
535
+ commandDefinition: {
536
+ description: "Write and save a reviewed implementation plan in the local plan store.",
537
+ handler: async (args, ctx) => handleWritePlanCommand(pi, args, ctx),
538
+ },
539
+ });
540
+
541
+ registerCommandWithImmediateAck({
542
+ host: pi,
543
+ commandName: WRITE_GRILLED_PLAN_COMMAND_NAME,
544
+ commandDefinition: {
545
+ description: "Write and save a grilled implementation plan using structured requirements UI.",
546
+ handler: async (args, ctx) => handleWriteGrilledPlanCommand(pi, args, ctx),
547
+ },
548
+ });
549
+
550
+ pi.registerTool(buildWriteSavedPlanFileTool(pi, options));
551
+ }