svamp-cli 0.2.190 → 0.2.193

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.
@@ -0,0 +1,390 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { m as resolveProjectRoot, t as searchIssues, q as listIssues, o as addComment, u as updateIssue, n as getIssue, w as summarize, p as addIssue } from './run-B_UWCqRV.mjs';
3
+ import 'os';
4
+ import 'fs/promises';
5
+ import 'fs';
6
+ import 'path';
7
+ import 'url';
8
+ import 'child_process';
9
+ import 'crypto';
10
+ import 'node:crypto';
11
+ import 'node:fs';
12
+ import 'util';
13
+ import 'node:path';
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
+ archived: "\u25A3"
28
+ };
29
+ function flag(args, name) {
30
+ const i = args.indexOf(name);
31
+ return i !== -1 && i + 1 < args.length ? args[i + 1] : void 0;
32
+ }
33
+ function has(args, name) {
34
+ return args.includes(name);
35
+ }
36
+ function positional(args) {
37
+ const out = [];
38
+ for (let i = 0; i < args.length; i++) {
39
+ const a = args[i];
40
+ if (a.startsWith("--")) {
41
+ if (a !== "--json" && a !== "--ready") i++;
42
+ continue;
43
+ }
44
+ out.push(a);
45
+ }
46
+ return out;
47
+ }
48
+ function fmtIssue(i) {
49
+ const tri = i.triaged === false ? " \u25B3triage" : "";
50
+ const disp = i.disposition === "crew" ? " {crew}" : "";
51
+ const label = i.labels.length ? ` [${i.labels.join(", ")}]` : "";
52
+ const v = i.verify ? ` (verify:${i.verify.type})` : "";
53
+ const br = i.branch ? ` {branch:${i.branch}}` : "";
54
+ return `#${i.id} ${STATUS_GLYPH[i.status]} ${i.title}${tri}${disp}${label}${v}${br}`;
55
+ }
56
+ function buildVerify(args) {
57
+ const cmd = flag(args, "--verify-cmd");
58
+ if (cmd) return { type: "command", text: cmd };
59
+ const agent = flag(args, "--verify");
60
+ if (agent) return { type: "agent", text: agent };
61
+ if (has(args, "--manual")) return { type: "manual" };
62
+ return null;
63
+ }
64
+ async function issueCommand(args) {
65
+ const sub = args[0];
66
+ const rest = args.slice(1);
67
+ const root = resolveProjectRoot();
68
+ const json = has(args, "--json");
69
+ const sessionId = process.env.SVAMP_SESSION_ID || null;
70
+ const out = (text) => console.log(text);
71
+ const ok = (obj, text) => {
72
+ if (json) console.log(JSON.stringify(obj));
73
+ else out(text);
74
+ };
75
+ switch (sub) {
76
+ case "add": {
77
+ let title = positional(rest)[0];
78
+ let body = flag(rest, "--body");
79
+ let original;
80
+ if (!title) {
81
+ const src = (body || "").trim();
82
+ if (!src) {
83
+ console.error('usage: svamp issue add "<title or text>" [--ready] [--label x] [--verify-cmd "npm test"] [--scope project|session] [--body "\u2026"]');
84
+ process.exit(1);
85
+ }
86
+ const nl = src.indexOf("\n");
87
+ title = (nl === -1 ? src : src.slice(0, nl)).trim().slice(0, 200);
88
+ original = src;
89
+ body = void 0;
90
+ }
91
+ const labels = rest.filter((a, idx) => rest[idx - 1] === "--label");
92
+ const owner = flag(rest, "--session") || sessionId;
93
+ const scope = flag(rest, "--scope") || (owner ? "session" : "project");
94
+ const issue = addIssue(root, {
95
+ title,
96
+ // New issues are actionable immediately (#0049) — there is no backlog to promote from.
97
+ // `--ready` is accepted for back-compat but is now the only behavior.
98
+ status: "ready",
99
+ scope,
100
+ labels,
101
+ verify: buildVerify(rest),
102
+ session: scope === "session" ? owner : null,
103
+ original,
104
+ body
105
+ });
106
+ ok(issue, `Created #${issue.id} (${issue.status}): ${issue.title}`);
107
+ break;
108
+ }
109
+ case "list":
110
+ case "ls": {
111
+ const status = flag(rest, "--status");
112
+ const opts = { includeArchived: status === "all" || status === "archived" || has(rest, "--all") };
113
+ if (status && status !== "all") opts.status = status;
114
+ if (flag(rest, "--label")) opts.label = flag(rest, "--label");
115
+ if (flag(rest, "--scope")) opts.scope = flag(rest, "--scope");
116
+ const mineList = flag(rest, "--session") || (has(rest, "--mine") ? sessionId : null);
117
+ let items = listIssues(root, opts);
118
+ if (mineList) items = items.filter((i) => i.scope === "project" || i.session === mineList);
119
+ if (json) {
120
+ console.log(JSON.stringify(items.map(({ body, original, ...r }) => r)));
121
+ break;
122
+ }
123
+ if (!items.length) {
124
+ out("No issues.");
125
+ break;
126
+ }
127
+ for (const i of items) out(fmtIssue(i));
128
+ const s = summarize(listIssues(root, { includeArchived: true }));
129
+ out(`
130
+ ${s.ready} ready \xB7 ${s.in_progress} in-progress \xB7 ${s.archived} archived`);
131
+ break;
132
+ }
133
+ case "show": {
134
+ const id = positional(rest)[0];
135
+ const issue = id ? getIssue(root, id) : null;
136
+ if (!issue) {
137
+ console.error(`Issue not found: ${id}`);
138
+ process.exit(1);
139
+ }
140
+ if (json) {
141
+ console.log(JSON.stringify(issue));
142
+ break;
143
+ }
144
+ out(fmtIssue(issue));
145
+ out(`status: ${issue.status} \xB7 scope: ${issue.scope} \xB7 created: ${issue.created}${issue.closed ? ` \xB7 closed: ${issue.closed}` : ""}`);
146
+ if (issue.original) out(`
147
+ Original request:
148
+ ${issue.original}`);
149
+ if (issue.body) out(`
150
+ Description:
151
+ ${issue.body}`);
152
+ break;
153
+ }
154
+ case "work": {
155
+ const id = positional(rest)[0];
156
+ if (!id) {
157
+ console.error("usage: svamp issue work <id> [--branch <name>] [--session <id>]");
158
+ process.exit(1);
159
+ }
160
+ const branch = flag(rest, "--branch");
161
+ const worker = flag(rest, "--session") || sessionId;
162
+ const cur = getIssue(root, id);
163
+ const claim = worker && cur && cur.scope === "project" && !cur.session ? { session: worker } : {};
164
+ const updated = updateIssue(root, id, { status: "in_progress", ...branch ? { branch } : {}, ...claim });
165
+ if (!updated) {
166
+ console.error(`Issue not found: ${id}`);
167
+ process.exit(1);
168
+ }
169
+ ok(updated, `#${updated.id} \u2192 in_progress${updated.branch ? ` on ${updated.branch}` : ""}${claim.session ? ` (claimed by ${claim.session})` : ""}`);
170
+ break;
171
+ }
172
+ case "ready":
173
+ case "start":
174
+ case "close":
175
+ case "done":
176
+ case "reopen":
177
+ case "archive":
178
+ case "backlog": {
179
+ const id = positional(rest)[0];
180
+ if (!id) {
181
+ console.error(`usage: svamp issue ${sub} <id>`);
182
+ process.exit(1);
183
+ }
184
+ const map = {
185
+ ready: "ready",
186
+ start: "in_progress",
187
+ close: "archived",
188
+ done: "archived",
189
+ reopen: "ready",
190
+ archive: "archived",
191
+ backlog: "ready"
192
+ };
193
+ if (sub === "close" || sub === "done") {
194
+ const current = getIssue(root, id);
195
+ const vc = current?.verify?.type === "command" ? current.verify.text?.trim() : "";
196
+ if (vc) {
197
+ if (has(rest, "--force")) {
198
+ console.error(`\u26A0 #${id}: closing WITHOUT running verify-cmd (--force): \`${vc}\``);
199
+ } else {
200
+ try {
201
+ execSync(vc, { cwd: root, stdio: "pipe", timeout: 6e5, maxBuffer: 64 * 1024 * 1024 });
202
+ } catch (e) {
203
+ const tail = (String(e?.stdout || "") + String(e?.stderr || "")).split("\n").slice(-15).join("\n").trim();
204
+ console.error(`Refusing to close #${id}: its verify-cmd failed \u2014 the fix is not verified.
205
+ $ ${vc}
206
+ ${tail}
207
+ (Fix the issue, or pass --force to override.)`);
208
+ process.exit(1);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ const updated = updateIssue(root, id, { status: map[sub] });
214
+ if (!updated) {
215
+ console.error(`Issue not found: ${id}`);
216
+ process.exit(1);
217
+ }
218
+ ok(updated, `#${updated.id} \u2192 ${updated.status}: ${updated.title}`);
219
+ break;
220
+ }
221
+ case "edit": {
222
+ const id = positional(rest)[0];
223
+ if (!id) {
224
+ console.error("usage: svamp issue edit <id> [--title \u2026] [--label x] [--verify-cmd \u2026] [--scope \u2026] [--body \u2026]");
225
+ process.exit(1);
226
+ }
227
+ const patch = {};
228
+ if (flag(rest, "--title")) patch.title = flag(rest, "--title");
229
+ const labels = rest.filter((a, idx) => rest[idx - 1] === "--label");
230
+ if (labels.length) patch.labels = labels;
231
+ const v = buildVerify(rest);
232
+ if (v || has(rest, "--no-verify")) patch.verify = has(rest, "--no-verify") ? null : v;
233
+ if (flag(rest, "--scope")) patch.scope = flag(rest, "--scope");
234
+ if (has(rest, "--unclaim")) patch.session = null;
235
+ else if (flag(rest, "--session")) patch.session = flag(rest, "--session");
236
+ if (flag(rest, "--body")) patch.body = flag(rest, "--body");
237
+ if (flag(rest, "--status")) patch.status = flag(rest, "--status");
238
+ if (has(rest, "--crew")) patch.disposition = "crew";
239
+ else if (has(rest, "--inline")) patch.disposition = "inline";
240
+ if (has(rest, "--triaged")) patch.triaged = true;
241
+ const updated = updateIssue(root, id, patch);
242
+ if (!updated) {
243
+ console.error(`Issue not found: ${id}`);
244
+ process.exit(1);
245
+ }
246
+ ok(updated, `Updated #${updated.id}: ${updated.title}`);
247
+ break;
248
+ }
249
+ case "triage": {
250
+ const id = positional(rest)[0];
251
+ if (!id) {
252
+ console.error('usage: svamp issue triage <id> [--title "\u2026"] [--label x \u2026] [--crew|--inline] [--verify-cmd "cmd" | --verify "criterion" | --manual] [--body "rewritten"] [--backlog]');
253
+ process.exit(1);
254
+ }
255
+ const patch = { triaged: true };
256
+ if (flag(rest, "--title")) patch.title = flag(rest, "--title");
257
+ const tlabels = rest.filter((a, idx) => rest[idx - 1] === "--label");
258
+ if (tlabels.length) patch.labels = tlabels;
259
+ if (has(rest, "--crew")) patch.disposition = "crew";
260
+ else if (has(rest, "--inline")) patch.disposition = "inline";
261
+ const tv = buildVerify(rest);
262
+ if (tv) patch.verify = tv;
263
+ if (flag(rest, "--body")) patch.body = flag(rest, "--body");
264
+ patch.status = "ready";
265
+ const updated = updateIssue(root, id, patch);
266
+ if (!updated) {
267
+ console.error(`Issue not found: ${id}`);
268
+ process.exit(1);
269
+ }
270
+ ok(updated, `Triaged #${updated.id}: ${updated.title} [${updated.disposition}${updated.verify ? ", verify:" + updated.verify.type : ""}] \u2192 ${updated.status}`);
271
+ break;
272
+ }
273
+ case "comment": {
274
+ const id = positional(rest)[0];
275
+ const text = positional(rest)[1];
276
+ if (!id || !text) {
277
+ console.error('usage: svamp issue comment <id> "<text>"');
278
+ process.exit(1);
279
+ }
280
+ const updated = addComment(root, id, text);
281
+ if (!updated) {
282
+ console.error(`Issue not found: ${id}`);
283
+ process.exit(1);
284
+ }
285
+ ok(updated, `Commented on #${updated.id}`);
286
+ break;
287
+ }
288
+ case "pending": {
289
+ const mine = flag(rest, "--session") || (has(rest, "--mine") ? sessionId : null);
290
+ const inScope = (i) => !mine || i.scope === "project" || i.session === mine;
291
+ const pending = listIssues(root).filter(inScope).filter((i) => i.status === "ready" || i.status === "in_progress");
292
+ if (json) console.log(JSON.stringify(pending));
293
+ else out(pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues.");
294
+ process.exit(pending.length ? 1 : 0);
295
+ break;
296
+ }
297
+ case "search": {
298
+ const q = positional(rest)[0] || "";
299
+ const items = searchIssues(root, q);
300
+ if (json) {
301
+ console.log(JSON.stringify(items.map(({ body, original, ...r }) => r)));
302
+ break;
303
+ }
304
+ if (!items.length) {
305
+ out(`No issues match "${q}".`);
306
+ break;
307
+ }
308
+ for (const i of items) out(fmtIssue(i));
309
+ break;
310
+ }
311
+ case "guide": {
312
+ const topic = positional(rest)[0];
313
+ if (topic === "triage") {
314
+ out([
315
+ "TRIAGE like a thoughtful human reviewer \u2014 turn a raw user post into the RIGHT outcome, which is",
316
+ 'not always "a ready inline issue". First READ it (svamp issue show <id> \u2014 the original text is',
317
+ "preserved separately), then DECIDE THE DISPOSITION:",
318
+ "",
319
+ " A. NOT-AN-ISSUE? Resolve directly instead of leaving a stale item:",
320
+ " - Already done / duplicate / obsolete \u2192 `svamp issue close <id>` with a one-line why (cite the",
321
+ " commit/PR or the issue it duplicates).",
322
+ " - A question you can just answer \u2192 reply, then close.",
323
+ " - Belongs to another session/repo \u2192 say where it should go (and route it), then close/redirect.",
324
+ " B. UNDERSPECIFIED? Do not guess. Enrich from the codebase where you can, and CONFIRM with the user",
325
+ " for anything genuinely ambiguous (ask 1-2 sharp questions) BEFORE marking it ready.",
326
+ " C. SPLIT if the post bundles several independent asks: `svamp issue add` one per piece (carry the",
327
+ " relevant context), triage each, keep the original as the primary.",
328
+ " D. AUTOMATION: if it implies running on a schedule/event, `svamp workflow add` it (instead of / in",
329
+ " addition to an issue).",
330
+ "",
331
+ " Once it IS a real, actionable issue, set crew-vs-inline by SIZE + INTERFERENCE:",
332
+ " - --inline \u2192 small, low-risk, self-contained: work it in THIS session.",
333
+ " - --crew \u2192 substantial, OR likely to interfere with this session's working tree / long-running",
334
+ " work, OR the user asked for a separate branch: an isolated worktree child does it.",
335
+ " Rule of thumb: if you would not want it half-done in your working tree mid-review, use --crew.",
336
+ " Then specify it:",
337
+ ' svamp issue triage <id> --title "<concise title>" [--label <tag>]... \\',
338
+ ` [--inline | --crew] [--verify-cmd "<pass/fail cmd>" | --verify "<how we know it's done>"] \\`,
339
+ ' [--body "<expanded description>"]',
340
+ " - verify-cmd = a real pass/fail command (oracle); --verify = an agent/human criterion.",
341
+ " Be smart and helpful \u2014 match the disposition to what the user actually needs. Reply with a one-line summary."
342
+ ].join("\n"));
343
+ } else if (topic === "work") {
344
+ out([
345
+ "WORK the ready issues in THIS session's backlog (your own + shared project items; leave other",
346
+ "sessions' private items alone). List: svamp issue list --status ready --session <id>.",
347
+ " TRIAGE FIRST any \u25B3triage (untriaged) issue \u2014 see `svamp issue guide triage`.",
348
+ " For each issue:",
349
+ " 1. On pickup: svamp issue work <id> (status \u2192 in_progress; live status reflects the work).",
350
+ ' 2. As you go: leave substantive `svamp issue comment <id> "\u2026"` notes \u2014 decisions, takeaways, blockers.',
351
+ " 3. Before closing: VERIFY with evidence \u2014 run its verify-cmd; for verify:agent cite the files/commit",
352
+ " that resolve it in a summary comment. Post the summary, THEN `svamp issue close <id>`.",
353
+ " NEVER close an issue that isn't genuinely resolved.",
354
+ " CONVERGE TO MAIN: if an issue used an isolated branch/worktree, merge it back to `main` (and remove",
355
+ " the worktree) BEFORE closing \u2014 never leave unmerged branches.",
356
+ " Keep going until `svamp issue pending --session <id>` is empty."
357
+ ].join("\n"));
358
+ } else {
359
+ out("usage: svamp issue guide <triage|work>");
360
+ }
361
+ break;
362
+ }
363
+ case "help":
364
+ case void 0:
365
+ case "--help":
366
+ case "-h":
367
+ out([
368
+ "svamp issue \u2014 the session backlog (folder of .svamp/issues/<id>.md)",
369
+ "",
370
+ ' add ["<title>"] [--ready] [--label x] [--verify-cmd "cmd"] [--scope project|session] [--body "\u2026"]',
371
+ " # title optional \u2014 if omitted, the first line of --body becomes the title",
372
+ " list [--status ready|in_progress|archived|all] [--label x] [--scope \u2026] [--session <id>] [--json]",
373
+ " show <id> [--json]",
374
+ " ready <id> | close <id> (\u2192 archived) | reopen <id> | archive <id> # lifecycle: ready \u2192 in_progress \u2192 archived",
375
+ " work <id> [--branch <name>] # mark in-progress (optionally on a branch; merge to main before close)",
376
+ " triage <id> [--title \u2026] [--label x] [--inline|--crew] [--verify-cmd \u2026|--verify \u2026] [--body \u2026]",
377
+ " edit <id> [--title \u2026] [--label x] [--verify-cmd \u2026] [--no-verify] [--status \u2026] [--body \u2026]",
378
+ ' comment <id> "<text>" # append a follow-up to the issue',
379
+ " pending [--session <id>] # exit 0 if no actionable (ready/in_progress) issues \u2014 the loop oracle",
380
+ ' search "<query>" [--json]',
381
+ " guide <triage|work> # the triage / work-loop procedure (so triggers can stay short)"
382
+ ].join("\n"));
383
+ break;
384
+ default:
385
+ console.error(`Unknown: svamp issue ${sub}. Try: svamp issue help`);
386
+ process.exit(1);
387
+ }
388
+ }
389
+
390
+ export { issueCommand };