@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.
@@ -0,0 +1,459 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import {
6
+ checkBrmemEntry,
7
+ putBrmemEntryFromFile,
8
+ type BrmemCommandErrorInfo,
9
+ type BrmemPutData,
10
+ } from "@nseng-ai/capability-kit/brmem-cli";
11
+ import {
12
+ commandSucceeded,
13
+ execApiToCommandRunner,
14
+ formatCommand,
15
+ formatCommandFailure,
16
+ formatShellArg,
17
+ piExecApiToCommandExecApi,
18
+ } from "@nseng-ai/foundation/command";
19
+ import { formatErrorMessage, optionalEntry } from "@nseng-ai/foundation/primitives";
20
+ import { runGraphiteCommand } from "@nseng-ai/capability-kit/graphite/branch";
21
+ import {
22
+ generateBranchSlug,
23
+ MAX_BRANCH_SLUG_LENGTH,
24
+ sanitizeBranchName,
25
+ trimBranchSlugToLength,
26
+ } from "./branch-slug.ts";
27
+ import { getPiLaunchOptions, type PiLaunchOptions } from "@nseng-ai/capability-kit/cmux/pi-launch";
28
+ import type { TextResult } from "@nseng-ai/foundation/primitives";
29
+ import { CCC_WORKSPACE_DISPATCH_PROMPT_COMMAND_NAME } from "./command-surfaces.ts";
30
+ import { openBranchInCmuxSlot } from "./slot.ts";
31
+ import { createCccSlotClient } from "./slot-checkout.ts";
32
+ import type { SlotCheckoutTarget, SlotClient } from "@nseng-ai/slots/api";
33
+ import type { CommandContext, ExtensionAPI } from "@nseng-ai/capability-kit/cmux/types";
34
+
35
+ const COMMAND_NAME = CCC_WORKSPACE_DISPATCH_PROMPT_COMMAND_NAME;
36
+ const DISPATCH_PROMPT_NAMESPACE = "ccc-dispatch";
37
+ const DISPATCH_PROMPT_KEY = "prompt.md";
38
+
39
+ export interface BranchCreateResult {
40
+ branchName: string;
41
+ parentBranch: string;
42
+ startPoint: string;
43
+ }
44
+
45
+ export interface DispatchPromptPayloadOptions {
46
+ stagingDir?: string;
47
+ now?: () => number;
48
+ shouldCleanupStagingFile?: boolean;
49
+ slotClient?: SlotClient;
50
+ }
51
+
52
+ interface ResolvedDispatchPromptPayloadOptions {
53
+ stagingDir?: string;
54
+ now: () => number;
55
+ shouldCleanupStagingFile: boolean;
56
+ }
57
+
58
+ export interface HandleCccSlotDispatchPromptOptions {
59
+ pi: Pick<ExtensionAPI, "exec" | "getThinkingLevel">;
60
+ payloadOptions: ResolvedDispatchPromptPayloadOptions;
61
+ slotClient?: SlotClient;
62
+ args: string;
63
+ ctx: CommandContext;
64
+ notifyProgress: (message: string) => void;
65
+ }
66
+
67
+ type StoredDispatchPromptPayload = BrmemPutData;
68
+
69
+ // Keep this workflow-local: branch-context's gateway error type is coupled to its namespace policy.
70
+ type BrmemErrorInfo = BrmemCommandErrorInfo;
71
+
72
+ export type DispatchPromptStorageResult =
73
+ | { ok: true; value: StoredDispatchPromptPayload }
74
+ | { ok: false; error: BrmemErrorInfo };
75
+
76
+ type DispatchPromptPresenceResult =
77
+ | { type: "present"; displayCommand: string }
78
+ | { type: "absent" }
79
+ | { type: "error"; error: BrmemErrorInfo };
80
+
81
+ interface StagedPayloadFile {
82
+ filePath: string;
83
+ cleanup(): Promise<void>;
84
+ }
85
+
86
+ export async function handleCccSlotDispatchPrompt(
87
+ options: HandleCccSlotDispatchPromptOptions,
88
+ ): Promise<void> {
89
+ const { pi, payloadOptions, args, ctx } = options;
90
+ const prompt = args.trim();
91
+ if (prompt.length === 0) {
92
+ ctx.ui.notify(`Usage: /${COMMAND_NAME} <prompt>`, "error");
93
+ return;
94
+ }
95
+
96
+ options.notifyProgress("Generating branch name…");
97
+ await ctx.waitForIdle();
98
+
99
+ const branch = await createTrackedBranchForPrompt(pi, ctx.cwd, prompt);
100
+ if ("error" in branch) {
101
+ ctx.ui.notify(branch.error, "error");
102
+ return;
103
+ }
104
+
105
+ await dispatchTrackedBranchPrompt({
106
+ pi,
107
+ ctx,
108
+ branch,
109
+ content: buildLaunchPrompt(prompt),
110
+ description: `dispatch-prompt from ${branch.parentBranch}`,
111
+ payloadOptions,
112
+ ...optionalEntry("slotClient", options.slotClient),
113
+ notifyProgress: options.notifyProgress,
114
+ });
115
+ }
116
+
117
+ export async function dispatchTrackedBranchPrompt(options: {
118
+ pi: Pick<ExtensionAPI, "exec" | "getThinkingLevel">;
119
+ ctx: CommandContext;
120
+ branch: BranchCreateResult;
121
+ content: string;
122
+ description: string;
123
+ payloadOptions: ResolvedDispatchPromptPayloadOptions;
124
+ slotClient?: SlotClient;
125
+ notifyProgress: (message: string) => void;
126
+ }): Promise<void> {
127
+ const { pi, ctx, branch } = options;
128
+ options.notifyProgress("Storing dispatch prompt in Branch Memory…");
129
+ const stored = await storeDispatchPromptPayload({
130
+ pi,
131
+ cwd: ctx.cwd,
132
+ branchName: branch.branchName,
133
+ content: options.content,
134
+ payloadOptions: options.payloadOptions,
135
+ });
136
+ if (!stored.ok) {
137
+ ctx.ui.notify(formatDispatchPromptStorageFailure(branch.branchName, stored.error), "error");
138
+ return;
139
+ }
140
+
141
+ const launchOptions = getPiLaunchOptions(pi, ctx);
142
+ await openBranchInCmuxSlot({
143
+ pi,
144
+ cwd: ctx.cwd,
145
+ branchName: branch.branchName,
146
+ command: buildBrmemPayloadPiLaunchCommand(branch.branchName, launchOptions),
147
+ description: options.description,
148
+ slotClient: options.slotClient ?? createCccSlotClient({ cwd: ctx.cwd }),
149
+ notify: (message, level) => ctx.ui.notify(message, level),
150
+ successMessage: (target) => formatDispatchPromptSuccessMessage(target, branch, stored.value),
151
+ });
152
+ }
153
+
154
+ function formatDispatchPromptSuccessMessage(
155
+ target: SlotCheckoutTarget,
156
+ branch: BranchCreateResult,
157
+ stored: StoredDispatchPromptPayload,
158
+ ): string {
159
+ return [
160
+ `Opened cmux workspace: ${target.branchName}`,
161
+ `Parent: ${branch.parentBranch}`,
162
+ `Start point: ${branch.startPoint}`,
163
+ `Dispatch payload: ${stored.namespace}/${stored.key}`,
164
+ `Entry Locator: ${stored.refName}`,
165
+ ].join("\n");
166
+ }
167
+
168
+ export async function createTrackedBranchForPrompt(
169
+ pi: Pick<ExtensionAPI, "exec">,
170
+ cwd: string,
171
+ prompt: string,
172
+ ): Promise<BranchCreateResult | { error: string }> {
173
+ const parent = await runText(pi, cwd, "git", ["symbolic-ref", "--short", "HEAD"]);
174
+ if (!parent.ok) {
175
+ return { error: `Could not resolve current branch: ${parent.message}` };
176
+ }
177
+
178
+ const startPoint = await runText(pi, cwd, "git", ["rev-parse", "HEAD"]);
179
+ if (!startPoint.ok) {
180
+ return { error: `Could not resolve HEAD: ${startPoint.message}` };
181
+ }
182
+
183
+ return createTrackedBranchFromResolvedParent({
184
+ pi,
185
+ cwd,
186
+ prompt,
187
+ parentBranch: parent.text,
188
+ startPoint: startPoint.text,
189
+ startRef: "HEAD",
190
+ });
191
+ }
192
+
193
+ export async function createTrackedBranchFromResolvedParent(options: {
194
+ pi: Pick<ExtensionAPI, "exec">;
195
+ cwd: string;
196
+ prompt: string;
197
+ parentBranch: string;
198
+ startPoint: string;
199
+ startRef: string;
200
+ createFailureContext?: string;
201
+ }): Promise<BranchCreateResult | { error: string }> {
202
+ const { pi, cwd, prompt, parentBranch, startPoint, startRef, createFailureContext } = options;
203
+ const slug = await generateBranchSlug(pi, cwd, { kind: "task", content: prompt });
204
+ if (!slug.ok) {
205
+ return { error: slug.message };
206
+ }
207
+
208
+ const branchName = await chooseAvailableBranchName(pi, cwd, slug.text);
209
+ const create = await runText(pi, cwd, "git", ["branch", branchName, startRef]);
210
+ if (!create.ok) {
211
+ const context = createFailureContext === undefined ? "" : ` ${createFailureContext}`;
212
+ return { error: `Failed to create branch ${branchName}${context}: ${create.message}` };
213
+ }
214
+
215
+ const trackArgs = ["track", branchName, "--parent", parentBranch, "--no-interactive"];
216
+ const track = await runGraphiteCommand(execApiToCommandRunner(piExecApiToCommandExecApi(pi)), {
217
+ cwd,
218
+ args: trackArgs,
219
+ });
220
+ if (!commandSucceeded(track)) {
221
+ return {
222
+ error: [
223
+ `Created git branch ${branchName}, but Graphite tracking failed:`,
224
+ formatCommandFailure("gt track failed", formatCommand("gt", trackArgs), track),
225
+ "The slot/capability-kit/cmux prompt session was not launched.",
226
+ ].join("\n"),
227
+ };
228
+ }
229
+
230
+ return {
231
+ branchName,
232
+ parentBranch,
233
+ startPoint,
234
+ };
235
+ }
236
+
237
+ async function chooseAvailableBranchName(
238
+ pi: Pick<ExtensionAPI, "exec">,
239
+ cwd: string,
240
+ baseName: string,
241
+ ): Promise<string> {
242
+ let candidate = baseName;
243
+ for (let suffix = 2; await branchExists(pi, cwd, candidate); suffix += 1) {
244
+ candidate = appendBranchSuffix(baseName, suffix);
245
+ }
246
+ return candidate;
247
+ }
248
+
249
+ async function branchExists(
250
+ pi: Pick<ExtensionAPI, "exec">,
251
+ cwd: string,
252
+ branchName: string,
253
+ ): Promise<boolean> {
254
+ const result = await pi.exec(
255
+ "git",
256
+ ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`],
257
+ {
258
+ cwd,
259
+ timeout: 5_000,
260
+ },
261
+ );
262
+ return result.code === 0;
263
+ }
264
+
265
+ function appendBranchSuffix(branchName: string, suffix: number): string {
266
+ const suffixText = `-${suffix}`;
267
+ const stem = trimBranchSlugToLength(branchName, MAX_BRANCH_SLUG_LENGTH - suffixText.length);
268
+ return `${stem}${suffixText}`;
269
+ }
270
+
271
+ export async function storeDispatchPromptPayload(options: {
272
+ pi: Pick<ExtensionAPI, "exec">;
273
+ cwd: string;
274
+ branchName: string;
275
+ content: string;
276
+ payloadOptions: ResolvedDispatchPromptPayloadOptions;
277
+ }): Promise<DispatchPromptStorageResult> {
278
+ const { pi, cwd, branchName, content, payloadOptions } = options;
279
+ const presence = await checkDispatchPromptPayload(pi, cwd, branchName);
280
+ switch (presence.type) {
281
+ case "present":
282
+ return {
283
+ ok: false,
284
+ error: {
285
+ code: "dispatch_prompt_collision",
286
+ message: `Branch Memory ${DISPATCH_PROMPT_NAMESPACE}/${DISPATCH_PROMPT_KEY} already exists on branch ${branchName}. Refusing to overwrite.`,
287
+ displayCommand: presence.displayCommand,
288
+ },
289
+ };
290
+ case "error":
291
+ return { ok: false, error: presence.error };
292
+ case "absent":
293
+ break;
294
+ }
295
+
296
+ let staged: StagedPayloadFile;
297
+ try {
298
+ staged = await stageDispatchPromptPayload(payloadOptions, branchName, content);
299
+ } catch (error) {
300
+ return {
301
+ ok: false,
302
+ error: {
303
+ code: "dispatch_prompt_stage_failed",
304
+ message: `Failed to stage dispatch prompt payload for Branch Memory: ${formatErrorMessage(error)}`,
305
+ },
306
+ };
307
+ }
308
+
309
+ try {
310
+ return await putDispatchPromptPayload({ pi, cwd, branchName, sourceFile: staged.filePath });
311
+ } finally {
312
+ try {
313
+ await staged.cleanup();
314
+ } catch {
315
+ // The payload has already been stored or reported as failed; cleanup failure should not change command outcome.
316
+ }
317
+ }
318
+ }
319
+
320
+ async function checkDispatchPromptPayload(
321
+ pi: Pick<ExtensionAPI, "exec">,
322
+ cwd: string,
323
+ branchName: string,
324
+ ): Promise<DispatchPromptPresenceResult> {
325
+ return checkBrmemEntry({
326
+ gateway: pi,
327
+ cwd,
328
+ namespace: DISPATCH_PROMPT_NAMESPACE,
329
+ key: DISPATCH_PROMPT_KEY,
330
+ branch: branchName,
331
+ });
332
+ }
333
+
334
+ async function putDispatchPromptPayload(options: {
335
+ pi: Pick<ExtensionAPI, "exec">;
336
+ cwd: string;
337
+ branchName: string;
338
+ sourceFile: string;
339
+ }): Promise<DispatchPromptStorageResult> {
340
+ return putBrmemEntryFromFile({
341
+ gateway: options.pi,
342
+ cwd: options.cwd,
343
+ namespace: DISPATCH_PROMPT_NAMESPACE,
344
+ key: DISPATCH_PROMPT_KEY,
345
+ branch: options.branchName,
346
+ sourceFile: options.sourceFile,
347
+ });
348
+ }
349
+
350
+ async function stageDispatchPromptPayload(
351
+ options: ResolvedDispatchPromptPayloadOptions,
352
+ branchName: string,
353
+ content: string,
354
+ ): Promise<StagedPayloadFile> {
355
+ const directory = options.stagingDir ?? (await mkdtemp(join(tmpdir(), "ccc-dispatch-prompt-")));
356
+ await mkdir(directory, { recursive: true });
357
+ const filePath = join(directory, `${options.now()}-${dispatchPromptFileStem(branchName)}.md`);
358
+ await writeFile(filePath, content, "utf8");
359
+
360
+ return {
361
+ filePath,
362
+ cleanup: async () => {
363
+ if (!options.shouldCleanupStagingFile) return;
364
+ if (options.stagingDir === undefined) {
365
+ await rm(directory, { recursive: true, force: true });
366
+ return;
367
+ }
368
+ await rm(filePath, { force: true });
369
+ },
370
+ };
371
+ }
372
+
373
+ function dispatchPromptFileStem(branchName: string): string {
374
+ return sanitizeBranchName(branchName)?.replace(/\//g, "-") ?? "prompt";
375
+ }
376
+
377
+ export function resolveDispatchPromptPayloadOptions(
378
+ options: DispatchPromptPayloadOptions,
379
+ ): ResolvedDispatchPromptPayloadOptions {
380
+ return {
381
+ ...(options.stagingDir === undefined ? {} : { stagingDir: options.stagingDir }),
382
+ now: options.now ?? Date.now,
383
+ shouldCleanupStagingFile: options.shouldCleanupStagingFile ?? true,
384
+ };
385
+ }
386
+
387
+ export function buildBrmemPayloadPiLaunchCommand(
388
+ branchName: string,
389
+ launchOptions: PiLaunchOptions,
390
+ ): string {
391
+ const getArgs = [
392
+ "get",
393
+ DISPATCH_PROMPT_KEY,
394
+ "--namespace",
395
+ DISPATCH_PROMPT_NAMESPACE,
396
+ "--branch",
397
+ branchName,
398
+ ];
399
+ const getCommand = formatCommand("brmem", getArgs);
400
+ const piArgs = ["pi"];
401
+ if (launchOptions.model !== undefined) {
402
+ piArgs.push("--provider", launchOptions.model.provider, "--model", launchOptions.model.id);
403
+ }
404
+ if (launchOptions.thinkingLevel !== "off") {
405
+ piArgs.push("--thinking", launchOptions.thinkingLevel);
406
+ }
407
+ const piCommand = `exec ${piArgs.map(formatShellArg).join(" ")} "$payload"`;
408
+ return `payload="$(${getCommand})" && ${piCommand}`;
409
+ }
410
+
411
+ export function formatDispatchPromptStorageFailure(
412
+ branchName: string,
413
+ error: BrmemErrorInfo,
414
+ ): string {
415
+ if (error.code === "dispatch_prompt_collision") {
416
+ return [
417
+ `Created Graphite-tracked branch ${branchName}, but dispatch prompt payload already exists at Branch Memory ${DISPATCH_PROMPT_NAMESPACE}/${DISPATCH_PROMPT_KEY} on that branch.`,
418
+ "Refusing to overwrite; no cmux workspace was opened.",
419
+ ].join("\n");
420
+ }
421
+ return [
422
+ `Created Graphite-tracked branch ${branchName}, but failed to store dispatch prompt payload in Branch Memory.`,
423
+ "No cmux workspace was opened.",
424
+ "",
425
+ error.message,
426
+ ].join("\n");
427
+ }
428
+
429
+ export function buildLaunchPrompt(prompt: string, contextNote?: string): string {
430
+ const lines = [
431
+ "## Completion instructions",
432
+ "After you finish the implementation:",
433
+ "1. Create or update the branch commit using the repo's normal workflow.",
434
+ "2. Then run `!ns flow submit`.",
435
+ "",
436
+ ];
437
+ if (contextNote !== undefined) {
438
+ lines.push("## Dispatch context", contextNote, "");
439
+ }
440
+ lines.push(prompt);
441
+ return lines.join("\n");
442
+ }
443
+
444
+ export async function runText(
445
+ pi: Pick<ExtensionAPI, "exec">,
446
+ cwd: string,
447
+ command: string,
448
+ args: string[],
449
+ ): Promise<TextResult> {
450
+ const result = await pi.exec(command, args, { cwd, timeout: 30_000 });
451
+ if (result.code === 0 && !result.killed) {
452
+ return { ok: true, text: result.stdout.trim() };
453
+ }
454
+ return {
455
+ ok: false,
456
+ message:
457
+ result.stderr.trim() || result.stdout.trim() || `${command} exited with ${result.code}`,
458
+ };
459
+ }