svamp-cli 0.2.116 → 0.2.118

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.
@@ -1,8 +1,8 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { execSync } from 'node:child_process';
3
- import { basename, resolve, join } from 'node:path';
3
+ import { basename, resolve, join, isAbsolute } from 'node:path';
4
4
  import os from 'node:os';
5
- import { G as normalizeAllowedUser, H as loadSecurityContextConfig, I as resolveSecurityContext, J as buildSecurityContextFromFlags, K as mergeSecurityContexts, c as connectToHypha, L as buildSessionShareUrl, M as computeOutboundHop, n as shortId, N as buildMachineShareUrl } from './run-DHPCWQUq.mjs';
5
+ import { G as normalizeAllowedUser, H as loadSecurityContextConfig, I as resolveSecurityContext, J as buildSecurityContextFromFlags, K as mergeSecurityContexts, c as connectToHypha, L as buildSessionShareUrl, M as computeOutboundHop, n as shortId, N as buildMachineShareUrl } from './run-9C2ogsuu.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
8
8
  import 'fs';
@@ -999,6 +999,22 @@ function generateWorktreeName() {
999
999
  const noun = WORKTREE_NOUNS[Math.floor(Math.random() * WORKTREE_NOUNS.length)];
1000
1000
  return `${adj}-${noun}`;
1001
1001
  }
