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.
- package/bin/skills/crew/SKILL.md +86 -0
- package/bin/skills/loop/SKILL.md +1 -1
- package/bin/skills/loop/bin/loop-init.mjs +0 -3
- package/dist/{agentCommands--H31qHbm.mjs → agentCommands-BTkU0PQb.mjs} +4 -4
- package/dist/{auth-CUzGJvRf.mjs → auth-DimbhOMP.mjs} +1 -1
- package/dist/cli.mjs +88 -52
- package/dist/{commands-C0715MEC.mjs → commands-3FsdWpJO.mjs} +2 -2
- package/dist/{commands-DdW5M7Le.mjs → commands-B5rek8XG.mjs} +59 -4
- package/dist/{commands-zMw02qH_.mjs → commands-BEjlVtvS.mjs} +1 -1
- package/dist/commands-BJfRk4KT.mjs +513 -0
- package/dist/{commands-ClxBUkI3.mjs → commands-Bw2V_awn.mjs} +6 -6
- package/dist/{commands-C1ERmRw4.mjs → commands-fbQs3jLx.mjs} +5 -5
- package/dist/{fleet-js8FwhM_.mjs → fleet-D5dNVJIp.mjs} +1 -1
- package/dist/{frpc-9qgaimIN.mjs → frpc-CdcXdQde.mjs} +1 -1
- package/dist/{headlessCli-my-nvBDO.mjs → headlessCli-Lk2OU1Gh.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{package-BdLUrz6e.mjs → package-CxWiFy_P.mjs} +3 -3
- package/dist/{run-DHPCWQUq.mjs → run-9C2ogsuu.mjs} +34 -5
- package/dist/{run-DnGdMH2k.mjs → run-DIoR81Ev.mjs} +1 -1
- package/dist/{serveCommands-D9KR-bC5.mjs → serveCommands-BqApmjmR.mjs} +5 -5
- package/dist/{serveManager-B19qVJeZ.mjs → serveManager-XsXnI804.mjs} +2 -2
- package/dist/{sideband-CYNK4foC.mjs → sideband-BHWq1P8E.mjs} +1 -1
- package/package.json +3 -3
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
170
|
+
console.log(` expose: svamp service expose triggers --port ${port}`);
|
|
171
171
|
});
|
|
172
172
|
process.on("SIGINT", () => {
|
|
173
173
|
clearInterval(timer);
|