svamp-cli 0.2.225 → 0.2.226
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-CGLZ9Nn0.mjs +526 -0
- package/dist/auth-DLB7CATM.mjs +83 -0
- package/dist/caddy-CuTbE3NY.mjs +322 -0
- package/dist/cli.mjs +2338 -0
- package/dist/commands-BQ2E18LH.mjs +643 -0
- package/dist/commands-Ci-jYuU7.mjs +265 -0
- package/dist/commands-Ctop57By.mjs +2762 -0
- package/dist/commands-DME0HxkN.mjs +428 -0
- package/dist/commands-_Dkx6klo.mjs +493 -0
- package/dist/commands-m_enQcEY.mjs +196 -0
- package/dist/commands-x0wZTLMO.mjs +610 -0
- package/dist/fleet-GZK3g7xJ.mjs +356 -0
- package/dist/frpc-CjX2a7K3.mjs +681 -0
- package/dist/headlessCli-DUzIzGu0.mjs +333 -0
- package/dist/httpServer-B1KVQJfm.mjs +276 -0
- package/dist/index.mjs +22 -0
- package/dist/package-DdcBR3X5.mjs +63 -0
- package/dist/pinnedClaudeCode-VIupR1NK.mjs +58 -0
- package/dist/rpc-BW-jkinw.mjs +92 -0
- package/dist/rpc-Dozy-lbH.mjs +194 -0
- package/dist/run-BCUQw-5v.mjs +16065 -0
- package/dist/run-VT3Ai9g8.mjs +1086 -0
- package/dist/runStore-CtptN7US.mjs +168 -0
- package/dist/sandboxDetect-DNTcbgWD.mjs +12 -0
- package/dist/scheduler-BAJ2fbGU.mjs +102 -0
- package/dist/serveCommands-KrDKB4O9.mjs +310 -0
- package/dist/serveManager-DYbR8dXr.mjs +781 -0
- package/dist/serviceManager-hlOVxkhW.mjs +78 -0
- package/dist/sideband-4j84ebl4.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,493 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { m as resolveProjectRoot, w as searchIssues, v as listIssues, o as resumeIssue, p as pauseIssue, q as addComment, u as updateIssue, n as getIssue, x as isVisibleTo, z as summarize, t as addIssue } from './run-BCUQw-5v.mjs';
|
|
5
|
+
import 'os';
|
|
6
|
+
import 'fs/promises';
|
|
7
|
+
import 'fs';
|
|
8
|
+
import 'path';
|
|
9
|
+
import 'url';
|
|
10
|
+
import 'child_process';
|
|
11
|
+
import 'crypto';
|
|
12
|
+
import 'node:crypto';
|
|
13
|
+
import 'util';
|
|
14
|
+
import 'node:events';
|
|
15
|
+
import 'node:os';
|
|
16
|
+
import '@agentclientprotocol/sdk';
|
|
17
|
+
import '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
+
import '@modelcontextprotocol/sdk/client/stdio.js';
|
|
19
|
+
import '@modelcontextprotocol/sdk/types.js';
|
|
20
|
+
import 'zod';
|
|
21
|
+
import 'node:fs/promises';
|
|
22
|
+
import 'node:util';
|
|
23
|
+
|
|
24
|
+
const STATUS_GLYPH = {
|
|
25
|
+
ready: "\u25C9",
|
|
26
|
+
in_progress: "\u25D0",
|
|
27
|
+
paused: "\u23F8",
|
|
28
|
+
archived: "\u25A3"
|
|
29
|
+
};
|
|
30
|
+
function allFlags(args, name) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (let i = 0; i < args.length; i++) if (args[i] === name && i + 1 < args.length) out.push(args[++i]);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function flag(args, name) {
|
|
36
|
+
const i = args.indexOf(name);
|
|
37
|
+
return i !== -1 && i + 1 < args.length ? args[i + 1] : void 0;
|
|
38
|
+
}
|
|
39
|
+
function has(args, name) {
|
|
40
|
+
return args.includes(name);
|
|
41
|
+
}
|
|
42
|
+
function loopActiveForCwd(cwd = process.cwd(), sid = process.env.SVAMP_SESSION_ID) {
|
|
43
|
+
const candidates = [];
|
|
44
|
+
if (sid) candidates.push(join(cwd, ".svamp", sid, "loop", "loop-state.json"));
|
|
45
|
+
candidates.push(join(cwd, ".claude", "loop", "loop-state.json"));
|
|
46
|
+
for (const p of candidates) {
|
|
47
|
+
try {
|
|
48
|
+
if (existsSync(p)) {
|
|
49
|
+
const st = JSON.parse(readFileSync(p, "utf-8"));
|
|
50
|
+
if (st && st.active === true) return true;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function positional(args) {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (let i = 0; i < args.length; i++) {
|
|
60
|
+
const a = args[i];
|
|
61
|
+
if (a.startsWith("--")) {
|
|
62
|
+
if (a !== "--json" && a !== "--ready") i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out.push(a);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function shortSess(s) {
|
|
70
|
+
if (!s) return "";
|
|
71
|
+
const p = s.split("-");
|
|
72
|
+
return p.length >= 2 ? `${p[0]}-${p[1]}` : s;
|
|
73
|
+
}
|
|
74
|
+
function fmtIssue(i) {
|
|
75
|
+
const tri = i.triaged === false ? " \u25B3triage" : "";
|
|
76
|
+
const owner = i.status === "in_progress" && i.session ? shortSess(i.session) : "";
|
|
77
|
+
const disp = i.disposition === "crew" ? owner ? ` {crew:${owner}}` : " {crew}" : owner ? ` {session:${owner}}` : "";
|
|
78
|
+
const label = i.labels.length ? ` [${i.labels.join(", ")}]` : "";
|
|
79
|
+
const v = i.verify ? ` (verify:${i.verify.type})` : "";
|
|
80
|
+
const br = i.branch ? ` {branch:${i.branch}}` : "";
|
|
81
|
+
const ans = i.answer ? " \u21A9answered" : "";
|
|
82
|
+
return `#${i.id} ${STATUS_GLYPH[i.status]} ${i.title}${tri}${disp}${label}${v}${br}${ans}`;
|
|
83
|
+
}
|
|
84
|
+
function buildVerify(args) {
|
|
85
|
+
const cmd = flag(args, "--verify-cmd");
|
|
86
|
+
if (cmd) return { type: "command", text: cmd };
|
|
87
|
+
const agent = flag(args, "--verify");
|
|
88
|
+
if (agent) return { type: "agent", text: agent };
|
|
89
|
+
if (has(args, "--manual")) return { type: "manual" };
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
async function issueCommand(args) {
|
|
93
|
+
const sub = args[0];
|
|
94
|
+
const rest = args.slice(1);
|
|
95
|
+
const root = resolveProjectRoot();
|
|
96
|
+
const json = has(args, "--json");
|
|
97
|
+
const sessionId = process.env.SVAMP_SESSION_ID || null;
|
|
98
|
+
const out = (text) => console.log(text);
|
|
99
|
+
const ok = (obj, text) => {
|
|
100
|
+
if (json) console.log(JSON.stringify(obj));
|
|
101
|
+
else out(text);
|
|
102
|
+
};
|
|
103
|
+
switch (sub) {
|
|
104
|
+
case "add": {
|
|
105
|
+
let title = positional(rest)[0];
|
|
106
|
+
let body = flag(rest, "--body");
|
|
107
|
+
let original;
|
|
108
|
+
if (!title) {
|
|
109
|
+
const src = (body || "").trim();
|
|
110
|
+
if (!src) {
|
|
111
|
+
console.error('usage: svamp issue add "<title or text>" [--ready] [--label x] [--verify-cmd "npm test"] [--scope project|session] [--body "\u2026"]');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const nl = src.indexOf("\n");
|
|
115
|
+
title = (nl === -1 ? src : src.slice(0, nl)).trim().slice(0, 200);
|
|
116
|
+
original = src;
|
|
117
|
+
body = void 0;
|
|
118
|
+
}
|
|
119
|
+
const labels = rest.filter((a, idx) => rest[idx - 1] === "--label");
|
|
120
|
+
const owner = flag(rest, "--session") || sessionId;
|
|
121
|
+
const scope = flag(rest, "--scope") || (owner ? "session" : "project");
|
|
122
|
+
const issue = addIssue(root, {
|
|
123
|
+
title,
|
|
124
|
+
// New issues are actionable immediately (#0049) — there is no backlog to promote from.
|
|
125
|
+
// `--ready` is accepted for back-compat but is now the only behavior.
|
|
126
|
+
status: "ready",
|
|
127
|
+
scope,
|
|
128
|
+
labels,
|
|
129
|
+
verify: buildVerify(rest),
|
|
130
|
+
session: scope === "session" ? owner : null,
|
|
131
|
+
owner,
|
|
132
|
+
// creating session (#0099) — scopes project issues to their creator/parent
|
|
133
|
+
original,
|
|
134
|
+
body
|
|
135
|
+
});
|
|
136
|
+
ok(issue, `Created #${issue.id} (${issue.status}): ${issue.title}`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "list":
|
|
140
|
+
case "ls": {
|
|
141
|
+
const status = flag(rest, "--status");
|
|
142
|
+
const opts = { includeArchived: status === "all" || status === "archived" || has(rest, "--all") };
|
|
143
|
+
if (status && status !== "all") opts.status = status;
|
|
144
|
+
if (flag(rest, "--label")) opts.label = flag(rest, "--label");
|
|
145
|
+
if (flag(rest, "--scope")) opts.scope = flag(rest, "--scope");
|
|
146
|
+
const mineList = flag(rest, "--session") || (has(rest, "--mine") ? sessionId : null);
|
|
147
|
+
let items = listIssues(root, opts);
|
|
148
|
+
if (mineList) items = items.filter((i) => isVisibleTo(i, mineList));
|
|
149
|
+
if (json) {
|
|
150
|
+
console.log(JSON.stringify(items.map(({ body, original, ...r }) => r)));
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (!items.length) {
|
|
154
|
+
out("No issues.");
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
for (const i of items) out(fmtIssue(i));
|
|
158
|
+
const s = summarize(listIssues(root, { includeArchived: true }));
|
|
159
|
+
out(`
|
|
160
|
+
${s.ready} ready \xB7 ${s.in_progress} in-progress \xB7 ${s.archived} archived`);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
case "show": {
|
|
164
|
+
const id = positional(rest)[0];
|
|
165
|
+
const issue = id ? getIssue(root, id) : null;
|
|
166
|
+
if (!issue) {
|
|
167
|
+
console.error(`Issue not found: ${id}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
if (json) {
|
|
171
|
+
console.log(JSON.stringify(issue));
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
out(fmtIssue(issue));
|
|
175
|
+
out(`status: ${issue.status} \xB7 scope: ${issue.scope} \xB7 created: ${issue.created}${issue.closed ? ` \xB7 closed: ${issue.closed}` : ""}`);
|
|
176
|
+
if (issue.answer) out(`
|
|
177
|
+
\u21A9 The user answered your question \u2014 act on this: ${issue.answer}`);
|
|
178
|
+
if (issue.question) out(`
|
|
179
|
+
\u23F8 Paused \u2014 awaiting your question: ${issue.question.text}`);
|
|
180
|
+
if (issue.original) out(`
|
|
181
|
+
Original request:
|
|
182
|
+
${issue.original}`);
|
|
183
|
+
if (issue.body) out(`
|
|
184
|
+
Description:
|
|
185
|
+
${issue.body}`);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "work": {
|
|
189
|
+
const id = positional(rest)[0];
|
|
190
|
+
if (!id) {
|
|
191
|
+
console.error("usage: svamp issue work <id> [--branch <name>] [--session <id>]");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
const branch = flag(rest, "--branch");
|
|
195
|
+
const worker = flag(rest, "--session") || sessionId;
|
|
196
|
+
const cur = getIssue(root, id);
|
|
197
|
+
const claim = worker && cur && cur.scope === "project" && !cur.session ? { session: worker } : {};
|
|
198
|
+
const updated = updateIssue(root, id, { status: "in_progress", answer: null, ...branch ? { branch } : {}, ...claim });
|
|
199
|
+
if (!updated) {
|
|
200
|
+
console.error(`Issue not found: ${id}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
ok(updated, `#${updated.id} \u2192 in_progress${updated.branch ? ` on ${updated.branch}` : ""}${claim.session ? ` (claimed by ${claim.session})` : ""}`);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case "ready":
|
|
207
|
+
case "start":
|
|
208
|
+
case "close":
|
|
209
|
+
case "done":
|
|
210
|
+
case "reopen":
|
|
211
|
+
case "archive":
|
|
212
|
+
case "backlog": {
|
|
213
|
+
const id = positional(rest)[0];
|
|
214
|
+
if (!id) {
|
|
215
|
+
console.error(`usage: svamp issue ${sub} <id>`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const map = {
|
|
219
|
+
ready: "ready",
|
|
220
|
+
start: "in_progress",
|
|
221
|
+
close: "archived",
|
|
222
|
+
done: "archived",
|
|
223
|
+
reopen: "ready",
|
|
224
|
+
archive: "archived",
|
|
225
|
+
backlog: "ready"
|
|
226
|
+
};
|
|
227
|
+
if (sub === "close" || sub === "done") {
|
|
228
|
+
const current = getIssue(root, id);
|
|
229
|
+
const vc = current?.verify?.type === "command" ? current.verify.text?.trim() : "";
|
|
230
|
+
if (vc) {
|
|
231
|
+
if (has(rest, "--force")) {
|
|
232
|
+
console.error(`\u26A0 #${id}: closing WITHOUT running verify-cmd (--force): \`${vc}\``);
|
|
233
|
+
} else {
|
|
234
|
+
try {
|
|
235
|
+
execSync(vc, { cwd: root, stdio: "pipe", timeout: 6e5, maxBuffer: 64 * 1024 * 1024 });
|
|
236
|
+
} catch (e) {
|
|
237
|
+
const tail = (String(e?.stdout || "") + String(e?.stderr || "")).split("\n").slice(-15).join("\n").trim();
|
|
238
|
+
console.error(`Refusing to close #${id}: its verify-cmd failed \u2014 the fix is not verified.
|
|
239
|
+
$ ${vc}
|
|
240
|
+
${tail}
|
|
241
|
+
(Fix the issue, or pass --force to override.)`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const updated = updateIssue(root, id, { status: map[sub] });
|
|
248
|
+
if (!updated) {
|
|
249
|
+
console.error(`Issue not found: ${id}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
ok(updated, `#${updated.id} \u2192 ${updated.status}: ${updated.title}`);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case "edit": {
|
|
256
|
+
const id = positional(rest)[0];
|
|
257
|
+
if (!id) {
|
|
258
|
+
console.error("usage: svamp issue edit <id> [--title \u2026] [--label x] [--verify-cmd \u2026] [--scope \u2026] [--body \u2026]");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const patch = {};
|
|
262
|
+
if (flag(rest, "--title")) patch.title = flag(rest, "--title");
|
|
263
|
+
const labels = rest.filter((a, idx) => rest[idx - 1] === "--label");
|
|
264
|
+
if (labels.length) patch.labels = labels;
|
|
265
|
+
const v = buildVerify(rest);
|
|
266
|
+
if (v || has(rest, "--no-verify")) patch.verify = has(rest, "--no-verify") ? null : v;
|
|
267
|
+
if (flag(rest, "--scope")) patch.scope = flag(rest, "--scope");
|
|
268
|
+
if (has(rest, "--unclaim")) patch.session = null;
|
|
269
|
+
else if (flag(rest, "--session")) patch.session = flag(rest, "--session");
|
|
270
|
+
if (flag(rest, "--body")) patch.body = flag(rest, "--body");
|
|
271
|
+
if (flag(rest, "--status")) patch.status = flag(rest, "--status");
|
|
272
|
+
if (has(rest, "--crew")) patch.disposition = "crew";
|
|
273
|
+
else if (has(rest, "--inline")) patch.disposition = "inline";
|
|
274
|
+
if (has(rest, "--triaged")) patch.triaged = true;
|
|
275
|
+
const updated = updateIssue(root, id, patch);
|
|
276
|
+
if (!updated) {
|
|
277
|
+
console.error(`Issue not found: ${id}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
ok(updated, `Updated #${updated.id}: ${updated.title}`);
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "triage": {
|
|
284
|
+
const id = positional(rest)[0];
|
|
285
|
+
if (!id) {
|
|
286
|
+
console.error('usage: svamp issue triage <id> [--title "\u2026"] [--label x \u2026] [--crew|--inline] [--verify-cmd "cmd" | --verify "criterion" | --manual] [--body "rewritten"] [--backlog]');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
const patch = { triaged: true };
|
|
290
|
+
if (flag(rest, "--title")) patch.title = flag(rest, "--title");
|
|
291
|
+
const tlabels = rest.filter((a, idx) => rest[idx - 1] === "--label");
|
|
292
|
+
if (tlabels.length) patch.labels = tlabels;
|
|
293
|
+
if (has(rest, "--crew")) patch.disposition = "crew";
|
|
294
|
+
else if (has(rest, "--inline")) patch.disposition = "inline";
|
|
295
|
+
const tv = buildVerify(rest);
|
|
296
|
+
if (tv) patch.verify = tv;
|
|
297
|
+
if (flag(rest, "--body")) patch.body = flag(rest, "--body");
|
|
298
|
+
patch.status = "ready";
|
|
299
|
+
const updated = updateIssue(root, id, patch);
|
|
300
|
+
if (!updated) {
|
|
301
|
+
console.error(`Issue not found: ${id}`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
ok(updated, `Triaged #${updated.id}: ${updated.title} [${updated.disposition}${updated.verify ? ", verify:" + updated.verify.type : ""}] \u2192 ${updated.status}`);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "comment": {
|
|
308
|
+
const id = positional(rest)[0];
|
|
309
|
+
const text = positional(rest)[1];
|
|
310
|
+
if (!id || !text) {
|
|
311
|
+
console.error('usage: svamp issue comment <id> "<text>"');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
const updated = addComment(root, id, text);
|
|
315
|
+
if (!updated) {
|
|
316
|
+
console.error(`Issue not found: ${id}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
ok(updated, `Commented on #${updated.id}`);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case "pause": {
|
|
323
|
+
const id = positional(rest)[0];
|
|
324
|
+
if (!id) {
|
|
325
|
+
console.error('usage: svamp issue pause <id> --blocked "<reason>" [--question "\u2026"] [--option "A" --option "B"]');
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
const question = flag(rest, "--question");
|
|
329
|
+
const options = allFlags(rest, "--option");
|
|
330
|
+
const blocked = flag(rest, "--blocked");
|
|
331
|
+
if (loopActiveForCwd() && !blocked) {
|
|
332
|
+
console.error(
|
|
333
|
+
`Refusing to pause #${id}: a loop is active and no genuine block was declared.
|
|
334
|
+
Finished work should be CLOSED, not paused for verification \u2014 self-verify (cite the
|
|
335
|
+
commit/tests, note anything you couldn't check), \`svamp issue close ${id}\`, and the user
|
|
336
|
+
reopens with a follow-up if it's wrong. Only pause for a REAL block you cannot resolve by
|
|
337
|
+
any default (missing secret/access, a destructive action needing sign-off, or a decision
|
|
338
|
+
the user explicitly reserved): re-run with \`--blocked "<the specific blocker>"\`.`
|
|
339
|
+
);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
const updated = pauseIssue(root, id, question ? { text: question, options } : void 0);
|
|
343
|
+
if (!updated) {
|
|
344
|
+
console.error(`Issue not found: ${id}`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
ok(updated, `Paused #${updated.id}${blocked ? ` \u2014 BLOCKED: ${blocked}` : ""}${question ? ` \u2014 asked: ${question}` : ""}`);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "resume": {
|
|
351
|
+
const id = positional(rest)[0];
|
|
352
|
+
if (!id) {
|
|
353
|
+
console.error('usage: svamp issue resume <id> [--answer "\u2026"]');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
const answer = flag(rest, "--answer") || positional(rest)[1];
|
|
357
|
+
const updated = resumeIssue(root, id, answer);
|
|
358
|
+
if (!updated) {
|
|
359
|
+
console.error(`Issue not found: ${id}`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
ok(updated, `Resumed #${updated.id}`);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "pending": {
|
|
366
|
+
const mine = flag(rest, "--session") || (has(rest, "--mine") ? sessionId : null);
|
|
367
|
+
const inScope = (i) => !mine || isVisibleTo(i, mine);
|
|
368
|
+
const pending = listIssues(root).filter(inScope).filter((i) => i.status === "ready" || i.status === "in_progress");
|
|
369
|
+
if (json) console.log(JSON.stringify(pending));
|
|
370
|
+
else out(pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues.");
|
|
371
|
+
process.exit(pending.length ? 1 : 0);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "search": {
|
|
375
|
+
const q = positional(rest)[0] || "";
|
|
376
|
+
const items = searchIssues(root, q);
|
|
377
|
+
if (json) {
|
|
378
|
+
console.log(JSON.stringify(items.map(({ body, original, ...r }) => r)));
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
if (!items.length) {
|
|
382
|
+
out(`No issues match "${q}".`);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
for (const i of items) out(fmtIssue(i));
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case "guide": {
|
|
389
|
+
const topic = positional(rest)[0];
|
|
390
|
+
if (topic === "triage") {
|
|
391
|
+
out([
|
|
392
|
+
"TRIAGE like a thoughtful human reviewer \u2014 turn a raw user post into the RIGHT outcome, which is",
|
|
393
|
+
'not always "a ready inline issue". First READ it (svamp issue show <id> \u2014 the original text is',
|
|
394
|
+
"preserved separately), then DECIDE THE DISPOSITION:",
|
|
395
|
+
"",
|
|
396
|
+
" A. NOT-AN-ISSUE? Resolve directly instead of leaving a stale item:",
|
|
397
|
+
" - Already done / duplicate / obsolete \u2192 `svamp issue close <id>` with a one-line why (cite the",
|
|
398
|
+
" commit/PR or the issue it duplicates).",
|
|
399
|
+
" - A question you can just answer \u2192 reply, then close.",
|
|
400
|
+
" - Belongs to another session/repo \u2192 say where it should go (and route it), then close/redirect.",
|
|
401
|
+
" B. UNDERSPECIFIED? Strongly prefer to PROCEED on a sensible default and document the assumption \u2014",
|
|
402
|
+
" the user dislikes being asked about every issue. Enrich from the codebase first. Only when a choice",
|
|
403
|
+
" is genuinely ambiguous AND you truly cannot pick a reasonable default (e.g. two incompatible",
|
|
404
|
+
" directions only the user can choose) pause with the real blocker declared \u2014 TRIAGE is the place for",
|
|
405
|
+
" that, since the work loop won't stop to ask:",
|
|
406
|
+
` svamp issue pause <id> --blocked "<why you can't default>" --question "\u2026" [--option "A" --option "B"]`,
|
|
407
|
+
" C. SPLIT if the post bundles several independent asks: `svamp issue add` one per piece (carry the",
|
|
408
|
+
" relevant context), triage each, keep the original as the primary.",
|
|
409
|
+
" D. AUTOMATION: if it implies running on a schedule/event, `svamp workflow add` it (instead of / in",
|
|
410
|
+
" addition to an issue).",
|
|
411
|
+
"",
|
|
412
|
+
" Once it IS a real, actionable issue, set crew-vs-inline by SIZE + INTERFERENCE:",
|
|
413
|
+
" - --inline \u2192 small, low-risk, self-contained: work it in THIS session.",
|
|
414
|
+
" - --crew \u2192 substantial, OR likely to interfere with this session's working tree / long-running",
|
|
415
|
+
" work, OR the user asked for a separate branch: an isolated worktree child does it.",
|
|
416
|
+
" Rule of thumb: if you would not want it half-done in your working tree mid-review, use --crew.",
|
|
417
|
+
" Then specify it:",
|
|
418
|
+
' svamp issue triage <id> --title "<concise title>" [--label <tag>]... \\',
|
|
419
|
+
` [--inline | --crew] [--verify-cmd "<pass/fail cmd>" | --verify "<how we know it's done>"] \\`,
|
|
420
|
+
' [--body "<expanded description>"]',
|
|
421
|
+
" - verify-cmd = a real pass/fail command (oracle); --verify = an agent/human criterion.",
|
|
422
|
+
" Be smart and helpful \u2014 match the disposition to what the user actually needs. Reply with a one-line summary."
|
|
423
|
+
].join("\n"));
|
|
424
|
+
} else if (topic === "work") {
|
|
425
|
+
out([
|
|
426
|
+
"WORK the actionable issues in THIS session's backlog (your own + shared project items; leave other",
|
|
427
|
+
"sessions' private items alone). List them with: svamp issue list --session <id> (shows ready AND",
|
|
428
|
+
"in-progress \u2014 a RESUMED issue is in-progress, so a ready-only filter would hide it). An issue badged",
|
|
429
|
+
"\u21A9answered carries a fresh user answer \u2014 work that one FIRST.",
|
|
430
|
+
" TRIAGE FIRST any \u25B3triage (untriaged) issue \u2014 see `svamp issue guide triage`.",
|
|
431
|
+
" For each issue:",
|
|
432
|
+
" 1. On pickup: `svamp issue show <id>` FIRST, then `svamp issue work <id>`. ALWAYS read show \u2014 its",
|
|
433
|
+
' top surfaces any "\u21A9 The user answered your question \u2014 act on this: \u2026" line. Picking up via',
|
|
434
|
+
" `work` clears that badge, so read it before you do. (status \u2192 in_progress; live status updates.)",
|
|
435
|
+
' 2. As you go: leave substantive `svamp issue comment <id> "\u2026"` notes \u2014 decisions, takeaways, blockers.',
|
|
436
|
+
" 3. Before closing: VERIFY with evidence \u2014 run its verify-cmd; for verify:agent cite the files/commit",
|
|
437
|
+
" that resolve it in a summary comment. Post the summary, THEN `svamp issue close <id>`.",
|
|
438
|
+
" NEVER close an issue that isn't genuinely resolved. The finished state is CLOSED, not paused.",
|
|
439
|
+
" Can't fully verify (a perceptual/on-device/UI look-and-feel result you can't observe headlessly)?",
|
|
440
|
+
" Verify what you CAN (build/tests/deploy + a clear summary of what you couldn't check), then CLOSE",
|
|
441
|
+
" anyway \u2014 the user reviews closed issues and reopens with a follow-up if it's wrong. Do NOT pause an",
|
|
442
|
+
' issue to ask the user to verify/confirm/"look at" finished work \u2014 that defeats the loop.',
|
|
443
|
+
" CONVERGE TO MAIN: if an issue used an isolated branch/worktree, merge it back to `main` (and remove",
|
|
444
|
+
" the worktree) BEFORE closing \u2014 never leave unmerged branches.",
|
|
445
|
+
"",
|
|
446
|
+
" DEFAULT FORWARD \u2014 DON'T PAUSE. Clarifying/preference questions belong in TRIAGE, not here. While",
|
|
447
|
+
" working, if something is ambiguous, pick the most sensible default, PROCEED, and DOCUMENT the assumption",
|
|
448
|
+
" in a `svamp issue comment` (so the user can correct it). An over-eager pause defeats the whole point of",
|
|
449
|
+
" an automated loop \u2014 and pausing just to have the user VERIFY/CONFIRM finished work is the #1 anti-pattern:",
|
|
450
|
+
" close it instead (see step 3). The user reopens if it's wrong.",
|
|
451
|
+
" PAUSE ONLY when you are genuinely BLOCKED and cannot proceed by ANY reasonable default \u2014 you need a",
|
|
452
|
+
" secret/credential/access only the user has, an irreversible/destructive action needs sign-off, or the",
|
|
453
|
+
" user EXPLICITLY reserved the decision. Inside a loop the pause is REFUSED unless you declare the real",
|
|
454
|
+
' blocker, and "I want the user to check my work" is NOT a blocker:',
|
|
455
|
+
' svamp issue pause <id> --blocked "<the specific blocker>" [--question "\u2026"] [--option "A" --option "B"]',
|
|
456
|
+
" A paused issue is EXCLUDED from `pending`, so the loop stops cleanly. The user answers (app renders the",
|
|
457
|
+
" options as buttons) \u2192 it resumes in-progress, badged \u21A9answered with the answer surfaced at the top of",
|
|
458
|
+
" `show` \u2014 re-pick it up and act on the answer (do NOT re-ask the same question).",
|
|
459
|
+
" Keep going until `svamp issue pending --session <id>` is empty (paused issues don't count)."
|
|
460
|
+
].join("\n"));
|
|
461
|
+
} else {
|
|
462
|
+
out("usage: svamp issue guide <triage|work>");
|
|
463
|
+
}
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "help":
|
|
467
|
+
case void 0:
|
|
468
|
+
case "--help":
|
|
469
|
+
case "-h":
|
|
470
|
+
out([
|
|
471
|
+
"svamp issue \u2014 the session backlog (folder of .svamp/issues/<id>.md)",
|
|
472
|
+
"",
|
|
473
|
+
' add ["<title>"] [--ready] [--label x] [--verify-cmd "cmd"] [--scope project|session] [--body "\u2026"]',
|
|
474
|
+
" # title optional \u2014 if omitted, the first line of --body becomes the title",
|
|
475
|
+
" list [--status ready|in_progress|archived|all] [--label x] [--scope \u2026] [--session <id>] [--json]",
|
|
476
|
+
" show <id> [--json]",
|
|
477
|
+
" ready <id> | close <id> (\u2192 archived) | reopen <id> | archive <id> # lifecycle: ready \u2192 in_progress \u2192 archived",
|
|
478
|
+
" work <id> [--branch <name>] # mark in-progress (optionally on a branch; merge to main before close)",
|
|
479
|
+
" triage <id> [--title \u2026] [--label x] [--inline|--crew] [--verify-cmd \u2026|--verify \u2026] [--body \u2026]",
|
|
480
|
+
" edit <id> [--title \u2026] [--label x] [--verify-cmd \u2026] [--no-verify] [--status \u2026] [--body \u2026]",
|
|
481
|
+
' comment <id> "<text>" # append a follow-up to the issue',
|
|
482
|
+
" pending [--session <id>] # exit 0 if no actionable (ready/in_progress) issues \u2014 the loop oracle",
|
|
483
|
+
' search "<query>" [--json]',
|
|
484
|
+
" guide <triage|work> # the triage / work-loop procedure (so triggers can stay short)"
|
|
485
|
+
].join("\n"));
|
|
486
|
+
break;
|
|
487
|
+
default:
|
|
488
|
+
console.error(`Unknown: svamp issue ${sub}. Try: svamp issue help`);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export { issueCommand, loopActiveForCwd };
|