1002
+ function ensureWorktreeExcludes(projectRoot) {
1003
+ try {
1004
+ const common = execSync("git rev-parse --git-common-dir", { cwd: projectRoot, stdio: "pipe" }).toString().trim();
1005
+ const commonAbs = isAbsolute(common) ? common : join(projectRoot, common);
1006
+ const excludeFile = join(commonAbs, "info", "exclude");
1007
+ const existing = existsSync(excludeFile) ? readFileSync(excludeFile, "utf-8") : "";
1008
+ const lines = existing.split("\n");
1009
+ const want = [".svamp/", ".dev/"];
1010
+ const missing = want.filter((w) => !lines.some((l) => l.trim() === w));
1011
+ if (missing.length) {
1012
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
1013
+ writeFileSync(excludeFile, existing + prefix + missing.join("\n") + "\n");
1014
+ }
1015
+ } catch {
1016
+ }
1017
+ }
1002
1018
  function createWorktree(baseDir) {
1003
1019
  const absBase = resolve(baseDir);
1004
1020
  const marker = "/.dev/worktree/";
@@ -1009,6 +1025,7 @@ function createWorktree(baseDir) {
1009
1025
  } catch {
1010
1026
  throw new Error(`Not a git repository: ${projectRoot}`);
1011
1027
  }
1028
+ ensureWorktreeExcludes(projectRoot);
1012
1029
  const name = generateWorktreeName();
1013
1030
  const relPath = `.dev/worktree/${name}`;
1014
1031
  try {
@@ -2360,6 +2377,44 @@ async function sessionLoopStatus(sessionIdPartial, machineId) {
2360
2377
  await server.disconnect();
2361
2378
  }
2362
2379
  }
2380
+ async function sessionSupervise(sessionIdPartial, criteria, machineId, opts) {
2381
+ const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
2382
+ try {
2383
+ const svc = getSessionProxy(machine, fullId);
2384
+ const judges = [];
2385
+ if (opts?.oracle) judges.push({ type: "oracle", cmd: opts.oracle, on_fail: opts?.parent ? "escalate" : "reject" });
2386
+ if (opts?.parent) judges.push({ type: "parent", parent: opts.parent });
2387
+ if (opts?.agent || judges.length === 0) judges.push({ type: "agent" });
2388
+ const maxRounds = opts?.maxRounds ?? 20;
2389
+ await svc.updateConfig({
2390
+ supervisor: {
2391
+ criteria,
2392
+ judges,
2393
+ action: opts?.action || "block",
2394
+ max_rounds: maxRounds
2395
+ }
2396
+ });
2397
+ const judgeLabel = judges.map((j) => j.type).join("\u2192");
2398
+ console.log(`\u{1F441} Supervisor attached to session ${fullId.slice(0, 8)}`);
2399
+ console.log(` Criteria: ${criteria.slice(0, 100)}${criteria.length > 100 ? "..." : ""}`);
2400
+ console.log(` Judges: ${judgeLabel}`);
2401
+ if (opts?.oracle) console.log(` Oracle: ${opts.oracle}`);
2402
+ if (opts?.parent) console.log(` Parent review: ${opts.parent.slice(0, 8)} (async)`);
2403
+ console.log(` Max rounds: ${maxRounds}`);
2404
+ } finally {
2405
+ await server.disconnect();
2406
+ }
2407
+ }
2408
+ async function sessionUnsupervise(sessionIdPartial, machineId) {
2409
+ const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
2410
+ try {
2411
+ const svc = getSessionProxy(machine, fullId);
2412
+ await svc.updateConfig({ supervisor: null });
2413
+ console.log(`Supervisor detached from session ${fullId.slice(0, 8)}`);
2414
+ } finally {
2415
+ await server.disconnect();
2416
+ }
2417
+ }
2363
2418
  async function sessionInboxSend(sessionIdPartial, body, machineId, opts) {
2364
2419
  const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
2365
2420
  try {
@@ -2500,4 +2555,4 @@ async function sessionInboxClear(sessionIdPartial, machineId, opts) {
2500
2555
  }
2501
2556
  }
2502
2557
 
2503
- export { collectAssistantResponse, connectAndGetMachine, connectAndResolveSession, createWorktree, generateWorktreeName, machineExec, machineInfo, machineLs, machineShare, parseShareArg, queryCore, renderMessage, resolveSessionId, sendCore, sessionApprove, sessionArchive, sessionAttach, sessionDelete, sessionDeny, sessionInboxClear, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxSend, sessionInfo, sessionList, sessionLoopCancel, sessionLoopStart, sessionLoopStatus, sessionMachines, sessionMessages, sessionQuery, sessionResume, sessionSend, sessionShare, sessionSpawn, sessionWait, sessionWhoami, snapshotLatestSeq, validateSendOptions, wiseAskCli };
2558
+ export { collectAssistantResponse, connectAndGetMachine, connectAndResolveSession, createWorktree, generateWorktreeName, machineExec, machineInfo, machineLs, machineShare, parseShareArg, queryCore, renderMessage, resolveSessionId, sendCore, sessionApprove, sessionArchive, sessionAttach, sessionDelete, sessionDeny, sessionInboxClear, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxSend, sessionInfo, sessionList, sessionLoopCancel, sessionLoopStart, sessionLoopStatus, sessionMachines, sessionMessages, sessionQuery, sessionResume, sessionSend, sessionShare, sessionSpawn, sessionSupervise, sessionUnsupervise, sessionWait, sessionWhoami, snapshotLatestSeq, validateSendOptions, wiseAskCli };
@@ -1,7 +1,7 @@
1
1
  import os from 'os';
2
2
  import fs__default from 'fs';
3
3
  import { resolve, join, relative } from 'path';
4
- import { p as parseFrontmatter, o as getSkillsServer, q as getSkillsWorkspaceName, t as getSkillsCollectionName, u as fetchWithTimeout, v as searchSkills, w as SKILLS_DIR, x as getSkillInfo, y as downloadSkillFile, z as listSkillFiles } from './run-DHPCWQUq.mjs';
4
+ import { p as parseFrontmatter, o as getSkillsServer, q as getSkillsWorkspaceName, t as getSkillsCollectionName, u as fetchWithTimeout, v as searchSkills, w as SKILLS_DIR, x as getSkillInfo, y as downloadSkillFile, z as listSkillFiles } from './run-9C2ogsuu.mjs';
5
5
  import 'fs/promises';
6
6
  import 'url';
7
7
  import 'child_process';
@@ -0,0 +1,513 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { connectAndGetMachine, resolveSessionId, createWorktree, connectAndResolveSession } from './commands-B5rek8XG.mjs';
3
+ import { execSync } from 'node:child_process';
4
+ import { n as shortId } from './run-9C2ogsuu.mjs';
5
+ import 'node:path';
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
+ if (!existsSync(worktreePath)) {
59
+ return { ok: false, stage: "verify", detail: `worktree not found: ${worktreePath}` };
60
+ }
61
+ let childStatus;
62
+ try {
63
+ childStatus = worktreeStatus(worktreePath);
64
+ } catch (e) {
65
+ return { ok: false, stage: "verify", detail: `not a git worktree: ${worktreePath} (${e.message})` };
66
+ }
67
+ if (childStatus) {
68
+ return {
69
+ ok: false,
70
+ stage: "verify",
71
+ rework: true,
72
+ detail: `feature worktree has uncommitted changes \u2014 commit (or run \`feature done\`) first:
73
+ ${childStatus}`
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
+ const detail = e.stderr?.toString() || "" || e.message;
112
+ return { ok: false, stage: "remove-worktree", detail: `merged OK but worktree removal failed: ${detail.trim()}` };
113
+ }
114
+ if (input.deleteBranch !== false) {
115
+ try {
116
+ git(projectRoot, `branch -d ${branch}`);
117
+ } catch {
118
+ }
119
+ }
120
+ return { ok: true, stage: "done", detail: `merged ${branch} into ${baseBranch}` };
121
+ }
122
+ function projectRootFromWorktree(worktreePath) {
123
+ const marker = "/.dev/worktree/";
124
+ const idx = worktreePath.indexOf(marker);
125
+ if (idx === -1) return null;
126
+ return worktreePath.slice(0, idx);
127
+ }
128
+
129
+ function requireLead(explicit) {
130
+ const id = explicit || process.env.SVAMP_SESSION_ID;
131
+ if (!id) {
132
+ console.error("No lead session. Run inside a Svamp session, or pass --lead <id>.");
133
+ process.exit(1);
134
+ }
135
+ return id;
136
+ }
137
+ async function rpc(machine, id, method, kwargs = {}) {
138
+ return machine.sessionRPC(id, method, kwargs);
139
+ }
140
+ async function getMeta(machine, id) {
141
+ const r = await rpc(machine, id, "getMetadata");
142
+ return r?.metadata ?? r ?? {};
143
+ }
144
+ async function inbox(machine, fromId, toId, body, subject) {
145
+ await rpc(machine, toId, "sendInboxMessage", {
146
+ message: {
147
+ messageId: shortId(),
148
+ body,
149
+ timestamp: Date.now(),
150
+ read: false,
151
+ from: `agent:${fromId}`,
152
+ fromSession: fromId,
153
+ to: toId,
154
+ subject,
155
+ urgency: "normal"
156
+ }
157
+ });
158
+ }
159
+ async function featureStart(brief, opts = {}) {
160
+ if (!brief?.trim()) {
161
+ console.error('A feature brief is required: svamp feature start "<brief>"');
162
+ process.exit(1);
163
+ }
164
+ const leadId = requireLead(opts.leadId);
165
+ const { server, machine } = await connectAndGetMachine(opts.machineId);
166
+ try {
167
+ const sessions = await machine.listSessions();
168
+ const lead = resolveSessionId(sessions, leadId);
169
+ const projectRoot = opts.directory || lead.directory;
170
+ if (!projectRoot) {
171
+ console.error("Could not determine the lead project directory.");
172
+ process.exit(1);
173
+ }
174
+ const base = opts.baseBranch || currentBranch(projectRoot) || "main";
175
+ let wt;
176
+ try {
177
+ wt = createWorktree(projectRoot);
178
+ } catch (e) {
179
+ console.error(`Failed to create worktree: ${e.message}`);
180
+ process.exit(1);
181
+ }
182
+ console.log(`Worktree: ${wt.branch} \u2192 ${wt.path}`);
183
+ const result = await machine.spawnSession({
184
+ directory: wt.path,
185
+ agent: "claude",
186
+ parentSessionId: lead.sessionId,
187
+ permissionMode: "bypassPermissions"
188
+ });
189
+ if (result.type !== "success" || !result.sessionId) {
190
+ console.error(`Failed to spawn feature child: ${result.errorMessage || result.type}`);
191
+ process.exit(1);
192
+ }
193
+ const childId = result.sessionId;
194
+ if (!lead.metadata?.crew) {
195
+ try {
196
+ await rpc(machine, lead.sessionId, "updateConfig", { patch: { crew: { role: "lead" } } });
197
+ } catch {
198
+ }
199
+ }
200
+ const oracle = opts.oracle?.trim();
201
+ const patch = {
202
+ crew: { role: "feature", feature: brief.trim(), branch: wt.branch, worktreePath: wt.path, baseBranch: base }
203
+ };
204
+ let judges = [];
205
+ if (oracle) {
206
+ judges = [{ type: "oracle", cmd: oracle, on_fail: "escalate" }, { type: "parent", parent: lead.sessionId }];
207
+ patch.supervisor = {
208
+ criteria: brief.trim(),
209
+ judges,
210
+ action: opts.action || "nudge",
211
+ max_rounds: opts.maxRounds ?? 20
212
+ };
213
+ }
214
+ try {
215
+ await rpc(machine, childId, "updateConfig", { patch });
216
+ } catch (e) {
217
+ console.warn(`Note: could not write crew/supervisor config yet (${e.message}).`);
218
+ }
219
+ try {
220
+ await rpc(machine, childId, "sendMessage", {
221
+ content: JSON.stringify({
222
+ role: "user",
223
+ content: { type: "text", text: `You are a crew feature child on branch \`${wt.branch}\` (base \`${base}\`), reporting to lead \`${lead.sessionId.slice(0, 8)}\`.
224
+
225
+ Feature brief:
226
+ ${brief.trim()}
227
+
228
+ 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.` },
229
+ meta: { sentFrom: "svamp-cli", crew: "feature-brief" }
230
+ })
231
+ });
232
+ } catch (e) {
233
+ console.warn(`Note: could not send the brief (${e.message}).`);
234
+ }
235
+ console.log(`Feature child started: ${childId}`);
236
+ console.log(` brief: ${brief.trim().slice(0, 80)}${brief.trim().length > 80 ? "\u2026" : ""}`);
237
+ console.log(` branch: ${wt.branch} (merges back to ${base})`);
238
+ console.log(judges.length ? ` gate: ${judges.map((j) => j.type).join(" \u2192 ")} (oracle auto-approves \u2192 lead merges)` : ` gate: none \u2014 child runs \`feature done\` when complete; lead reviews & merges`);
239
+ } finally {
240
+ await server.disconnect();
241
+ }
242
+ }
243
+ async function featureList(opts = {}) {
244
+ const leadId = requireLead(opts.leadId);
245
+ const { server, machine } = await connectAndGetMachine(opts.machineId);
246
+ try {
247
+ const sessions = await machine.listSessions();
248
+ const lead = resolveSessionId(sessions, leadId);
249
+ const children = sessions.filter((s) => s.metadata?.parentSessionId === lead.sessionId);
250
+ const rows = children.map((c) => {
251
+ const crew = c.metadata?.crew || {};
252
+ const dir = c.directory || crew.worktreePath || "";
253
+ const base = crew.baseBranch || "main";
254
+ const branch = crew.branch || (dir && existsSync(dir) ? currentBranch(dir) : "") || "";
255
+ let ahead = 0, behind = 0, dirty = false, gone = false;
256
+ if (dir && existsSync(dir)) {
257
+ const ab = aheadBehind(dir, base);
258
+ ahead = ab.ahead;
259
+ behind = ab.behind;
260
+ try {
261
+ dirty = worktreeStatus(dir) !== "";
262
+ } catch {
263
+ }
264
+ } else if (crew.worktreePath) {
265
+ gone = true;
266
+ }
267
+ return {
268
+ id: c.sessionId,
269
+ title: c.metadata?.summary?.text || c.metadata?.name || "",
270
+ role: crew.role || (crew.worktreePath ? "feature" : ""),
271
+ branch,
272
+ base,
273
+ ahead,
274
+ behind,
275
+ dirty,
276
+ gone,
277
+ active: c.active,
278
+ feature: crew.feature || ""
279
+ };
280
+ });
281
+ if (opts.json) {
282
+ console.log(JSON.stringify(rows, null, 2));
283
+ return;
284
+ }
285
+ if (rows.length === 0) {
286
+ console.log(`No feature children for lead ${lead.sessionId.slice(0, 8)}.`);
287
+ return;
288
+ }
289
+ console.log(`Features under lead ${lead.sessionId.slice(0, 8)}:
290
+ `);
291
+ for (const r of rows) {
292
+ const state = r.gone ? "worktree-gone" : r.active ? "active" : "idle";
293
+ const ab = r.gone ? "" : `+${r.ahead}/-${r.behind}${r.dirty ? " \u2717dirty" : ""}`;
294
+ const title = (r.feature || r.title || "").slice(0, 48);
295
+ console.log(` ${r.id.slice(0, 8)} ${state.padEnd(13)} ${(r.branch || "\u2014").padEnd(28)} ${ab.padEnd(14)} ${title}`);
296
+ }
297
+ console.log(`
298
+ Review a finished feature with: svamp feature merge <id>`);
299
+ } finally {
300
+ await server.disconnect();
301
+ }
302
+ }
303
+ async function featureReport(text, opts = {}) {
304
+ if (!text?.trim()) {
305
+ console.error("A report message is required.");
306
+ process.exit(1);
307
+ }
308
+ const selfId = requireLead(opts.selfId);
309
+ const { server, machine } = await connectAndGetMachine(opts.machineId);
310
+ try {
311
+ const meta = await getMeta(machine, selfId);
312
+ const parent = meta?.parentSessionId;
313
+ if (!parent) {
314
+ console.error("This session has no parent (lead) to report to.");
315
+ process.exit(1);
316
+ }
317
+ const kind = opts.blocker ? "blocker" : "progress";
318
+ await inbox(machine, selfId, parent, `<feature-${kind} from="${selfId}">
319
+ ${text.trim()}
320
+ </feature-${kind}>`, `feature ${kind}`);
321
+ console.log(`Reported ${kind} to lead ${parent.slice(0, 8)}.`);
322
+ } finally {
323
+ await server.disconnect();
324
+ }
325
+ }
326
+ async function featureDone(opts = {}) {
327
+ const selfId = requireLead(opts.selfId);
328
+ const { server, machine } = await connectAndGetMachine(opts.machineId);
329
+ try {
330
+ const meta = await getMeta(machine, selfId);
331
+ const parent = meta?.parentSessionId;
332
+ if (!parent) {
333
+ console.error("This session has no parent (lead) to report to.");
334
+ process.exit(1);
335
+ }
336
+ const crew = meta?.crew || {};
337
+ const dir = meta?.path || crew.worktreePath || "";
338
+ const base = crew.baseBranch || "main";
339
+ const branch = crew.branch || (dir && existsSync(dir) ? currentBranch(dir) : "") || "";
340
+ const ab = dir && existsSync(dir) ? aheadBehind(dir, base) : { ahead: 0, behind: 0 };
341
+ let dirty = false;
342
+ try {
343
+ dirty = dir && existsSync(dir) ? worktreeStatus(dir) !== "" : false;
344
+ } catch {
345
+ }
346
+ const body = [
347
+ `<merge-request child="${selfId}" branch="${branch}" base="${base}" commits="${ab.ahead}"${dirty ? ' dirty="true"' : ""}>`,
348
+ crew.feature ? `Feature: ${crew.feature}` : "",
349
+ opts.summary ? `Summary: ${opts.summary.trim()}` : "",
350
+ dirty ? "WARNING: worktree has uncommitted changes \u2014 commit before this can merge." : "",
351
+ `Review & merge with: svamp feature merge ${selfId.slice(0, 8)}`,
352
+ `</merge-request>`
353
+ ].filter(Boolean).join("\n");
354
+ await inbox(machine, selfId, parent, body, `merge-request (${branch || "feature"})`);
355
+ console.log(`Sent merge-request to lead ${parent.slice(0, 8)} (branch ${branch || "?"}, +${ab.ahead} commits).`);
356
+ if (dirty) console.log(" \u26A0 Worktree is dirty \u2014 commit your work or the merge will be rejected.");
357
+ } finally {
358
+ await server.disconnect();
359
+ }
360
+ }
361
+ async function featureMerge(childPartial, opts = {}) {
362
+ const leadId = opts.leadId || process.env.SVAMP_SESSION_ID || "";
363
+ const { server, machine, fullId: childId } = await connectAndResolveSession(childPartial, opts.machineId);
364
+ try {
365
+ const meta = await getMeta(machine, childId);
366
+ const crew = meta?.crew || {};
367
+ const worktreePath = crew.worktreePath || meta?.path || "";
368
+ if (!worktreePath) {
369
+ console.error("Could not determine the child worktree path.");
370
+ process.exit(1);
371
+ }
372
+ const base = crew.baseBranch || "main";
373
+ const branch = crew.branch || currentBranch(worktreePath) || "";
374
+ if (!branch) {
375
+ console.error("Could not determine the feature branch.");
376
+ process.exit(1);
377
+ }
378
+ const projectRoot = projectRootFromWorktree(worktreePath) || (leadId ? (await getMeta(machine, leadId).catch(() => ({})))?.path : null) || "";
379
+ if (!projectRoot) {
380
+ console.error("Could not determine the lead project root (base worktree).");
381
+ process.exit(1);
382
+ }
383
+ if (leadId) {
384
+ try {
385
+ await inbox(
386
+ machine,
387
+ leadId,
388
+ childId,
389
+ `<crew-freeze>Your lead is merging branch ${branch} into ${base}. Stop editing files now; do not commit until told.</crew-freeze>`,
390
+ "crew: freeze for merge"
391
+ );
392
+ } catch {
393
+ }
394
+ }
395
+ const r = mergeBack({ projectRoot, branch, worktreePath, baseBranch: base, deleteBranch: opts.deleteBranch });
396
+ if (!r.ok) {
397
+ if (r.rework && leadId) {
398
+ try {
399
+ await inbox(
400
+ machine,
401
+ leadId,
402
+ childId,
403
+ `<crew-rework branch="${branch}">
404
+ Merge was not applied: ${r.detail}
405
+ Resolve and run \`svamp feature done\` again.
406
+ </crew-rework>`,
407
+ "crew: rework needed"
408
+ );
409
+ } catch {
410
+ }
411
+ console.error(`Merge deferred (rework) at ${r.stage}: ${r.detail}`);
412
+ console.error(`Child ${childId.slice(0, 8)} re-woken with guidance; left running.`);
413
+ } else {
414
+ console.error(`Merge failed at ${r.stage}: ${r.detail}`);
415
+ }
416
+ process.exit(1);
417
+ }
418
+ if (!opts.keepChild) {
419
+ try {
420
+ await machine.archiveSession(childId);
421
+ } catch (e) {
422
+ console.warn(`Merged, but could not archive child: ${e.message}`);
423
+ }
424
+ }
425
+ console.log(`\u2705 Merged ${branch} \u2192 ${base}; worktree removed; child ${childId.slice(0, 8)} ${opts.keepChild ? "kept" : "archived"}.`);
426
+ } finally {
427
+ await server.disconnect();
428
+ }
429
+ }
430
+ function flag(args, ...names) {
431
+ for (const n of names) {
432
+ const i = args.indexOf(n);
433
+ if (i !== -1 && i + 1 < args.length) return args[i + 1];
434
+ }
435
+ return void 0;
436
+ }
437
+ function has(args, ...names) {
438
+ return names.some((n) => args.includes(n));
439
+ }
440
+ function positionals(args, valueFlags) {
441
+ const out = [];
442
+ for (let i = 0; i < args.length; i++) {
443
+ const a = args[i];
444
+ if (a.startsWith("-")) {
445
+ if (valueFlags.includes(a)) i++;
446
+ continue;
447
+ }
448
+ out.push(a);
449
+ }
450
+ return out;
451
+ }
452
+ const FEATURE_HELP = `Usage: svamp feature <command>
453
+
454
+ start "<brief>" [--oracle "<cmd>"] [--base <branch>] [--action nudge|block]
455
+ [--max N] [--dir <path>] [--lead <id>] [-m <machine>]
456
+ Spawn a managed worktree child of the lead + attach a supervisor (oracle\u2192parent).
457
+ list [--json] [--lead <id>] [-m <machine>]
458
+ The lead's feature children: branch, ahead/behind, status.
459
+ report "<text>" [--blocker] [-m <machine>] (run by a child) progress/blocker \u2192 lead
460
+ done [--summary "<text>"] [-m <machine>] (run by a child) send a merge-request \u2192 lead
461
+ merge <child-id> [--keep-child] [--no-delete-branch] [-m <machine>]
462
+ (run by the lead) verify \u2192 merge \u2192 remove worktree \u2192 archive child.`;
463
+ async function crewCommand(args) {
464
+ const sub = args[0];
465
+ const rest = args.slice(1);
466
+ const machineId = flag(rest, "-m", "--machine");
467
+ const lead = flag(rest, "--lead");
468
+ if (!sub || sub === "--help" || sub === "-h") {
469
+ console.log(FEATURE_HELP);
470
+ return;
471
+ }
472
+ if (sub === "start") {
473
+ const valueFlags = ["--oracle", "--base", "--action", "--max", "--dir", "-d", "--lead", "-m", "--machine"];
474
+ const brief = positionals(rest, valueFlags).join(" ");
475
+ const maxStr = flag(rest, "--max");
476
+ const action = flag(rest, "--action");
477
+ await featureStart(brief, {
478
+ leadId: lead,
479
+ machineId,
480
+ directory: flag(rest, "--dir", "-d"),
481
+ oracle: flag(rest, "--oracle"),
482
+ baseBranch: flag(rest, "--base"),
483
+ action: action === "block" ? "block" : action === "nudge" ? "nudge" : void 0,
484
+ maxRounds: maxStr ? parseInt(maxStr, 10) : void 0
485
+ });
486
+ } else if (sub === "list" || sub === "ls") {
487
+ await featureList({ leadId: lead, machineId, json: has(rest, "--json") });
488
+ } else if (sub === "report") {
489
+ const text = positionals(rest, ["-m", "--machine", "--lead"]).join(" ");
490
+ await featureReport(text, { machineId, blocker: has(rest, "--blocker") });
491
+ } else if (sub === "done") {
492
+ await featureDone({ machineId, summary: flag(rest, "--summary") });
493
+ } else if (sub === "merge") {
494
+ const child = positionals(rest, ["-m", "--machine", "--lead"])[0];
495
+ if (!child) {
496
+ console.error("Usage: svamp feature merge <child-id>");
497
+ process.exit(1);
498
+ }
499
+ await featureMerge(child, {
500
+ leadId: lead,
501
+ machineId,
502
+ keepChild: has(rest, "--keep-child"),
503
+ deleteBranch: has(rest, "--no-delete-branch") ? false : void 0
504
+ });
505
+ } else {
506
+ console.error(`Unknown feature command: ${sub}
507
+ `);
508
+ console.log(FEATURE_HELP);
509
+ process.exit(1);
510
+ }
511
+ }
512
+
513
+ export { crewCommand, featureDone, featureList, featureMerge, featureReport, featureStart };
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createServer } from 'node:http';
3
- import { R as RoutineStore, m as RoutineRunner } from './run-DHPCWQUq.mjs';
3
+ import { R as RoutineStore, m as RoutineRunner } from './run-9C2ogsuu.mjs';
4
4
  import 'os';
5
5
  import 'fs/promises';
6
6
  import 'fs';
@@ -89,7 +89,7 @@ async function routineCommand(args) {
89
89
  process.exit(1);
90
90
  }
91
91
  const r = store.save(buildRoutineFromArgs(a));
92
- console.log(`\u2705 added routine ${r.id} (${r.name}) trigger=${r.trigger.type} action=${r.action.kind} session=${r.session_id}`);
92
+ console.log(`\u2705 added trigger ${r.id} (${r.name}) source=${r.trigger.type} action=${r.action.kind} session=${r.session_id}`);
93
93
  if (r.trigger.key) console.log(` webhook key: ${r.trigger.key}`);
94
94
  break;
95
95
  }
