@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 +48 -0
- package/src/api/handlers.ts +75 -0
- package/src/api/index.ts +11 -0
- package/src/cmux/branch-slug.ts +174 -0
- package/src/cmux/claude-plan-tab.ts +124 -0
- package/src/cmux/command-surfaces.ts +51 -0
- package/src/cmux/dispatch-from-trunk.ts +177 -0
- package/src/cmux/dispatch-prompt.ts +459 -0
- package/src/cmux/objective-sidebar.ts +373 -0
- package/src/cmux/prompt-file.ts +34 -0
- package/src/cmux/sidebar.ts +386 -0
- package/src/cmux/slot-checkout.ts +46 -0
- package/src/cmux/slot-dispatch-plan.ts +560 -0
- package/src/cmux/slot-open-branch.ts +241 -0
- package/src/cmux/slot.ts +126 -0
- package/src/cmux/workspace-summary.ts +222 -0
- package/src/cmux/worktree-description.ts +87 -0
- package/src/ns/autobranch/checkpoint.ts +35 -0
- package/src/ns/autobranch/flow.ts +18 -0
- package/src/ns/autoslot-presentation.ts +33 -0
- package/src/ns/autoslot.ts +2 -0
- package/src/ns/cli-command-io.ts +23 -0
- package/src/ns/cli.ts +186 -0
- package/src/ns/land.ts +12 -0
- package/src/ns/trunk-pull.ts +2 -0
- package/src/pi/claude-plan-tab.ts +32 -0
- package/src/pi/command-backed-skills.ts +7 -0
- package/src/pi/dispatch-from-trunk.ts +41 -0
- package/src/pi/dispatch-prompt.ts +41 -0
- package/src/pi/extension.ts +23 -0
- package/src/pi/index.ts +11 -0
- package/src/pi/sidebar.ts +58 -0
- package/src/pi/slot-dispatch-plan.ts +70 -0
- package/src/pi/slot-open-branch.ts +39 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { basename, isAbsolute, posix, relative, resolve, sep } from "node:path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ExecResult,
|
|
5
|
+
formatCommand,
|
|
6
|
+
formatOutputSection,
|
|
7
|
+
tailText,
|
|
8
|
+
} from "@nseng-ai/foundation/command";
|
|
9
|
+
import {
|
|
10
|
+
parseMachineEnvelopeData,
|
|
11
|
+
type MachineEnvelopeDataParseValid,
|
|
12
|
+
} from "@nseng-ai/foundation/machine-envelope";
|
|
13
|
+
import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
|
|
14
|
+
import type { ExtensionAPI } from "@nseng-ai/capability-kit/cmux/types";
|
|
15
|
+
|
|
16
|
+
const OBJECTIVE_READ_TIMEOUT_MS = 30_000;
|
|
17
|
+
export const CMUX_WORKSPACE_SUMMARY_TIMEOUT_MS = 30_000;
|
|
18
|
+
const MAX_ERROR_CHARS = 4_000;
|
|
19
|
+
const MAX_ERROR_LINES = 20;
|
|
20
|
+
const ACTIVE_OBJECTIVE_PREFIX = ".ns/objectives/";
|
|
21
|
+
const ACTIVE_OBJECTIVE_ROOT = ".ns/objectives";
|
|
22
|
+
const ARCHIVE_OBJECTIVE_ROOT = ".ns/objective-archive";
|
|
23
|
+
|
|
24
|
+
export type ObjectiveSelectorParseResult =
|
|
25
|
+
| {
|
|
26
|
+
type: "valid";
|
|
27
|
+
slug: string;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: "invalid";
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ObjectiveSidebarFormatInput {
|
|
35
|
+
objectiveSlug: string;
|
|
36
|
+
slotSlug: string;
|
|
37
|
+
branchSlug: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SidebarFields {
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ObjectiveSidebarValidationResult =
|
|
46
|
+
| {
|
|
47
|
+
type: "validated";
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
type: "failed";
|
|
51
|
+
message: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ObjectiveSidebarApplyResult =
|
|
55
|
+
| {
|
|
56
|
+
type: "applied";
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
type: "failed";
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type BranchSlugReadResult =
|
|
64
|
+
| {
|
|
65
|
+
type: "loaded";
|
|
66
|
+
branchSlug: string;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
type: "failed";
|
|
70
|
+
message: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function resolveObjectiveSelector(
|
|
74
|
+
selector: string,
|
|
75
|
+
cwd: string,
|
|
76
|
+
): ObjectiveSelectorParseResult {
|
|
77
|
+
const trimmed = selector.trim();
|
|
78
|
+
if (trimmed.length === 0) {
|
|
79
|
+
return invalidSelector("Pass an Objective slug or .ns/objectives/<slug> path.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (trimmed.includes("\\")) {
|
|
83
|
+
return invalidSelector(
|
|
84
|
+
"Objective selector must be a slug or a .ns/objectives/<slug> path using forward slashes.",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isAbsolute(trimmed)) {
|
|
89
|
+
return resolveAbsoluteObjectiveSelector(trimmed, cwd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!trimmed.includes("/")) {
|
|
93
|
+
return validSlugSelector(trimmed);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return resolveRepoRelativeObjectiveSelector(trimmed);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function validateObjectiveSidebarSlug(
|
|
100
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
101
|
+
cwd: string,
|
|
102
|
+
slug: string,
|
|
103
|
+
): Promise<ObjectiveSidebarValidationResult> {
|
|
104
|
+
const parsed = await runJsonExecCommand({
|
|
105
|
+
pi,
|
|
106
|
+
cwd,
|
|
107
|
+
command: "ns",
|
|
108
|
+
args: ["objective", "exec", "read-objective", slug, "--format", "json"],
|
|
109
|
+
timeoutMs: OBJECTIVE_READ_TIMEOUT_MS,
|
|
110
|
+
summary: "Could not read Objective.",
|
|
111
|
+
label: "objective read JSON",
|
|
112
|
+
});
|
|
113
|
+
if (parsed.type === "failed") return parsed;
|
|
114
|
+
|
|
115
|
+
return parseObjectiveSidebarValidation(parsed.data, slug);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function readCurrentBranchSlug(
|
|
119
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
120
|
+
cwd: string,
|
|
121
|
+
): Promise<BranchSlugReadResult> {
|
|
122
|
+
const args = ["branch", "--show-current"];
|
|
123
|
+
let result: ExecResult;
|
|
124
|
+
try {
|
|
125
|
+
result = await pi.exec("git", args, { cwd, timeout: OBJECTIVE_READ_TIMEOUT_MS });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
type: "failed",
|
|
129
|
+
message: formatStartupFailure(
|
|
130
|
+
"Could not read current branch for cmux Objective sidebar.",
|
|
131
|
+
"git",
|
|
132
|
+
args,
|
|
133
|
+
error,
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const commandDisplay = formatCommand("git", args);
|
|
139
|
+
if (result.killed || result.code !== 0) {
|
|
140
|
+
return {
|
|
141
|
+
type: "failed",
|
|
142
|
+
message: formatExecFailure(
|
|
143
|
+
"Could not read current branch for cmux Objective sidebar.",
|
|
144
|
+
commandDisplay,
|
|
145
|
+
result,
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const branchSlug = result.stdout.trim();
|
|
151
|
+
if (branchSlug.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
type: "failed",
|
|
154
|
+
message:
|
|
155
|
+
"Could not read current branch for cmux Objective sidebar: detached HEAD or blank branch name.",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { type: "loaded", branchSlug };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function slotSlugFromCwd(cwd: string): string {
|
|
163
|
+
return basename(resolve(cwd));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function formatObjectiveSidebarFields(input: ObjectiveSidebarFormatInput): SidebarFields {
|
|
167
|
+
return {
|
|
168
|
+
title: `obj:${input.objectiveSlug}`,
|
|
169
|
+
description: `${input.slotSlug}::${input.branchSlug}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function applyObjectiveSidebarFields(
|
|
174
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
175
|
+
cwd: string,
|
|
176
|
+
fields: SidebarFields,
|
|
177
|
+
): Promise<ObjectiveSidebarApplyResult> {
|
|
178
|
+
const parsed = await runJsonExecCommand({
|
|
179
|
+
pi,
|
|
180
|
+
cwd,
|
|
181
|
+
command: "ccc",
|
|
182
|
+
args: [
|
|
183
|
+
"exec",
|
|
184
|
+
"cmux-workspace-summary",
|
|
185
|
+
"--title",
|
|
186
|
+
fields.title,
|
|
187
|
+
"--description",
|
|
188
|
+
fields.description,
|
|
189
|
+
"--format",
|
|
190
|
+
"json",
|
|
191
|
+
],
|
|
192
|
+
timeoutMs: CMUX_WORKSPACE_SUMMARY_TIMEOUT_MS,
|
|
193
|
+
summary: "Could not apply cmux Objective sidebar.",
|
|
194
|
+
label: "cmux workspace summary JSON",
|
|
195
|
+
});
|
|
196
|
+
if (parsed.type === "failed") return parsed;
|
|
197
|
+
|
|
198
|
+
if (parsed.data.success !== true) {
|
|
199
|
+
return {
|
|
200
|
+
type: "failed",
|
|
201
|
+
message: "Invalid cmux workspace summary JSON: expected data.success true.",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { type: "applied" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runJsonExecCommand(options: {
|
|
209
|
+
pi: Pick<ExtensionAPI, "exec">;
|
|
210
|
+
cwd: string;
|
|
211
|
+
command: string;
|
|
212
|
+
args: string[];
|
|
213
|
+
timeoutMs: number;
|
|
214
|
+
summary: string;
|
|
215
|
+
label: string;
|
|
216
|
+
}): Promise<MachineEnvelopeDataParseValid | { type: "failed"; message: string }> {
|
|
217
|
+
let result: ExecResult;
|
|
218
|
+
try {
|
|
219
|
+
result = await options.pi.exec(options.command, options.args, {
|
|
220
|
+
cwd: options.cwd,
|
|
221
|
+
timeout: options.timeoutMs,
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return {
|
|
225
|
+
type: "failed",
|
|
226
|
+
message: formatStartupFailure(options.summary, options.command, options.args, error),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const commandDisplay = formatCommand(options.command, options.args);
|
|
231
|
+
if (result.killed || result.code !== 0) {
|
|
232
|
+
return {
|
|
233
|
+
type: "failed",
|
|
234
|
+
message: formatFailedEnvelopeOrExecFailure(
|
|
235
|
+
options.summary,
|
|
236
|
+
commandDisplay,
|
|
237
|
+
result,
|
|
238
|
+
options.label,
|
|
239
|
+
),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const parsed = parseMachineEnvelopeData(result.stdout, {
|
|
244
|
+
label: options.label,
|
|
245
|
+
stdoutTail: { maxChars: MAX_ERROR_CHARS, maxLines: MAX_ERROR_LINES },
|
|
246
|
+
});
|
|
247
|
+
if (parsed.type !== "valid") return { type: "failed", message: parsed.message };
|
|
248
|
+
|
|
249
|
+
return parsed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveAbsoluteObjectiveSelector(
|
|
253
|
+
selector: string,
|
|
254
|
+
cwd: string,
|
|
255
|
+
): ObjectiveSelectorParseResult {
|
|
256
|
+
const normalizedSelector = resolve(selector);
|
|
257
|
+
const activeRoot = resolve(cwd, ACTIVE_OBJECTIVE_ROOT);
|
|
258
|
+
const relativePath = relative(activeRoot, normalizedSelector);
|
|
259
|
+
if (relativePath.length === 0) {
|
|
260
|
+
return invalidSelector("Pass an Objective slug or path below .ns/objectives/<slug>.");
|
|
261
|
+
}
|
|
262
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
263
|
+
return invalidSelector(
|
|
264
|
+
"Objective path must be inside the current repo's .ns/objectives directory.",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const slug = relativePath.split(sep)[0];
|
|
269
|
+
return validSlugSelector(slug ?? "");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function resolveRepoRelativeObjectiveSelector(selector: string): ObjectiveSelectorParseResult {
|
|
273
|
+
const normalized = posix.normalize(selector);
|
|
274
|
+
if (
|
|
275
|
+
normalized === ARCHIVE_OBJECTIVE_ROOT ||
|
|
276
|
+
normalized.startsWith(`${ARCHIVE_OBJECTIVE_ROOT}/`)
|
|
277
|
+
) {
|
|
278
|
+
return invalidSelector(
|
|
279
|
+
"Archived Objective paths are not supported; pass an active .ns/objectives/<slug> path.",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (normalized === ACTIVE_OBJECTIVE_ROOT) {
|
|
283
|
+
return invalidSelector("Pass an Objective slug or path below .ns/objectives/<slug>.");
|
|
284
|
+
}
|
|
285
|
+
if (!normalized.startsWith(ACTIVE_OBJECTIVE_PREFIX)) {
|
|
286
|
+
return invalidSelector("Pass an Objective slug or .ns/objectives/<slug> path.");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const relativePath = normalized.slice(ACTIVE_OBJECTIVE_PREFIX.length);
|
|
290
|
+
const slug = relativePath.split("/")[0] ?? "";
|
|
291
|
+
return validSlugSelector(slug);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function validSlugSelector(slug: string): ObjectiveSelectorParseResult {
|
|
295
|
+
if (!isValidObjectiveSlug(slug)) {
|
|
296
|
+
return invalidSelector(
|
|
297
|
+
"Objective selector must be a single slug, not '.', '..', or a nested path.",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return { type: "valid", slug };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function invalidSelector(message: string): ObjectiveSelectorParseResult {
|
|
304
|
+
return { type: "invalid", message };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isValidObjectiveSlug(slug: string): boolean {
|
|
308
|
+
return (
|
|
309
|
+
slug.length > 0 && slug !== "." && slug !== ".." && !slug.includes("/") && !slug.includes("\\")
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseObjectiveSidebarValidation(
|
|
314
|
+
data: Record<string, unknown>,
|
|
315
|
+
expectedSlug: string,
|
|
316
|
+
): ObjectiveSidebarValidationResult {
|
|
317
|
+
if (data.status !== "ok" || data.slug !== expectedSlug) {
|
|
318
|
+
return {
|
|
319
|
+
type: "failed",
|
|
320
|
+
message: "Invalid objective read JSON: expected status ok and matching slug.",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { type: "validated" };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function formatStartupFailure(
|
|
328
|
+
summary: string,
|
|
329
|
+
command: string,
|
|
330
|
+
args: readonly string[],
|
|
331
|
+
error: unknown,
|
|
332
|
+
): string {
|
|
333
|
+
return tailText(
|
|
334
|
+
`${summary}\nCommand: ${formatCommand(command, args)}\nError: ${formatErrorMessage(error)}`,
|
|
335
|
+
{ maxChars: MAX_ERROR_CHARS, maxLines: MAX_ERROR_LINES },
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function formatFailedEnvelopeOrExecFailure(
|
|
340
|
+
summary: string,
|
|
341
|
+
commandDisplay: string,
|
|
342
|
+
result: ExecResult,
|
|
343
|
+
label: string,
|
|
344
|
+
): string {
|
|
345
|
+
if (result.stdout.trim().length > 0) {
|
|
346
|
+
const parsed = parseMachineEnvelopeData(result.stdout, {
|
|
347
|
+
label,
|
|
348
|
+
stdoutTail: { maxChars: MAX_ERROR_CHARS, maxLines: MAX_ERROR_LINES },
|
|
349
|
+
});
|
|
350
|
+
if (parsed.type !== "valid") {
|
|
351
|
+
return `${summary}\nCommand: ${commandDisplay}\n${parsed.message}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return formatExecFailure(summary, commandDisplay, result);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function formatExecFailure(summary: string, commandDisplay: string, result: ExecResult): string {
|
|
358
|
+
const lines = [
|
|
359
|
+
summary,
|
|
360
|
+
`Command: ${commandDisplay}`,
|
|
361
|
+
`Exit code: ${result.code}`,
|
|
362
|
+
`Killed: ${result.killed ? "yes" : "no"}`,
|
|
363
|
+
formatOutputSection("stdout", result.stdout, {
|
|
364
|
+
maxChars: MAX_ERROR_CHARS,
|
|
365
|
+
maxLines: MAX_ERROR_LINES,
|
|
366
|
+
}),
|
|
367
|
+
formatOutputSection("stderr", result.stderr, {
|
|
368
|
+
maxChars: MAX_ERROR_CHARS,
|
|
369
|
+
maxLines: MAX_ERROR_LINES,
|
|
370
|
+
}),
|
|
371
|
+
];
|
|
372
|
+
return tailText(lines.join("\n"), { maxChars: MAX_ERROR_CHARS, maxLines: MAX_ERROR_LINES });
|
|
373
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface PromptFileOptions {
|
|
5
|
+
promptDir?: string;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ResolvedPromptFileOptions {
|
|
10
|
+
promptDir: string;
|
|
11
|
+
now: () => number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolvePromptFileOptions(
|
|
15
|
+
options: PromptFileOptions,
|
|
16
|
+
defaultPromptDir: string,
|
|
17
|
+
): ResolvedPromptFileOptions {
|
|
18
|
+
return {
|
|
19
|
+
promptDir: options.promptDir ?? defaultPromptDir,
|
|
20
|
+
now: options.now ?? Date.now,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function writeTimestampedPromptFile(options: {
|
|
25
|
+
promptDir: string;
|
|
26
|
+
now: () => number;
|
|
27
|
+
stem: string;
|
|
28
|
+
content: string;
|
|
29
|
+
}): Promise<string> {
|
|
30
|
+
await mkdir(options.promptDir, { recursive: true });
|
|
31
|
+
const path = join(options.promptDir, `${options.now()}-${options.stem}.md`);
|
|
32
|
+
await writeFile(path, options.content, "utf8");
|
|
33
|
+
return path;
|
|
34
|
+
}
|