svamp-cli 0.2.225 → 0.2.227
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/dist/agentCommands-7sqKMVIC.mjs +526 -0
- package/dist/auth-BfDZbHl6.mjs +83 -0
- package/dist/caddy-CuTbE3NY.mjs +322 -0
- package/dist/cli.mjs +2338 -0
- package/dist/commands-CU8JDM2d.mjs +428 -0
- package/dist/commands-CWP-J9Cf.mjs +610 -0
- package/dist/commands-CnlQyDxb.mjs +2762 -0
- package/dist/commands-D2iQ2v7_.mjs +196 -0
- package/dist/commands-DA_Nx893.mjs +265 -0
- package/dist/commands-H9h_Mtc4.mjs +493 -0
- package/dist/commands-YML8Rs_R.mjs +643 -0
- package/dist/fleet-B_78L11T.mjs +356 -0
- package/dist/frpc-BQyt6j5A.mjs +681 -0
- package/dist/headlessCli-CYfN9XvR.mjs +333 -0
- package/dist/httpServer-B1KVQJfm.mjs +276 -0
- package/dist/index.mjs +22 -0
- package/dist/package-VYP3g593.mjs +63 -0
- package/dist/pinnedClaudeCode-VIupR1NK.mjs +58 -0
- package/dist/rpc-DMj8ZJPz.mjs +92 -0
- package/dist/rpc-a0fp9HhD.mjs +194 -0
- package/dist/run-DkjwFIFH.mjs +1086 -0
- package/dist/run-LEPH8aka.mjs +16075 -0
- package/dist/runStore-CtptN7US.mjs +168 -0
- package/dist/sandboxDetect-DNTcbgWD.mjs +12 -0
- package/dist/scheduler-Bz80Ipep.mjs +102 -0
- package/dist/serveCommands-u_GoFaBX.mjs +310 -0
- package/dist/serveManager-9vhexH1L.mjs +781 -0
- package/dist/serviceManager-hlOVxkhW.mjs +78 -0
- package/dist/sideband-RJijk2GE.mjs +80 -0
- package/dist/store-BTs0H_y0.mjs +148 -0
- package/dist/supervisorLock-DmfzJx7B.mjs +159 -0
- package/package.json +1 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { connectAndGetMachine, resolveSessionId, createWorktree, connectAndResolveSession } from './commands-CnlQyDxb.mjs';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { u as updateIssue, q as addComment, t as addIssue, A as shortId } from './run-LEPH8aka.mjs';
|
|
6
|
+
import 'node:os';
|
|
7
|
+
import 'os';
|
|
8
|
+
import 'fs/promises';
|
|
9
|
+
import 'fs';
|
|
10
|
+
import 'path';
|
|
11
|
+
import 'url';
|
|
12
|
+
import 'child_process';
|
|
13
|
+
import 'crypto';
|
|
14
|
+
import 'node:crypto';
|
|
15
|
+
import 'util';
|
|
16
|
+
import 'node:events';
|
|
17
|
+
import '@agentclientprotocol/sdk';
|
|
18
|
+
import '@modelcontextprotocol/sdk/client/index.js';
|
|
19
|
+
import '@modelcontextprotocol/sdk/client/stdio.js';
|
|
20
|
+
import '@modelcontextprotocol/sdk/types.js';
|
|
21
|
+
import 'zod';
|
|
22
|
+
import 'node:fs/promises';
|
|
23
|
+
import 'node:util';
|
|
24
|
+
|
|
25
|
+
function git(cwd, args) {
|
|
26
|
+
return execSync(`git ${args}`, { cwd, stdio: "pipe", encoding: "utf-8" }).toString();
|
|
27
|
+
}
|
|
28
|
+
function worktreeStatus(worktreePath) {
|
|
29
|
+
const raw = git(worktreePath, "status --porcelain").trim();
|
|
30
|
+
if (!raw) return "";
|
|
31
|
+
return raw.split("\n").filter((line) => {
|
|
32
|
+
const p = line.slice(3);
|
|
33
|
+
return !p.startsWith(".svamp/") && !p.startsWith(".dev/");
|
|
34
|
+
}).join("\n").trim();
|
|
35
|
+
}
|
|
36
|
+
function aheadBehind(worktreePath, baseBranch) {
|
|
37
|
+
try {
|
|
38
|
+
const out = git(worktreePath, `rev-list --left-right --count ${baseBranch}...HEAD`).trim();
|
|
39
|
+
const [behind, ahead] = out.split(/\s+/).map((n) => parseInt(n, 10) || 0);
|
|
40
|
+
return { ahead, behind };
|
|
41
|
+
} catch {
|
|
42
|
+
return { ahead: 0, behind: 0 };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function currentBranch(worktreePath) {
|
|
46
|
+
try {
|
|
47
|
+
return git(worktreePath, "branch --show-current").trim();
|
|
48
|
+
} catch {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function mergeBack(input) {
|
|
53
|
+
const baseBranch = input.baseBranch || "main";
|
|
54
|
+
const { projectRoot, branch, worktreePath } = input;
|
|
55
|
+
if (!existsSync(projectRoot)) {
|
|
56
|
+
return { ok: false, stage: "verify", detail: `project root not found: ${projectRoot}` };
|
|
57
|
+
}
|
|
58
|
+
const worktreeGone = !existsSync(worktreePath);
|
|
59
|
+
if (!worktreeGone) {
|
|
60
|
+
let childStatus;
|
|
61
|
+
try {
|
|
62
|
+
childStatus = worktreeStatus(worktreePath);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { ok: false, stage: "verify", detail: `not a git worktree: ${worktreePath} (${e.message})` };
|
|
65
|
+
}
|
|
66
|
+
if (childStatus) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
stage: "verify",
|
|
70
|
+
rework: true,
|
|
71
|
+
detail: `feature worktree has uncommitted changes \u2014 commit (or run \`feature done\`) first:
|
|
72
|
+
${childStatus}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const leadBranch = currentBranch(projectRoot);
|
|
77
|
+
if (leadBranch !== baseBranch) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
stage: "verify",
|
|
81
|
+
detail: `lead tree ${projectRoot} is on '${leadBranch || "detached"}', not base '${baseBranch}'. Check out ${baseBranch} there first.`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
let leadStatus;
|
|
85
|
+
try {
|
|
86
|
+
leadStatus = git(projectRoot, "status --porcelain -uno").trim();
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return { ok: false, stage: "verify", detail: `lead tree status failed: ${e.message}` };
|
|
89
|
+
}
|
|
90
|
+
if (leadStatus) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
stage: "verify",
|
|
94
|
+
detail: `lead tree ${projectRoot} has uncommitted tracked changes; commit/stash before merging.`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
git(projectRoot, `merge --no-ff ${branch} -m "crew: merge ${branch} into ${baseBranch}"`);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const detail = (e.stdout?.toString() || "") + (e.stderr?.toString() || "") || e.message;
|
|
101
|
+
try {
|
|
102
|
+
git(projectRoot, "merge --abort");
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, stage: "merge", rework: true, detail: `merge conflict \u2014 aborted:
|
|
106
|
+
${detail.trim()}` };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
git(projectRoot, `worktree remove --force ${worktreePath}`);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
try {
|
|
112
|
+
git(projectRoot, "worktree prune");
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
let stillRegistered = false;
|
|
116
|
+
try {
|
|
117
|
+
stillRegistered = git(projectRoot, "worktree list --porcelain").includes(worktreePath);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
if (stillRegistered) {
|
|
121
|
+
const detail = e.stderr?.toString() || "" || e.message;
|
|
122
|
+
return { ok: false, stage: "remove-worktree", detail: `merged OK but worktree removal failed: ${detail.trim()}` };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (input.deleteBranch !== false) {
|
|
126
|
+
try {
|
|
127
|
+
git(projectRoot, `branch -d ${branch}`);
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, stage: "done", detail: `merged ${branch} into ${baseBranch}` };
|
|
132
|
+
}
|
|
133
|
+
function projectRootFromWorktree(worktreePath) {
|
|
134
|
+
const marker = "/.dev/worktree/";
|
|
135
|
+
const idx = worktreePath.indexOf(marker);
|
|
136
|
+
if (idx === -1) return null;
|
|
137
|
+
return worktreePath.slice(0, idx);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isStandaloneChild(crew) {
|
|
141
|
+
return !!crew && crew.standalone === true;
|
|
142
|
+
}
|
|
143
|
+
function worktreeCrewMeta(feature, branch, worktreePath, baseBranch) {
|
|
144
|
+
return { role: "feature", feature, branch, worktreePath, baseBranch };
|
|
145
|
+
}
|
|
146
|
+
function standaloneCrewMeta(feature, directory) {
|
|
147
|
+
return { role: "feature", feature, standalone: true, directory };
|
|
148
|
+
}
|
|
149
|
+
function childDirectory(crew, sessionDir) {
|
|
150
|
+
return crew?.worktreePath || crew?.directory || sessionDir || "";
|
|
151
|
+
}
|
|
152
|
+
function standaloneBrief(directory, leadShort, brief) {
|
|
153
|
+
return `You are a crew STANDALONE child working directly in folder \`${directory}\` (no git worktree, no feature branch), reporting to lead \`${leadShort}\`.
|
|
154
|
+
|
|
155
|
+
Feature brief:
|
|
156
|
+
${brief}
|
|
157
|
+
|
|
158
|
+
Work in this folder. Commit there yourself if it is a git repo. Report milestones with \`svamp feature report\`, and when done run \`svamp feature done\` \u2014 there is no branch to merge, so your lead just detaches & closes you.`;
|
|
159
|
+
}
|
|
160
|
+
function worktreeBrief(branch, base, leadShort, brief) {
|
|
161
|
+
return `You are a crew feature child on branch \`${branch}\` (base \`${base}\`), reporting to lead \`${leadShort}\`.
|
|
162
|
+
|
|
163
|
+
Feature brief:
|
|
164
|
+
${brief}
|
|
165
|
+
|
|
166
|
+
Work in this worktree. Keep tests green, report milestones with \`svamp feature report\`, and when done run \`svamp feature done\` so your lead can review & merge. After merge you'll be closed.`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function requireLead(explicit) {
|
|
170
|
+
const id = explicit || process.env.SVAMP_SESSION_ID;
|
|
171
|
+
if (!id) {
|
|
172
|
+
console.error("No lead session. Run inside a Svamp session, or pass --lead <id>.");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
return id;
|
|
176
|
+
}
|
|
177
|
+
async function rpc(machine, id, method, kwargs = {}) {
|
|
178
|
+
return machine.sessionRPC(id, method, kwargs);
|
|
179
|
+
}
|
|
180
|
+
async function getMeta(machine, id) {
|
|
181
|
+
const r = await rpc(machine, id, "getMetadata");
|
|
182
|
+
return r?.metadata ?? r ?? {};
|
|
183
|
+
}
|
|
184
|
+
async function inbox(machine, fromId, toId, body, subject) {
|
|
185
|
+
await rpc(machine, toId, "sendInboxMessage", {
|
|
186
|
+
message: {
|
|
187
|
+
messageId: shortId(),
|
|
188
|
+
body,
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
read: false,
|
|
191
|
+
from: `agent:${fromId}`,
|
|
192
|
+
fromSession: fromId,
|
|
193
|
+
to: toId,
|
|
194
|
+
subject,
|
|
195
|
+
urgency: "normal"
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function featureStart(brief, opts = {}) {
|
|
200
|
+
if (!brief?.trim()) {
|
|
201
|
+
console.error('A feature brief is required: svamp feature start "<brief>"');
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const leadId = requireLead(opts.leadId);
|
|
205
|
+
const { server, machine } = await connectAndGetMachine(opts.machineId);
|
|
206
|
+
try {
|
|
207
|
+
const sessions = await machine.listSessions();
|
|
208
|
+
const lead = resolveSessionId(sessions, leadId);
|
|
209
|
+
const standalone = !!opts.noWorktree;
|
|
210
|
+
let childDir;
|
|
211
|
+
let base = "";
|
|
212
|
+
let wt = null;
|
|
213
|
+
if (standalone) {
|
|
214
|
+
const dir = opts.directory;
|
|
215
|
+
if (!dir) {
|
|
216
|
+
console.error("--no-worktree requires --dir <folder> (the existing folder to run the child in).");
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
childDir = resolve(dir);
|
|
220
|
+
if (!existsSync(childDir)) {
|
|
221
|
+
console.error(`Folder not found: ${childDir}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
console.log(`Standalone child folder: ${childDir} (no worktree)`);
|
|
225
|
+
} else {
|
|
226
|
+
const projectRoot2 = opts.directory || lead.directory;
|
|
227
|
+
if (!projectRoot2) {
|
|
228
|
+
console.error("Could not determine the lead project directory.");
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
base = opts.baseBranch || currentBranch(projectRoot2) || "main";
|
|
232
|
+
try {
|
|
233
|
+
wt = createWorktree(projectRoot2);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error(`Failed to create worktree: ${e.message}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
childDir = wt.path;
|
|
239
|
+
console.log(`Worktree: ${wt.branch} \u2192 ${wt.path}`);
|
|
240
|
+
}
|
|
241
|
+
const projectRoot = lead.directory || opts.directory || childDir;
|
|
242
|
+
const result = await machine.spawnSession({
|
|
243
|
+
directory: childDir,
|
|
244
|
+
agent: "claude",
|
|
245
|
+
parentSessionId: lead.sessionId,
|
|
246
|
+
permissionMode: "bypassPermissions"
|
|
247
|
+
});
|
|
248
|
+
if (result.type !== "success" || !result.sessionId) {
|
|
249
|
+
console.error(`Failed to spawn feature child: ${result.errorMessage || result.type}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
const childId = result.sessionId;
|
|
253
|
+
try {
|
|
254
|
+
const delegatedWhere = standalone ? `standalone folder \`${childDir}\`` : `\`${wt.branch}\` (base \`${base}\`)`;
|
|
255
|
+
let linkedId = opts.issueId;
|
|
256
|
+
if (linkedId) {
|
|
257
|
+
const u = updateIssue(projectRoot, linkedId, { disposition: "crew", session: childId, owner: leadId, ...standalone ? {} : { branch: wt.branch }, status: "in_progress" });
|
|
258
|
+
if (!u) {
|
|
259
|
+
console.warn(`Note: issue ${linkedId} not found \u2014 creating a new crew issue instead.`);
|
|
260
|
+
linkedId = void 0;
|
|
261
|
+
} else {
|
|
262
|
+
addComment(projectRoot, u.id, `Delegated to crew child ${childId.slice(0, 8)} on ${delegatedWhere}.`);
|
|
263
|
+
linkedId = u.id;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!linkedId) {
|
|
267
|
+
const created = addIssue(projectRoot, {
|
|
268
|
+
title: brief.trim().split("\n")[0].slice(0, 80),
|
|
269
|
+
status: "in_progress",
|
|
270
|
+
disposition: "crew",
|
|
271
|
+
triaged: true,
|
|
272
|
+
session: childId,
|
|
273
|
+
owner: leadId,
|
|
274
|
+
scope: "project",
|
|
275
|
+
// owner=lead (#0099): visible to the lead, not unrelated sessions
|
|
276
|
+
...opts.oracle?.trim() ? { verify: { type: "command", text: opts.oracle.trim() } } : {},
|
|
277
|
+
body: brief.trim()
|
|
278
|
+
});
|
|
279
|
+
if (!standalone) updateIssue(projectRoot, created.id, { branch: wt.branch });
|
|
280
|
+
linkedId = created.id;
|
|
281
|
+
}
|
|
282
|
+
console.log(` issue: #${linkedId} (crew, child ${childId.slice(0, 8)})`);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.warn(`Note: could not link a crew issue (${e.message}).`);
|
|
285
|
+
}
|
|
286
|
+
if (!lead.metadata?.crew) {
|
|
287
|
+
try {
|
|
288
|
+
await rpc(machine, lead.sessionId, "updateConfig", { patch: { crew: { role: "lead" } } });
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const oracle = opts.oracle?.trim();
|
|
293
|
+
const patch = {
|
|
294
|
+
crew: standalone ? standaloneCrewMeta(brief.trim(), childDir) : worktreeCrewMeta(brief.trim(), wt.branch, wt.path, base)
|
|
295
|
+
};
|
|
296
|
+
let judges = [];
|
|
297
|
+
if (oracle) {
|
|
298
|
+
judges = [{ type: "oracle", cmd: oracle, on_fail: "escalate" }, { type: "parent", parent: lead.sessionId }];
|
|
299
|
+
patch.loop = {
|
|
300
|
+
until: brief.trim(),
|
|
301
|
+
oracle,
|
|
302
|
+
parent: lead.sessionId,
|
|
303
|
+
max_iterations: opts.maxRounds ?? 20
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
await rpc(machine, childId, "updateConfig", { patch });
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.warn(`Note: could not write crew/loop config yet (${e.message}).`);
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
await rpc(machine, childId, "sendMessage", {
|
|
313
|
+
content: JSON.stringify({
|
|
314
|
+
role: "user",
|
|
315
|
+
content: { type: "text", text: standalone ? standaloneBrief(childDir, lead.sessionId.slice(0, 8), brief.trim()) : worktreeBrief(wt.branch, base, lead.sessionId.slice(0, 8), brief.trim()) },
|
|
316
|
+
meta: { sentFrom: "svamp-cli", crew: "feature-brief" }
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.warn(`Note: could not send the brief (${e.message}).`);
|
|
321
|
+
}
|
|
322
|
+
console.log(`Feature child started: ${childId}`);
|
|
323
|
+
console.log(` brief: ${brief.trim().slice(0, 80)}${brief.trim().length > 80 ? "\u2026" : ""}`);
|
|
324
|
+
if (standalone) {
|
|
325
|
+
console.log(` folder: ${childDir} (standalone \u2014 no branch; done detaches & closes)`);
|
|
326
|
+
} else {
|
|
327
|
+
console.log(` branch: ${wt.branch} (merges back to ${base})`);
|
|
328
|
+
}
|
|
329
|
+
console.log(judges.length ? ` gate: ${judges.map((j) => j.type).join(" \u2192 ")} (oracle auto-approves \u2192 lead ${standalone ? "closes" : "merges"})` : ` gate: none \u2014 child runs \`feature done\` when complete; lead ${standalone ? "detaches & closes" : "reviews & merges"}`);
|
|
330
|
+
} finally {
|
|
331
|
+
await server.disconnect();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function featureList(opts = {}) {
|
|
335
|
+
const leadId = requireLead(opts.leadId);
|
|
336
|
+
const { server, machine } = await connectAndGetMachine(opts.machineId);
|
|
337
|
+
try {
|
|
338
|
+
const sessions = await machine.listSessions();
|
|
339
|
+
const lead = resolveSessionId(sessions, leadId);
|
|
340
|
+
const children = sessions.filter((s) => s.metadata?.parentSessionId === lead.sessionId);
|
|
341
|
+
const rows = children.map((c) => {
|
|
342
|
+
const crew = c.metadata?.crew || {};
|
|
343
|
+
const standalone = isStandaloneChild(crew);
|
|
344
|
+
const dir = childDirectory(crew, c.directory);
|
|
345
|
+
const base = crew.baseBranch || "main";
|
|
346
|
+
let branch = "", ahead = 0, behind = 0, dirty = false, gone = false;
|
|
347
|
+
if (standalone) {
|
|
348
|
+
gone = !(dir && existsSync(dir));
|
|
349
|
+
} else {
|
|
350
|
+
branch = crew.branch || (dir && existsSync(dir) ? currentBranch(dir) : "") || "";
|
|
351
|
+
if (dir && existsSync(dir)) {
|
|
352
|
+
const ab = aheadBehind(dir, base);
|
|
353
|
+
ahead = ab.ahead;
|
|
354
|
+
behind = ab.behind;
|
|
355
|
+
try {
|
|
356
|
+
dirty = worktreeStatus(dir) !== "";
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
} else if (crew.worktreePath) {
|
|
360
|
+
gone = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
id: c.sessionId,
|
|
365
|
+
title: c.metadata?.summary?.text || c.metadata?.name || "",
|
|
366
|
+
role: crew.role || (crew.worktreePath ? "feature" : ""),
|
|
367
|
+
standalone,
|
|
368
|
+
dir,
|
|
369
|
+
branch,
|
|
370
|
+
base,
|
|
371
|
+
ahead,
|
|
372
|
+
behind,
|
|
373
|
+
dirty,
|
|
374
|
+
gone,
|
|
375
|
+
active: c.active,
|
|
376
|
+
feature: crew.feature || ""
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
if (opts.json) {
|
|
380
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (rows.length === 0) {
|
|
384
|
+
console.log(`No feature children for lead ${lead.sessionId.slice(0, 8)}.`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
console.log(`Features under lead ${lead.sessionId.slice(0, 8)}:
|
|
388
|
+
`);
|
|
389
|
+
for (const r of rows) {
|
|
390
|
+
const state = r.standalone ? r.gone ? "folder-gone" : r.active ? "standalone" : "standalone\xB7idle" : r.gone ? "worktree-gone" : r.active ? "active" : "idle";
|
|
391
|
+
const ab = r.standalone || r.gone ? "" : `+${r.ahead}/-${r.behind}${r.dirty ? " \u2717dirty" : ""}`;
|
|
392
|
+
const ref = r.standalone ? r.dir || "\u2014" : r.branch || "\u2014";
|
|
393
|
+
const title = (r.feature || r.title || "").slice(0, 48);
|
|
394
|
+
console.log(` ${r.id.slice(0, 8)} ${state.padEnd(15)} ${ref.padEnd(28)} ${ab.padEnd(14)} ${title}`);
|
|
395
|
+
}
|
|
396
|
+
console.log(`
|
|
397
|
+
Review a finished feature with: svamp feature merge <id>`);
|
|
398
|
+
} finally {
|
|
399
|
+
await server.disconnect();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function featureReport(text, opts = {}) {
|
|
403
|
+
if (!text?.trim()) {
|
|
404
|
+
console.error("A report message is required.");
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const selfId = requireLead(opts.selfId);
|
|
408
|
+
const { server, machine } = await connectAndGetMachine(opts.machineId);
|
|
409
|
+
try {
|
|
410
|
+
const meta = await getMeta(machine, selfId);
|
|
411
|
+
const parent = meta?.parentSessionId;
|
|
412
|
+
if (!parent) {
|
|
413
|
+
console.error("This session has no parent (lead) to report to.");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
const kind = opts.blocker ? "blocker" : "progress";
|
|
417
|
+
await inbox(machine, selfId, parent, `<feature-${kind} from="${selfId}">
|
|
418
|
+
${text.trim()}
|
|
419
|
+
</feature-${kind}>`, `feature ${kind}`);
|
|
420
|
+
console.log(`Reported ${kind} to lead ${parent.slice(0, 8)}.`);
|
|
421
|
+
} finally {
|
|
422
|
+
await server.disconnect();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function featureDone(opts = {}) {
|
|
426
|
+
const selfId = requireLead(opts.selfId);
|
|
427
|
+
const { server, machine } = await connectAndGetMachine(opts.machineId);
|
|
428
|
+
try {
|
|
429
|
+
const meta = await getMeta(machine, selfId);
|
|
430
|
+
const parent = meta?.parentSessionId;
|
|
431
|
+
if (!parent) {
|
|
432
|
+
console.error("This session has no parent (lead) to report to.");
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const crew = meta?.crew || {};
|
|
436
|
+
if (isStandaloneChild(crew)) {
|
|
437
|
+
const dir2 = childDirectory(crew, meta?.path);
|
|
438
|
+
const body2 = [
|
|
439
|
+
`<done-request child="${selfId}" standalone="true" folder="${dir2}">`,
|
|
440
|
+
crew.feature ? `Feature: ${crew.feature}` : "",
|
|
441
|
+
opts.summary ? `Summary: ${opts.summary.trim()}` : "",
|
|
442
|
+
`No branch to merge (standalone folder). Detach & close with: svamp feature merge ${selfId.slice(0, 8)}`,
|
|
443
|
+
`</done-request>`
|
|
444
|
+
].filter(Boolean).join("\n");
|
|
445
|
+
await inbox(machine, selfId, parent, body2, `done-request (standalone)`);
|
|
446
|
+
console.log(`Sent done-request to lead ${parent.slice(0, 8)} (standalone folder ${dir2 || "?"}; no branch to merge).`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const dir = meta?.path || crew.worktreePath || "";
|
|
450
|
+
const base = crew.baseBranch || "main";
|
|
451
|
+
const branch = crew.branch || (dir && existsSync(dir) ? currentBranch(dir) : "") || "";
|
|
452
|
+
const ab = dir && existsSync(dir) ? aheadBehind(dir, base) : { ahead: 0, behind: 0 };
|
|
453
|
+
let dirty = false;
|
|
454
|
+
try {
|
|
455
|
+
dirty = dir && existsSync(dir) ? worktreeStatus(dir) !== "" : false;
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
const body = [
|
|
459
|
+
`<merge-request child="${selfId}" branch="${branch}" base="${base}" commits="${ab.ahead}"${dirty ? ' dirty="true"' : ""}>`,
|
|
460
|
+
crew.feature ? `Feature: ${crew.feature}` : "",
|
|
461
|
+
opts.summary ? `Summary: ${opts.summary.trim()}` : "",
|
|
462
|
+
dirty ? "WARNING: worktree has uncommitted changes \u2014 commit before this can merge." : "",
|
|
463
|
+
`Review & merge with: svamp feature merge ${selfId.slice(0, 8)}`,
|
|
464
|
+
`</merge-request>`
|
|
465
|
+
].filter(Boolean).join("\n");
|
|
466
|
+
await inbox(machine, selfId, parent, body, `merge-request (${branch || "feature"})`);
|
|
467
|
+
console.log(`Sent merge-request to lead ${parent.slice(0, 8)} (branch ${branch || "?"}, +${ab.ahead} commits).`);
|
|
468
|
+
if (dirty) console.log(" \u26A0 Worktree is dirty \u2014 commit your work or the merge will be rejected.");
|
|
469
|
+
} finally {
|
|
470
|
+
await server.disconnect();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async function featureMerge(childPartial, opts = {}) {
|
|
474
|
+
const leadId = opts.leadId || process.env.SVAMP_SESSION_ID || "";
|
|
475
|
+
const { server, machine, fullId: childId } = await connectAndResolveSession(childPartial, opts.machineId);
|
|
476
|
+
try {
|
|
477
|
+
const meta = await getMeta(machine, childId);
|
|
478
|
+
const crew = meta?.crew || {};
|
|
479
|
+
if (isStandaloneChild(crew)) {
|
|
480
|
+
const dir = childDirectory(crew, meta?.path);
|
|
481
|
+
if (!opts.keepChild) {
|
|
482
|
+
try {
|
|
483
|
+
await machine.archiveSession(childId);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
console.warn(`Could not archive standalone child: ${e.message}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
console.log(`\u2705 Detached standalone child ${childId.slice(0, 8)} (folder ${dir || "?"}; no branch to merge) \u2014 ${opts.keepChild ? "kept" : "archived"}.`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const worktreePath = crew.worktreePath || meta?.path || "";
|
|
492
|
+
if (!worktreePath) {
|
|
493
|
+
console.error("Could not determine the child worktree path.");
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
const base = crew.baseBranch || "main";
|
|
497
|
+
const branch = crew.branch || currentBranch(worktreePath) || "";
|
|
498
|
+
if (!branch) {
|
|
499
|
+
console.error("Could not determine the feature branch.");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
const projectRoot = projectRootFromWorktree(worktreePath) || (leadId ? (await getMeta(machine, leadId).catch(() => ({})))?.path : null) || "";
|
|
503
|
+
if (!projectRoot) {
|
|
504
|
+
console.error("Could not determine the lead project root (base worktree).");
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
if (leadId) {
|
|
508
|
+
try {
|
|
509
|
+
await inbox(
|
|
510
|
+
machine,
|
|
511
|
+
leadId,
|
|
512
|
+
childId,
|
|
513
|
+
`<crew-freeze>Your lead is merging branch ${branch} into ${base}. Stop editing files now; do not commit until told.</crew-freeze>`,
|
|
514
|
+
"crew: freeze for merge"
|
|
515
|
+
);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const r = mergeBack({ projectRoot, branch, worktreePath, baseBranch: base, deleteBranch: opts.deleteBranch });
|
|
520
|
+
if (!r.ok) {
|
|
521
|
+
if (r.rework && leadId) {
|
|
522
|
+
try {
|
|
523
|
+
await inbox(
|
|
524
|
+
machine,
|
|
525
|
+
leadId,
|
|
526
|
+
childId,
|
|
527
|
+
`<crew-rework branch="${branch}">
|
|
528
|
+
Merge was not applied: ${r.detail}
|
|
529
|
+
Resolve and run \`svamp feature done\` again.
|
|
530
|
+
</crew-rework>`,
|
|
531
|
+
"crew: rework needed"
|
|
532
|
+
);
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
console.error(`Merge deferred (rework) at ${r.stage}: ${r.detail}`);
|
|
536
|
+
console.error(`Child ${childId.slice(0, 8)} re-woken with guidance; left running.`);
|
|
537
|
+
} else {
|
|
538
|
+
console.error(`Merge failed at ${r.stage}: ${r.detail}`);
|
|
539
|
+
}
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
if (!opts.keepChild) {
|
|
543
|
+
try {
|
|
544
|
+
await machine.archiveSession(childId);
|
|
545
|
+
} catch (e) {
|
|
546
|
+
console.warn(`Merged, but could not archive child: ${e.message}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
console.log(`\u2705 Merged ${branch} \u2192 ${base}; worktree removed; child ${childId.slice(0, 8)} ${opts.keepChild ? "kept" : "archived"}.`);
|
|
550
|
+
} finally {
|
|
551
|
+
await server.disconnect();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function flag(args, ...names) {
|
|
555
|
+
for (const n of names) {
|
|
556
|
+
const i = args.indexOf(n);
|
|
557
|
+
if (i !== -1 && i + 1 < args.length) return args[i + 1];
|
|
558
|
+
}
|
|
559
|
+
return void 0;
|
|
560
|
+
}
|
|
561
|
+
function has(args, ...names) {
|
|
562
|
+
return names.some((n) => args.includes(n));
|
|
563
|
+
}
|
|
564
|
+
function positionals(args, valueFlags) {
|
|
565
|
+
const out = [];
|
|
566
|
+
for (let i = 0; i < args.length; i++) {
|
|
567
|
+
const a = args[i];
|
|
568
|
+
if (a.startsWith("-")) {
|
|
569
|
+
if (valueFlags.includes(a)) i++;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
out.push(a);
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
const FEATURE_HELP = `Usage: svamp feature <command>
|
|
577
|
+
|
|
578
|
+
start "<brief>" [--oracle "<cmd>"] [--base <branch>] [--action nudge|block]
|
|
579
|
+
[--max N] [--dir <path>] [--no-worktree] [--lead <id>] [-m <machine>]
|
|
580
|
+
Spawn a managed child of the lead + attach a loop gate (oracle\u2192parent).
|
|
581
|
+
Default: a git worktree + branch off the lead's project root (merges back).
|
|
582
|
+
--no-worktree --dir <folder>: run the child directly IN <folder> (a subdir or
|
|
583
|
+
sibling project) with NO worktree/branch \u2014 done/merge just detach & close it.
|
|
584
|
+
list [--json] [--lead <id>] [-m <machine>]
|
|
585
|
+
The lead's feature children: branch/folder, ahead/behind, status.
|
|
586
|
+
report "<text>" [--blocker] [-m <machine>] (run by a child) progress/blocker \u2192 lead
|
|
587
|
+
done [--summary "<text>"] [-m <machine>] (run by a child) merge-request (worktree) or done-request (standalone) \u2192 lead
|
|
588
|
+
merge <child-id> [--keep-child] [--no-delete-branch] [-m <machine>]
|
|
589
|
+
(run by the lead) worktree: verify \u2192 merge \u2192 remove worktree \u2192 archive child.
|
|
590
|
+
standalone: just detach & archive the child (no branch to merge).`;
|
|
591
|
+
async function crewCommand(args) {
|
|
592
|
+
const sub = args[0];
|
|
593
|
+
const rest = args.slice(1);
|
|
594
|
+
const machineId = flag(rest, "-m", "--machine");
|
|
595
|
+
const lead = flag(rest, "--lead");
|
|
596
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
597
|
+
console.log(FEATURE_HELP);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (sub === "start") {
|
|
601
|
+
const valueFlags = ["--oracle", "--base", "--action", "--max", "--dir", "-d", "--lead", "-m", "--machine", "--issue"];
|
|
602
|
+
const brief = positionals(rest, valueFlags).join(" ");
|
|
603
|
+
const maxStr = flag(rest, "--max");
|
|
604
|
+
const action = flag(rest, "--action");
|
|
605
|
+
await featureStart(brief, {
|
|
606
|
+
leadId: lead,
|
|
607
|
+
machineId,
|
|
608
|
+
directory: flag(rest, "--dir", "-d"),
|
|
609
|
+
noWorktree: has(rest, "--no-worktree", "--standalone"),
|
|
610
|
+
oracle: flag(rest, "--oracle"),
|
|
611
|
+
baseBranch: flag(rest, "--base"),
|
|
612
|
+
issueId: flag(rest, "--issue"),
|
|
613
|
+
action: action === "block" ? "block" : action === "nudge" ? "nudge" : void 0,
|
|
614
|
+
maxRounds: maxStr ? parseInt(maxStr, 10) : void 0
|
|
615
|
+
});
|
|
616
|
+
} else if (sub === "list" || sub === "ls") {
|
|
617
|
+
await featureList({ leadId: lead, machineId, json: has(rest, "--json") });
|
|
618
|
+
} else if (sub === "report") {
|
|
619
|
+
const text = positionals(rest, ["-m", "--machine", "--lead"]).join(" ");
|
|
620
|
+
await featureReport(text, { machineId, blocker: has(rest, "--blocker") });
|
|
621
|
+
} else if (sub === "done") {
|
|
622
|
+
await featureDone({ machineId, summary: flag(rest, "--summary") });
|
|
623
|
+
} else if (sub === "merge") {
|
|
624
|
+
const child = positionals(rest, ["-m", "--machine", "--lead"])[0];
|
|
625
|
+
if (!child) {
|
|
626
|
+
console.error("Usage: svamp feature merge <child-id>");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
await featureMerge(child, {
|
|
630
|
+
leadId: lead,
|
|
631
|
+
machineId,
|
|
632
|
+
keepChild: has(rest, "--keep-child"),
|
|
633
|
+
deleteBranch: has(rest, "--no-delete-branch") ? false : void 0
|
|
634
|
+
});
|
|
635
|
+
} else {
|
|
636
|
+
console.error(`Unknown feature command: ${sub}
|
|
637
|
+
`);
|
|
638
|
+
console.log(FEATURE_HELP);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export { crewCommand, featureDone, featureList, featureMerge, featureReport, featureStart };
|