@@ -100,7 +100,7 @@ async function routineCommand(args) {
100
100
  break;
101
101
  }
102
102
  if (!rs.length) {
103
- console.log("(no routines)");
103
+ console.log("(no triggers)");
104
104
  break;
105
105
  }
106
106
  for (const r of rs) console.log(`${r.enabled ? "\u25CF" : "\u25CB"} ${r.id} ${r.name} [${r.trigger.type}${r.trigger.cron ? " " + r.trigger.cron : ""}] -> ${r.action.kind} ${r.bind === "stateless" ? `stateless(${r.dir || "?"})` : `session=${r.session_id}`}`);
@@ -124,7 +124,7 @@ async function routineCommand(args) {
124
124
  await serve(runner, Number(a.port) || 8722);
125
125
  break;
126
126
  default:
127
- console.log(`usage: svamp routine <add|list|remove|enable|disable|run-now|serve>
127
+ console.log(`usage: svamp trigger <add|list|remove|enable|disable|run-now|serve> (alias: svamp routine)
128
128
  add --session <id> --name <n> [--schedule "*/5 * * * *" [--missed catchup]] [--webhook|--api [--get] [--public]] [--stateless --dir <path>] (--message "..." | --loop --dir <path> --task "..." [--oracle "cmd"])
129
129
  list [--session <id>] [--json]
130
130
  serve [--port 8722]`);
@@ -165,9 +165,9 @@ async function serve(runner, port) {
165
165
  });
166
166
  });
167
167
  server.listen(port, () => {
168
- console.log(`\u{1FA9D} routine server on http://localhost:${port}`);
168
+ console.log(`\u{1FA9D} trigger server on http://localhost:${port}`);
169
169
  console.log(` webhook URL: http://localhost:${port}/routine/<id>?key=<key>`);
170
- console.log(` expose: svamp service expose routines --port ${port}`);
170
+ console.log(` expose: svamp service expose triggers --port ${port}`);
171
171
  });
172
172
  process.on("SIGINT", () => {
173
173
  clearInterval(timer);