@nseng-ai/ccc 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/ccc",
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
+ "ccc": "src/ns/cli.ts"
17
+ },
18
+ "exports": {
19
+ ".": "./src/api/index.ts",
20
+ "./api": "./src/api/handlers.ts",
21
+ "./autoslot": "./src/ns/autoslot.ts",
22
+ "./cli": "./src/ns/cli.ts",
23
+ "./land": "./src/ns/land.ts",
24
+ "./trunk-pull": "./src/ns/trunk-pull.ts",
25
+ "./pi": "./src/pi/index.ts",
26
+ "./pi/extension": "./src/pi/extension.ts"
27
+ },
28
+ "dependencies": {
29
+ "@nseng-ai/branch-context": "0.1.1",
30
+ "@nseng-ai/capability-kit": "0.1.1",
31
+ "@nseng-ai/clinkr": "0.1.1",
32
+ "@nseng-ai/foundation": "0.1.1",
33
+ "@nseng-ai/flow": "0.1.1",
34
+ "@nseng-ai/ns": "0.1.1",
35
+ "@nseng-ai/objectives": "0.1.1",
36
+ "@nseng-ai/plans": "0.1.1",
37
+ "@nseng-ai/slots": "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,75 @@
1
+ export { CCC_PACKAGE_IDENTITY } from "./index.ts";
2
+
3
+ // Canonical ns:ccc:* command surface names
4
+ export {
5
+ CCC_CLAUDE_PLAN_TAB_COMMAND_NAME,
6
+ CCC_COMMAND_NAMES,
7
+ CCC_SIDEBAR_BRANCH_STATE_SUMMARY_COMMAND_NAME,
8
+ CCC_SIDEBAR_OBJECTIVE_SUMMARY_COMMAND_NAME,
9
+ CCC_SIDEBAR_SESSION_SUMMARY_COMMAND_NAME,
10
+ CCC_SURFACE_DISPATCH_PLAN_COMMAND_NAME,
11
+ CCC_WORKSPACE_DISPATCH_FROM_TRUNK_COMMAND_NAME,
12
+ CCC_WORKSPACE_DISPATCH_PLAN_COMMAND_NAME,
13
+ CCC_WORKSPACE_DISPATCH_PROMPT_COMMAND_NAME,
14
+ CCC_WORKSPACE_OPEN_BRANCH_COMMAND_NAME,
15
+ } from "../cmux/command-surfaces.ts";
16
+
17
+ // Dispatch prompt
18
+ export {
19
+ handleCccSlotDispatchPrompt,
20
+ resolveDispatchPromptPayloadOptions,
21
+ type DispatchPromptPayloadOptions,
22
+ type HandleCccSlotDispatchPromptOptions,
23
+ } from "../cmux/dispatch-prompt.ts";
24
+
25
+ // Claude plan tab
26
+ export {
27
+ handleCccClaudePlanTab,
28
+ extractLastAssistantText,
29
+ buildClaudePlanTabTitle,
30
+ buildClaudePlanLaunchCommand,
31
+ } from "../cmux/claude-plan-tab.ts";
32
+
33
+ // Prompt file utilities
34
+ export {
35
+ resolvePromptFileOptions,
36
+ writeTimestampedPromptFile,
37
+ type PromptFileOptions,
38
+ type ResolvedPromptFileOptions,
39
+ } from "../cmux/prompt-file.ts";
40
+
41
+ // Dispatch from trunk
42
+ export {
43
+ handleCccSlotDispatchFromTrunk,
44
+ createTrackedBranchFromTrunkForPrompt,
45
+ } from "../cmux/dispatch-from-trunk.ts";
46
+
47
+ // Slot open branch
48
+ export {
49
+ handleCccSlotOpenBranch,
50
+ getBranchCompletions,
51
+ extractCommandArgumentPrefix,
52
+ type HandleCccSlotOpenBranchOptions,
53
+ type CccSlotOpenBranchOptions,
54
+ } from "../cmux/slot-open-branch.ts";
55
+
56
+ // Slot dispatch plan
57
+ export {
58
+ handleCccSlotDispatchPlan,
59
+ type CccSlotDispatchPlanOptions,
60
+ type DispatchPlanConfig,
61
+ type DispatchDestination,
62
+ } from "../cmux/slot-dispatch-plan.ts";
63
+
64
+ // Sidebar
65
+ export {
66
+ createCccSidebarController,
67
+ getCallerWorkspaceId,
68
+ buildCmuxSessionSidebarPrompt,
69
+ buildCmuxBranchStateSidebarPrompt,
70
+ type CccSidebarController,
71
+ type ObjectiveSidebarHandlerOptions,
72
+ } from "../cmux/sidebar.ts";
73
+
74
+ // Slot client
75
+ export type { SlotClient } from "@nseng-ai/slots/api";
@@ -0,0 +1,11 @@
1
+ export const CCC_PACKAGE_IDENTITY = {
2
+ packageName: "@nseng-ai/ccc",
3
+ vocabularyName: "CCC",
4
+ expandedName: "Cmux Command and Control",
5
+ visibility: "private-workspace",
6
+ ownedConcerns: [
7
+ "cmux-workspace-orchestration",
8
+ "graphite-stack-orchestration",
9
+ "worktree-flow-coordination",
10
+ ],
11
+ } as const;
@@ -0,0 +1,174 @@
1
+ import {
2
+ finalizeBranchSlug,
3
+ MAX_BRANCH_SLUG_LENGTH,
4
+ sanitizeBranchName,
5
+ trimBranchSlugToLength,
6
+ } from "@nseng-ai/foundation/branch-slug";
7
+ import type { TextResult } from "@nseng-ai/foundation/primitives";
8
+ import type { ExtensionAPI } from "@nseng-ai/capability-kit/cmux/types";
9
+
10
+ export { finalizeBranchSlug, MAX_BRANCH_SLUG_LENGTH, sanitizeBranchName, trimBranchSlugToLength };
11
+
12
+ const GPT_NANO_PROVIDER = "openai";
13
+ const GPT_NANO_MODEL = "gpt-5.4-nano";
14
+ const SLUG_PROMPT_TIMEOUT_MS = 60_000;
15
+ const SUMMARY_PROMPT_TIMEOUT_MS = 60_000;
16
+ const MAX_SLUG_INPUT_CHARS = 12_000;
17
+ const MAX_SUMMARY_INPUT_CHARS = 16_000;
18
+
19
+ export type BranchSlugContentKind = "task" | "plan";
20
+
21
+ type BranchSlugRuntime = Pick<ExtensionAPI, "exec">;
22
+
23
+ export async function generateBranchSlug(
24
+ pi: BranchSlugRuntime,
25
+ cwd: string,
26
+ input: {
27
+ kind: BranchSlugContentKind;
28
+ content: string;
29
+ sourceLabel?: string;
30
+ fallbackText?: string;
31
+ },
32
+ ): Promise<TextResult> {
33
+ const prompt = buildSlugPrompt(input);
34
+ const result = await runGptNanoText(pi, cwd, prompt, SLUG_PROMPT_TIMEOUT_MS);
35
+ if (!result.ok) {
36
+ return {
37
+ ok: false,
38
+ message: `Could not generate branch slug with GPT Nano: ${result.message}`,
39
+ };
40
+ }
41
+
42
+ const slug =
43
+ sanitizeBranchName(result.text) || sanitizeBranchName(input.fallbackText ?? input.content);
44
+ if (!slug) {
45
+ return { ok: false, message: "Could not derive a usable branch slug." };
46
+ }
47
+
48
+ return { ok: true, text: slug };
49
+ }
50
+
51
+ export async function summarizePlanWithGptNano(
52
+ pi: BranchSlugRuntime,
53
+ cwd: string,
54
+ input: {
55
+ content: string;
56
+ sourceLabel?: string;
57
+ },
58
+ ): Promise<TextResult> {
59
+ const result = await runGptNanoText(
60
+ pi,
61
+ cwd,
62
+ buildPlanSummaryPrompt(input),
63
+ SUMMARY_PROMPT_TIMEOUT_MS,
64
+ );
65
+ if (!result.ok) {
66
+ return { ok: false, message: `Could not summarize plan with GPT Nano: ${result.message}` };
67
+ }
68
+
69
+ const summary = stripResponseFence(result.text).trim();
70
+ if (!summary) {
71
+ return { ok: false, message: "GPT Nano did not return a usable bullet summary." };
72
+ }
73
+ return { ok: true, text: summary };
74
+ }
75
+
76
+ async function runGptNanoText(
77
+ pi: BranchSlugRuntime,
78
+ cwd: string,
79
+ prompt: string,
80
+ timeout: number,
81
+ ): Promise<TextResult> {
82
+ const result = await pi.exec("pi", buildGptNanoTextArgs(prompt), { cwd, timeout });
83
+ if (result.code !== 0) {
84
+ const details = result.stderr.trim() || result.stdout.trim();
85
+ return { ok: false, message: details };
86
+ }
87
+ return { ok: true, text: result.stdout.trim() };
88
+ }
89
+
90
+ export function buildGptNanoTextArgs(prompt: string): string[] {
91
+ return [
92
+ "--provider",
93
+ GPT_NANO_PROVIDER,
94
+ "--model",
95
+ GPT_NANO_MODEL,
96
+ "--thinking",
97
+ "low",
98
+ "--no-session",
99
+ "--no-extensions",
100
+ "--no-skills",
101
+ "--no-prompt-templates",
102
+ "--no-context-files",
103
+ "--no-tools",
104
+ "--mode",
105
+ "text",
106
+ "--print",
107
+ prompt,
108
+ ];
109
+ }
110
+
111
+ export function buildSlugPrompt(input: {
112
+ kind: BranchSlugContentKind;
113
+ content: string;
114
+ sourceLabel?: string;
115
+ }): string {
116
+ const kindDescription =
117
+ input.kind === "plan"
118
+ ? "an implementation plan that will be stashed on a new branch"
119
+ : "a user task prompt that will run in a new branch workspace";
120
+ const contentLabel = input.sourceLabel ? `Source: ${input.sourceLabel}` : undefined;
121
+
122
+ return [
123
+ "Generate a concise git branch slug for this work item.",
124
+ `The content is ${kindDescription}.`,
125
+ "Infer the actual code/product change or outcome. Do not name the document, prompt, plan, context, storage workflow, or how this work item was initiated.",
126
+ "Ignore metadata and provenance such as saved-plan filenames, source labels, suggested slugs, objective-next output, branch-create handoff text, and brmem storage details.",
127
+ "If a command name appears only because it generated or initiated the plan, do not include it. Include command/product names only when the proposed work directly changes that command/product.",
128
+ "Rules:",
129
+ "- Return only the slug, with no quotes, markdown, or explanation.",
130
+ "- Use kebab-case: lowercase ASCII words separated by hyphens.",
131
+ `- Keep it at or under ${MAX_BRANCH_SLUG_LENGTH} characters.`,
132
+ "- Lead with a verb when natural, such as add-, fix-, refactor-, migrate-, rename-, remove-, or update-.",
133
+ "- Do not use spaces, underscores, slashes, punctuation, or special characters.",
134
+ "- Do not include generic suffixes like -plan, -prompt, -context, -branch, -task, or -suggestion unless they are the real feature name.",
135
+ "- Prefer concrete deliverables and specific nouns from the work item over broad words like changes, cleanup, or improvements.",
136
+ "",
137
+ contentLabel,
138
+ "Content:",
139
+ truncateForPrompt(input.content, MAX_SLUG_INPUT_CHARS),
140
+ ]
141
+ .filter((line): line is string => line !== undefined)
142
+ .join("\n");
143
+ }
144
+
145
+ function buildPlanSummaryPrompt(input: { content: string; sourceLabel?: string }): string {
146
+ const contentLabel = input.sourceLabel ? `Source: ${input.sourceLabel}` : undefined;
147
+ return [
148
+ "Summarize this implementation plan for display after it is persisted on a branch.",
149
+ "Return only 3 to 5 markdown bullet lines.",
150
+ "Each bullet must start with '- '. No heading, intro, code fence, or closing prose.",
151
+ "Focus on concrete implementation work, important scope boundaries, and validation steps.",
152
+ "Do not mention that the plan was stashed, persisted, saved, or stored.",
153
+ "",
154
+ contentLabel,
155
+ "Plan:",
156
+ truncateForPrompt(input.content, MAX_SUMMARY_INPUT_CHARS),
157
+ ]
158
+ .filter((line): line is string => line !== undefined)
159
+ .join("\n");
160
+ }
161
+
162
+ function truncateForPrompt(text: string, maxChars: number): string {
163
+ if (text.length <= maxChars) {
164
+ return text;
165
+ }
166
+ return `${text.slice(0, maxChars)}\n...[truncated]`;
167
+ }
168
+
169
+ function stripResponseFence(value: string): string {
170
+ return value
171
+ .replace(/^```[a-zA-Z0-9_-]*\n?/, "")
172
+ .replace(/\n?```$/, "")
173
+ .trim();
174
+ }
@@ -0,0 +1,124 @@
1
+ import { formatShellArg } from "@nseng-ai/foundation/command";
2
+ import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
3
+ import {
4
+ launchFocusedCmuxTab,
5
+ type FocusedCmuxTabLaunchResult,
6
+ } from "@nseng-ai/capability-kit/cmux/focused-terminal-tab";
7
+ import { isRecord, stringField } from "@nseng-ai/foundation/primitives";
8
+ import { writeTimestampedPromptFile, type ResolvedPromptFileOptions } from "./prompt-file.ts";
9
+ import type { CommandContext, ExtensionAPI } from "@nseng-ai/capability-kit/cmux/types";
10
+
11
+ const TITLE_PREFIX = "claude-plan: ";
12
+ const MAX_TITLE_SEED_CHARS = 40;
13
+
14
+ export async function handleCccClaudePlanTab(options: {
15
+ pi: ExtensionAPI;
16
+ ctx: CommandContext;
17
+ args: string;
18
+ promptOptions: ResolvedPromptFileOptions;
19
+ }): Promise<void> {
20
+ const { pi, ctx, promptOptions } = options;
21
+ await ctx.waitForIdle();
22
+
23
+ const seed = resolveClaudePlanSeed(ctx, options.args);
24
+ if (seed === undefined) {
25
+ ctx.ui.notify("No assistant message found in this session to use as a seed plan.", "error");
26
+ return;
27
+ }
28
+
29
+ let promptFile: string;
30
+ try {
31
+ promptFile = await writeTimestampedPromptFile({
32
+ ...promptOptions,
33
+ stem: "claude-plan",
34
+ content: seed,
35
+ });
36
+ } catch (error) {
37
+ ctx.ui.notify(`Failed to write Claude plan prompt file: ${formatErrorMessage(error)}`, "error");
38
+ return;
39
+ }
40
+
41
+ const launched = await launchFocusedCmuxTab({
42
+ host: pi,
43
+ cwd: ctx.cwd,
44
+ tabTitle: buildClaudePlanTabTitle(seed),
45
+ command: buildClaudePlanLaunchCommand(promptFile),
46
+ signal: undefined,
47
+ });
48
+ if (launched.type === "failed") {
49
+ ctx.ui.notify(launched.message, "error");
50
+ return;
51
+ }
52
+
53
+ ctx.ui.notify(formatClaudePlanTabLaunchSuccess(launched, promptFile), "info");
54
+ }
55
+
56
+ export function extractLastAssistantText(entries: unknown[]): string | undefined {
57
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
58
+ const entry = entries[index];
59
+ if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) continue;
60
+ if (stringField(entry.message, "role") !== "assistant") continue;
61
+
62
+ return textFromAssistantMessage(entry.message);
63
+ }
64
+
65
+ return undefined;
66
+ }
67
+
68
+ export function buildClaudePlanLaunchCommand(promptFilePath: string): string {
69
+ return `claude --permission-mode plan "$(cat ${formatShellArg(promptFilePath)})"`;
70
+ }
71
+
72
+ function resolveClaudePlanSeed(ctx: CommandContext, args: string): string | undefined {
73
+ if (args.trim().length > 0) return args;
74
+
75
+ const entries = ctx.sessionManager?.getBranch?.() ?? ctx.sessionManager?.getEntries?.() ?? [];
76
+ return extractLastAssistantText(entries);
77
+ }
78
+
79
+ export function buildClaudePlanTabTitle(seed: string): string {
80
+ const firstLine = seed
81
+ .split(/\r?\n/)
82
+ .map((line) => line.trim())
83
+ .find((line) => line.length > 0);
84
+ const titleSeed = truncateTitleSeed(firstLine ?? "seed plan");
85
+ return `${TITLE_PREFIX}${titleSeed}`;
86
+ }
87
+
88
+ function formatClaudePlanTabLaunchSuccess(
89
+ result: Extract<FocusedCmuxTabLaunchResult, { type: "launched" }>,
90
+ promptFile: string,
91
+ ): string {
92
+ return [
93
+ "Opened Claude plan tab.",
94
+ `Tab title: ${result.tabTitle}`,
95
+ `Surface: ${result.surfaceId}`,
96
+ `Workspace: ${result.workspaceId}`,
97
+ `Prompt file: ${promptFile}`,
98
+ `Command: ${result.command}`,
99
+ ].join("\n");
100
+ }
101
+
102
+ function textFromAssistantMessage(message: Record<string, unknown>): string | undefined {
103
+ const text = textFromAssistantContent(message.content);
104
+ if (text === undefined) return undefined;
105
+ if (text.trim().length === 0) return undefined;
106
+ return text;
107
+ }
108
+
109
+ function textFromAssistantContent(content: unknown): string | undefined {
110
+ if (typeof content === "string") return content;
111
+ if (!Array.isArray(content)) return undefined;
112
+ return content.map(textFromContentBlock).join("");
113
+ }
114
+
115
+ function textFromContentBlock(block: unknown): string {
116
+ if (!isRecord(block) || stringField(block, "type") !== "text") return "";
117
+ return stringField(block, "text") ?? "";
118
+ }
119
+
120
+ function truncateTitleSeed(seed: string): string {
121
+ const chars = Array.from(seed);
122
+ if (chars.length <= MAX_TITLE_SEED_CHARS) return seed;
123
+ return `${chars.slice(0, MAX_TITLE_SEED_CHARS - 1).join("")}…`;
124
+ }
@@ -0,0 +1,51 @@
1
+ import { nsCommandSurface } from "@nseng-ai/foundation/command";
2
+
3
+ // Canonical catalog of ns:ccc:* command surfaces. Pi command registration and
4
+ // cmux extension commands share these names; import from here instead of
5
+ // spelling out raw `ns:ccc:*` literals.
6
+
7
+ export const CCC_WORKSPACE_DISPATCH_PLAN_COMMAND_NAME = nsCommandSurface(
8
+ "ccc",
9
+ "workspace:dispatch-plan",
10
+ );
11
+ export const CCC_SURFACE_DISPATCH_PLAN_COMMAND_NAME = nsCommandSurface(
12
+ "ccc",
13
+ "surface:dispatch-plan",
14
+ );
15
+ export const CCC_WORKSPACE_OPEN_BRANCH_COMMAND_NAME = nsCommandSurface(
16
+ "ccc",
17
+ "workspace:open-branch",
18
+ );
19
+ export const CCC_WORKSPACE_DISPATCH_PROMPT_COMMAND_NAME = nsCommandSurface(
20
+ "ccc",
21
+ "workspace:dispatch-prompt",
22
+ );
23
+ export const CCC_WORKSPACE_DISPATCH_FROM_TRUNK_COMMAND_NAME = nsCommandSurface(
24
+ "ccc",
25
+ "workspace:dispatch-from-trunk",
26
+ );
27
+ export const CCC_CLAUDE_PLAN_TAB_COMMAND_NAME = nsCommandSurface("ccc", "claude-plan-tab");
28
+ export const CCC_SIDEBAR_SESSION_SUMMARY_COMMAND_NAME = nsCommandSurface(
29
+ "ccc",
30
+ "sidebar:session-summary",
31
+ );
32
+ export const CCC_SIDEBAR_BRANCH_STATE_SUMMARY_COMMAND_NAME = nsCommandSurface(
33
+ "ccc",
34
+ "sidebar:branch-state-summary",
35
+ );
36
+ export const CCC_SIDEBAR_OBJECTIVE_SUMMARY_COMMAND_NAME = nsCommandSurface(
37
+ "ccc",
38
+ "sidebar:objective-summary",
39
+ );
40
+
41
+ export const CCC_COMMAND_NAMES = [
42
+ CCC_CLAUDE_PLAN_TAB_COMMAND_NAME,
43
+ CCC_SIDEBAR_BRANCH_STATE_SUMMARY_COMMAND_NAME,
44
+ CCC_SIDEBAR_OBJECTIVE_SUMMARY_COMMAND_NAME,
45
+ CCC_SIDEBAR_SESSION_SUMMARY_COMMAND_NAME,
46
+ CCC_SURFACE_DISPATCH_PLAN_COMMAND_NAME,
47
+ CCC_WORKSPACE_DISPATCH_FROM_TRUNK_COMMAND_NAME,
48
+ CCC_WORKSPACE_DISPATCH_PLAN_COMMAND_NAME,
49
+ CCC_WORKSPACE_DISPATCH_PROMPT_COMMAND_NAME,
50
+ CCC_WORKSPACE_OPEN_BRANCH_COMMAND_NAME,
51
+ ] as const;
@@ -0,0 +1,177 @@
1
+ import {
2
+ commandSucceeded,
3
+ execApiToCommandRunner,
4
+ formatCommand,
5
+ formatCommandFailure,
6
+ piExecApiToCommandExecApi,
7
+ } from "@nseng-ai/foundation/command";
8
+ import { firstNonEmptyLine } from "@nseng-ai/foundation/text-normalization";
9
+ import {
10
+ planLocalBranchRefreshFromWorktrees,
11
+ type LocalBranchRefreshPlan,
12
+ } from "@nseng-ai/capability-kit/git";
13
+ import { runGraphiteCommand } from "@nseng-ai/capability-kit/graphite/branch";
14
+ import { optionalEntry } from "@nseng-ai/foundation/primitives";
15
+
16
+ import { CCC_WORKSPACE_DISPATCH_FROM_TRUNK_COMMAND_NAME } from "./command-surfaces.ts";
17
+ import {
18
+ buildLaunchPrompt,
19
+ createTrackedBranchFromResolvedParent,
20
+ dispatchTrackedBranchPrompt,
21
+ resolveDispatchPromptPayloadOptions,
22
+ runText,
23
+ type BranchCreateResult,
24
+ } from "./dispatch-prompt.ts";
25
+ import type { SlotClient } from "@nseng-ai/slots/api";
26
+ import type { CommandContext, ExtensionAPI } from "@nseng-ai/capability-kit/cmux/types";
27
+
28
+ const COMMAND_NAME = CCC_WORKSPACE_DISPATCH_FROM_TRUNK_COMMAND_NAME;
29
+ const GIT_TRUNK_REFRESH_TIMEOUT_MS = 2 * 60 * 1000;
30
+ const TRUNK_DISPATCH_CONTEXT_NOTE =
31
+ "This branch was created from refreshed Graphite trunk and is intentionally unrelated to the caller's current stack.";
32
+
33
+ export async function handleCccSlotDispatchFromTrunk(options: {
34
+ pi: Pick<ExtensionAPI, "exec" | "getThinkingLevel">;
35
+ payloadOptions: ReturnType<typeof resolveDispatchPromptPayloadOptions>;
36
+ args: string;
37
+ ctx: CommandContext;
38
+ slotClient?: SlotClient;
39
+ notifyProgress: (message: string) => void;
40
+ }): Promise<void> {
41
+ const { pi, payloadOptions, args, ctx } = options;
42
+ const prompt = args.trim();
43
+ if (prompt.length === 0) {
44
+ ctx.ui.notify(`Usage: /${COMMAND_NAME} <prompt>`, "error");
45
+ return;
46
+ }
47
+
48
+ options.notifyProgress("Resolving Graphite trunk…");
49
+ await ctx.waitForIdle();
50
+ const branch = await createTrackedBranchFromTrunkForPrompt({
51
+ pi,
52
+ cwd: ctx.cwd,
53
+ prompt,
54
+ notify: options.notifyProgress,
55
+ });
56
+ if ("error" in branch) {
57
+ ctx.ui.notify(branch.error, "error");
58
+ return;
59
+ }
60
+
61
+ await dispatchTrackedBranchPrompt({
62
+ pi,
63
+ ctx,
64
+ branch,
65
+ content: buildLaunchPrompt(prompt, TRUNK_DISPATCH_CONTEXT_NOTE),
66
+ description: `dispatch-from-trunk from ${branch.parentBranch}`,
67
+ payloadOptions,
68
+ ...optionalEntry("slotClient", options.slotClient),
69
+ notifyProgress: options.notifyProgress,
70
+ });
71
+ }
72
+
73
+ export async function createTrackedBranchFromTrunkForPrompt(options: {
74
+ pi: Pick<ExtensionAPI, "exec">;
75
+ cwd: string;
76
+ prompt: string;
77
+ notify?: (message: string) => void;
78
+ }): Promise<BranchCreateResult | { error: string }> {
79
+ const { pi, cwd, prompt, notify } = options;
80
+ notify?.("Resolving Graphite trunk…");
81
+ const trunk = await runGraphiteCommand(execApiToCommandRunner(piExecApiToCommandExecApi(pi)), {
82
+ cwd,
83
+ args: ["trunk", "--no-interactive"],
84
+ });
85
+ if (!commandSucceeded(trunk)) {
86
+ return {
87
+ error: formatCommandFailure(
88
+ "Could not resolve Graphite trunk.",
89
+ "gt trunk --no-interactive",
90
+ trunk,
91
+ ),
92
+ };
93
+ }
94
+ const trunkBranch = firstNonEmptyLine(trunk.stdout);
95
+ if (trunkBranch === undefined) {
96
+ return { error: "gt trunk --no-interactive returned no branch." };
97
+ }
98
+
99
+ notify?.("Refreshing Graphite trunk…");
100
+ const refresh = await refreshLocalTrunkBranch({ pi, cwd, trunkBranch });
101
+ if (!refresh.ok) {
102
+ return {
103
+ error: [
104
+ `Graphite trunk refresh failed for ${trunkBranch}; no branch was created.`,
105
+ refresh.message,
106
+ ].join("\n"),
107
+ };
108
+ }
109
+
110
+ const startPoint = await runText(pi, cwd, "git", ["rev-parse", trunkBranch]);
111
+ if (!startPoint.ok) {
112
+ return { error: `Could not resolve refreshed trunk ${trunkBranch}: ${startPoint.message}` };
113
+ }
114
+
115
+ notify?.("Generating branch name…");
116
+ return createTrackedBranchFromResolvedParent({
117
+ pi,
118
+ cwd,
119
+ prompt,
120
+ parentBranch: trunkBranch,
121
+ startPoint: startPoint.text,
122
+ startRef: trunkBranch,
123
+ createFailureContext: `from refreshed trunk ${trunkBranch}`,
124
+ });
125
+ }
126
+
127
+ async function refreshLocalTrunkBranch(options: {
128
+ pi: Pick<ExtensionAPI, "exec">;
129
+ cwd: string;
130
+ trunkBranch: string;
131
+ }): Promise<{ ok: true } | { ok: false; message: string }> {
132
+ const { pi, cwd, trunkBranch } = options;
133
+ const worktrees = await pi.exec("git", ["worktree", "list", "--porcelain"], {
134
+ cwd,
135
+ timeout: GIT_TRUNK_REFRESH_TIMEOUT_MS,
136
+ });
137
+ if (!commandSucceeded(worktrees)) {
138
+ return {
139
+ ok: false,
140
+ message: formatCommandFailure(
141
+ "Could not inspect Git worktrees.",
142
+ "git worktree list --porcelain",
143
+ worktrees,
144
+ ),
145
+ };
146
+ }
147
+
148
+ const plan = planLocalBranchRefreshFromWorktrees({
149
+ branch: trunkBranch,
150
+ cwd,
151
+ worktreePorcelain: worktrees.stdout,
152
+ });
153
+ const refresh = await pi.exec("git", plan.args, {
154
+ cwd: plan.cwd,
155
+ timeout: GIT_TRUNK_REFRESH_TIMEOUT_MS,
156
+ });
157
+ if (commandSucceeded(refresh)) return { ok: true };
158
+ return {
159
+ ok: false,
160
+ message: [
161
+ formatCommandFailure(
162
+ formatTrunkRefreshFailureTitle(plan, trunkBranch),
163
+ formatCommand("git", plan.args),
164
+ refresh,
165
+ ),
166
+ `Cwd: ${plan.cwd}`,
167
+ ].join("\n"),
168
+ };
169
+ }
170
+
171
+ function formatTrunkRefreshFailureTitle(plan: LocalBranchRefreshPlan, trunkBranch: string): string {
172
+ if (plan.type === "pull-checked-out-branch") {
173
+ return `Could not pull checked-out trunk branch ${trunkBranch}.`;
174
+ }
175
+
176
+ return `Could not fetch trunk branch ${trunkBranch}.`;
177
+ }