@lumoai/cli 1.58.0 → 1.60.0

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.
Files changed (47) hide show
  1. package/assets/skill/SKILL.md +15 -17
  2. package/assets/skill/references/artifacts-figma.md +4 -3
  3. package/assets/skill/references/confirmation.md +133 -0
  4. package/assets/skill/references/criteria.md +12 -21
  5. package/assets/skill/references/doc-editing.md +11 -9
  6. package/assets/skill/references/docs.md +4 -3
  7. package/assets/skill/references/memory.md +4 -2
  8. package/assets/skill/references/milestones.md +3 -2
  9. package/assets/skill/references/outcome.md +1 -14
  10. package/assets/skill/references/plan-runs.md +5 -0
  11. package/assets/skill/references/sessions.md +4 -4
  12. package/assets/skill/references/sprints.md +18 -17
  13. package/assets/skill/references/task-deps.md +4 -3
  14. package/assets/skill/references/tasks.md +34 -2
  15. package/assets/skill/references/verify.md +71 -71
  16. package/assets/skill/references/worktree.md +13 -7
  17. package/dist/cli/src/commands/crossing-disposition.js +342 -0
  18. package/dist/cli/src/commands/crossing-explain.js +10 -21
  19. package/dist/cli/src/commands/doc-delete.js +35 -23
  20. package/dist/cli/src/commands/doc-rebuild-source.js +16 -4
  21. package/dist/cli/src/commands/memory-rm.js +68 -10
  22. package/dist/cli/src/commands/milestone-delete.js +13 -10
  23. package/dist/cli/src/commands/outcome.js +0 -77
  24. package/dist/cli/src/commands/session-attach.js +8 -2
  25. package/dist/cli/src/commands/sprint-close.js +29 -9
  26. package/dist/cli/src/commands/sprint-delete.js +13 -10
  27. package/dist/cli/src/commands/sprint-show.js +3 -9
  28. package/dist/cli/src/commands/task-artifact-rm.js +58 -28
  29. package/dist/cli/src/commands/task-criteria-list.js +1 -4
  30. package/dist/cli/src/commands/task-criteria-set.js +3 -12
  31. package/dist/cli/src/commands/task-deps.js +20 -6
  32. package/dist/cli/src/commands/task-status.js +196 -111
  33. package/dist/cli/src/commands/task-update.js +129 -0
  34. package/dist/cli/src/commands/verify.js +22 -13
  35. package/dist/cli/src/commands/worktree-rm.js +35 -7
  36. package/dist/cli/src/index.js +60 -48
  37. package/dist/cli/src/lib/blocked-error.js +183 -0
  38. package/dist/cli/src/lib/bound-task.js +32 -0
  39. package/dist/cli/src/lib/confirmation.js +119 -0
  40. package/dist/cli/src/lib/hook-runner.js +23 -11
  41. package/dist/cli/src/lib/open-crossings.js +6 -6
  42. package/dist/shared/src/referent-kind.js +31 -1
  43. package/dist/shared/src/security-scan.js +125 -0
  44. package/package.json +1 -1
  45. package/assets/skill/references/fidelity.md +0 -32
  46. package/dist/cli/src/commands/fidelity.js +0 -108
  47. package/dist/cli/src/commands/verdict.js +0 -189
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeCrossingDisposition = describeCrossingDisposition;
4
+ exports.describeRulingConfirmation = describeRulingConfirmation;
5
+ exports.crossingDisposition = crossingDisposition;
6
+ const config_1 = require("../lib/config");
7
+ const api_1 = require("../lib/api");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ const confirmation_1 = require("../lib/confirmation");
10
+ const bound_task_1 = require("../lib/bound-task");
11
+ const open_crossings_1 = require("../lib/open-crossings");
12
+ /**
13
+ * The changes[] block of the read envelope (step 1, exit 4): what ruling is
14
+ * about to be recorded, on which crossing, with everything the user needs to
15
+ * judge it — severity, category, detail, recurrence, and the agent's own
16
+ * explanations (labelled as an unverified self-report). The ⚠ lines carry
17
+ * the consequence: approving clears this crossing's block on DONE.
18
+ */
19
+ function describeCrossingDisposition(crossing, target, ctx) {
20
+ const lines = [
21
+ `Will disposition crossing ${(0, sanitize_1.sanitizeField)(crossing.id)} on ${ctx.taskIdentifier} as ${target}`,
22
+ `Disposition: ${crossing.disposition ?? 'OPEN'} → ${target}`,
23
+ ];
24
+ const detail = (0, sanitize_1.sanitizeField)(crossing.detail).replace(/\s+/g, ' ').trim();
25
+ lines.push(`[${crossing.severity}] ${(0, sanitize_1.sanitizeField)(crossing.category)}${detail ? ` — ${detail}` : ''}`);
26
+ if ((crossing.occurrenceCount ?? 1) > 1) {
27
+ lines.push(`Seen ×${crossing.occurrenceCount} while open`);
28
+ }
29
+ const explanations = crossing.explanations ?? [];
30
+ if (explanations.length === 0) {
31
+ lines.push('Agent explanations: none recorded');
32
+ }
33
+ else {
34
+ lines.push(`Agent explanations (${explanations.length}, agent self-report · unverified):`);
35
+ for (const e of explanations) {
36
+ lines.push(` - ${(0, sanitize_1.sanitizeField)(e.note).replace(/\s+/g, ' ').trim()}`);
37
+ }
38
+ }
39
+ if (ctx.note)
40
+ lines.push(`Note to record: ${(0, sanitize_1.sanitizeField)(ctx.note)}`);
41
+ if (crossing.advisory) {
42
+ lines.push('Advisory crossing — it never blocked DONE; the ruling is for the record only');
43
+ }
44
+ else {
45
+ lines.push(`⚠ Approving clears this crossing's block on moving ${ctx.taskIdentifier} to DONE`);
46
+ }
47
+ if (crossing.severity === 'HIGH') {
48
+ lines.push('⚠ HIGH severity — this class always needs a human decision; make sure the user has read the detail above');
49
+ }
50
+ if (crossing.reversible === false) {
51
+ lines.push('⚠ Irreversible category — the action reached outside the repo; a false-positive ruling should rest on the detail, not on the explanation alone');
52
+ }
53
+ return lines;
54
+ }
55
+ /**
56
+ * The changes[] block of the ruling envelope (step 2, exit 4): the read is
57
+ * acknowledged and recorded; what remains is the ruling itself.
58
+ */
59
+ function describeRulingConfirmation(crossing, target, ctx) {
60
+ const lines = [
61
+ `Read acknowledged for crossing ${(0, sanitize_1.sanitizeField)(crossing.id)} (shown ${ctx.readAt}, acknowledged ${ctx.readAcknowledgedAt}) — recorded with the ruling`,
62
+ `Step 2 of 2 — record the ruling: ${crossing.disposition ?? 'OPEN'} → ${target} on ${ctx.taskIdentifier}`,
63
+ ];
64
+ if (ctx.note)
65
+ lines.push(`Note to record: ${(0, sanitize_1.sanitizeField)(ctx.note)}`);
66
+ lines.push(crossing.advisory
67
+ ? 'Advisory crossing — it never blocked DONE; the ruling is for the record only'
68
+ : `⚠ Approving clears this crossing's block on moving ${ctx.taskIdentifier} to DONE`);
69
+ return lines;
70
+ }
71
+ /**
72
+ * `lumo crossing disposition <id> --false-positive | --confirmed [--note …]
73
+ * [--task LUM-N] [--receipt <token>] [--confirm-read | --confirm]` — rule on
74
+ * a boundary crossing from the terminal (LUM-769), through a THREE-step
75
+ * confirmation handshake the server enforces:
76
+ *
77
+ * 1. no step flag: read the crossing, obtain a stage-1 READ RECEIPT
78
+ * (bound to crossing, ruling, caller and the crossing's current state),
79
+ * print the read envelope (exit 4), write nothing. Its confirmCommand
80
+ * carries `--receipt <r1> --confirm-read`;
81
+ * 2. `--confirm-read --receipt <r1>`: the user confirmed they read it. The
82
+ * server verifies r1 and issues the stage-2 receipt; the CLI prints the
83
+ * ruling envelope (exit 4), write nothing. Its confirmCommand carries
84
+ * `--receipt <r2> --confirm`;
85
+ * 3. `--confirm --receipt <r2>`: the user approved the ruling. The server
86
+ * verifies r2 — a stage-1 receipt is refused with "confirm read first",
87
+ * as is anything forged, expired, foreign or stale — then writes, with
88
+ * the shown / acknowledged / confirmed timestamps on the audit row.
89
+ *
90
+ * Skipping or reordering a step is refused at the CLI before any request
91
+ * where it can be told locally (no receipt, a stage-1 receipt with --confirm,
92
+ * a stage-2 receipt with --confirm-read) and by the server otherwise. The
93
+ * agent relays each envelope and never supplies a step flag on its own.
94
+ * Server-side the ruling is stamped `channel: 'CLI'` so the audit trail keeps
95
+ * it apart from a web-panel click. What stays web-only: reverting to OPEN and
96
+ * the repository suppression rule — neither exists on this path.
97
+ */
98
+ async function crossingDisposition(crossingId, options = {}) {
99
+ if (!crossingId || crossingId.trim() === '') {
100
+ console.error('Error: a crossing id is required: lumo crossing disposition <id> --false-positive | --confirmed');
101
+ return 1;
102
+ }
103
+ const target = pickTarget(options);
104
+ if (!target) {
105
+ console.error('Error: pass exactly one of --false-positive or --confirmed (the ruling to record).');
106
+ return 1;
107
+ }
108
+ if (options.confirmRead && options.confirm) {
109
+ console.error('Error: --confirm-read and --confirm are separate steps — run them one at a time, each from the envelope the previous step printed.');
110
+ return 1;
111
+ }
112
+ const note = options.note?.trim() || undefined;
113
+ const receipt = options.receipt?.trim() || undefined;
114
+ // Local ordering checks — cheap, and they name the missing step before any
115
+ // request goes out. The server re-checks all of them.
116
+ if (options.confirm) {
117
+ if (!receipt) {
118
+ console.error('Error: --confirm requires the stage-2 read receipt (--receipt <token>) from the --confirm-read step. Run the command without any step flag first, relay the envelope, confirm the read, relay that envelope, then re-run its confirmCommand verbatim.');
119
+ return 1;
120
+ }
121
+ if (receipt.startsWith('r1.')) {
122
+ console.error('Error: read not yet acknowledged — this is the stage-1 receipt. Run the command with --confirm-read --receipt <this token> first (after the user confirms they read the envelope); only the receipt that step returns is accepted with --confirm.');
123
+ return 1;
124
+ }
125
+ }
126
+ if (options.confirmRead) {
127
+ if (!receipt) {
128
+ console.error('Error: --confirm-read requires the read receipt (--receipt <token>) from the envelope. Run the command without any step flag first and relay the envelope.');
129
+ return 1;
130
+ }
131
+ if (receipt.startsWith('r2.')) {
132
+ console.error('Error: this receipt already acknowledges the read — the next step is --confirm with this same receipt (after the user approves the ruling).');
133
+ return 1;
134
+ }
135
+ }
136
+ const creds = (0, config_1.readCredentials)();
137
+ if (!creds) {
138
+ console.error('Error: not logged in. Run `lumo auth login` first.');
139
+ return 1;
140
+ }
141
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
142
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
143
+ const headers = {
144
+ Authorization: `Bearer ${creds.token}`,
145
+ };
146
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
147
+ if (sessionId)
148
+ headers['X-Lumo-Session-Id'] = sessionId;
149
+ const bound = await (0, bound_task_1.resolveBoundTask)({
150
+ base,
151
+ headers,
152
+ explicit: options.task,
153
+ sessionId,
154
+ });
155
+ if (!bound.ok) {
156
+ console.error(bound.message);
157
+ return 1;
158
+ }
159
+ const taskId = bound.taskIdentifier;
160
+ // Read the crossing first, on every path: the envelopes need it, and a
161
+ // confirmed write against a crossing that can't be read must not go out
162
+ // (fail closed — a read hiccup never turns into a blind ruling).
163
+ const read = await readCrossing(base, headers, taskId, crossingId);
164
+ if (read.status === 'error') {
165
+ console.error(`Error: could not confirm the crossing (${read.reason}) — nothing was written.`);
166
+ return 1;
167
+ }
168
+ const crossing = read.crossing;
169
+ if (!crossing) {
170
+ console.error(`Error: crossing ${(0, sanitize_1.sanitizeField)(crossingId)} not found on ${taskId}. Ids are listed by \`lumo task status ${taskId}\`.`);
171
+ return 1;
172
+ }
173
+ if (crossing.disposition === target) {
174
+ process.stdout.write(`Crossing ${(0, sanitize_1.sanitizeField)(crossing.id)} is already ${target} — nothing to do.\n`);
175
+ return;
176
+ }
177
+ // Step 1 — read envelope.
178
+ if (!options.confirmRead && !options.confirm) {
179
+ const issued = await requestReceipt(base, headers, taskId, crossingId, {
180
+ disposition: target,
181
+ });
182
+ if (issued.status === 'error') {
183
+ console.error(`Error: could not obtain a read receipt for the crossing (${issued.reason}) — nothing was written and no envelope can be issued.`);
184
+ return 1;
185
+ }
186
+ return (0, confirmation_1.emitConfirmation)({
187
+ command: 'crossing disposition',
188
+ changes: [
189
+ ...describeCrossingDisposition(crossing, target, {
190
+ taskIdentifier: taskId,
191
+ note,
192
+ }),
193
+ `Step 1 of 2 — read receipt issued for this crossing as read above (valid until ${issued.expiresAt}; void if the crossing changes). Next: the user confirms they have READ it, then run the command below; the ruling itself is confirmed in a second envelope`,
194
+ ],
195
+ confirmFlag: '--confirm-read',
196
+ receipt: { token: issued.receipt, expiresAt: issued.expiresAt },
197
+ });
198
+ }
199
+ // Step 2 — acknowledge the read, get the stage-2 receipt, ruling envelope.
200
+ if (options.confirmRead) {
201
+ const issued = await requestReceipt(base, headers, taskId, crossingId, {
202
+ disposition: target,
203
+ acknowledge: receipt,
204
+ });
205
+ if (issued.status === 'error') {
206
+ console.error(`Error: read acknowledgement rejected (${issued.reason}) — nothing was written.`);
207
+ return 1;
208
+ }
209
+ return (0, confirmation_1.emitConfirmation)({
210
+ command: 'crossing disposition',
211
+ changes: describeRulingConfirmation(crossing, target, {
212
+ taskIdentifier: taskId,
213
+ note,
214
+ readAt: issued.readAt,
215
+ readAcknowledgedAt: issued.readAcknowledgedAt ?? issued.readAt,
216
+ }),
217
+ confirmFlag: '--confirm',
218
+ receipt: { token: issued.receipt, expiresAt: issued.expiresAt },
219
+ });
220
+ }
221
+ // Step 3 — the write.
222
+ let res;
223
+ try {
224
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/boundary-crossings/${encodeURIComponent(crossingId)}/disposition`, {
225
+ method: 'POST',
226
+ headers: { ...headers, 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({
228
+ disposition: target,
229
+ ...(note ? { dispositionNote: note } : {}),
230
+ receipt,
231
+ }),
232
+ });
233
+ }
234
+ catch (err) {
235
+ const msg = err instanceof Error ? err.message : String(err);
236
+ console.error(`Error: could not reach Lumo API (${msg})`);
237
+ return 1;
238
+ }
239
+ if (res.status === 401) {
240
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
241
+ return 1;
242
+ }
243
+ if (!res.ok) {
244
+ const errBody = (await res.json().catch(() => null));
245
+ const detail = errBody && typeof errBody.error === 'string'
246
+ ? (0, sanitize_1.sanitizeField)(errBody.error)
247
+ : '';
248
+ console.error(`Error: disposition rejected (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
249
+ return 1;
250
+ }
251
+ const outcome = (await res.json());
252
+ const url = (0, open_crossings_1.dispositionUrl)(apiUrl, creds.workspaceSlug ?? '', taskId);
253
+ process.stdout.write(`✓ Dispositioned crossing ${(0, sanitize_1.sanitizeField)(outcome.crossingId)} as ${outcome.disposition ?? 'OPEN'} (read acknowledged and ruling approved via CLI).\n` +
254
+ (crossing.advisory
255
+ ? ''
256
+ : ` This clears its block on moving ${taskId} to DONE. `) +
257
+ `The ruling is audited on the task timeline as a CLI-channel disposition with the shown / read-acknowledged / confirmed times; it can be corrected in the web panel: ${url}\n`);
258
+ return;
259
+ }
260
+ function pickTarget(options) {
261
+ if (options.falsePositive && options.confirmed)
262
+ return null;
263
+ if (options.falsePositive)
264
+ return 'FALSE_POSITIVE';
265
+ if (options.confirmed)
266
+ return 'CONFIRMED';
267
+ return null;
268
+ }
269
+ /** The handshake's server call (LUM-769): stage 1 without `acknowledge`,
270
+ * stage 2 with the stage-1 receipt in it. Fails closed — without a receipt
271
+ * no envelope is issued, because its confirmCommand could never be accepted. */
272
+ async function requestReceipt(base, headers, taskId, crossingId, body) {
273
+ let res;
274
+ try {
275
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/boundary-crossings/${encodeURIComponent(crossingId)}/disposition/receipt`, {
276
+ method: 'POST',
277
+ headers: { ...headers, 'Content-Type': 'application/json' },
278
+ body: JSON.stringify(body),
279
+ });
280
+ }
281
+ catch (err) {
282
+ return {
283
+ status: 'error',
284
+ reason: err instanceof Error ? err.message : 'network error',
285
+ };
286
+ }
287
+ if (!res.ok) {
288
+ const errBody = (await res.json().catch(() => null));
289
+ const detail = errBody && typeof errBody.error === 'string'
290
+ ? ` — ${(0, sanitize_1.sanitizeField)(errBody.error)}`
291
+ : '';
292
+ return { status: 'error', reason: `HTTP ${res.status}${detail}` };
293
+ }
294
+ let data;
295
+ try {
296
+ data = (await res.json());
297
+ }
298
+ catch {
299
+ return { status: 'error', reason: 'invalid response body' };
300
+ }
301
+ if (typeof data.receipt !== 'string' || data.receipt.length === 0) {
302
+ return { status: 'error', reason: 'no receipt in response' };
303
+ }
304
+ const str = (v) => (typeof v === 'string' ? v : '');
305
+ return {
306
+ status: 'ok',
307
+ receipt: data.receipt,
308
+ expiresAt: str(data.expiresAt),
309
+ readAt: str(data.readAt),
310
+ readAcknowledgedAt: typeof data.readAcknowledgedAt === 'string'
311
+ ? data.readAcknowledgedAt
312
+ : null,
313
+ };
314
+ }
315
+ /** One task-scoped read of the LUM-435 view; the crossing picked by id. Fails
316
+ * closed: transport / non-ok / unparseable → error, never "not found". */
317
+ async function readCrossing(base, headers, taskId, crossingId) {
318
+ let res;
319
+ try {
320
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/boundary-crossings`, { headers });
321
+ }
322
+ catch (err) {
323
+ return {
324
+ status: 'error',
325
+ reason: err instanceof Error ? err.message : 'network error',
326
+ };
327
+ }
328
+ if (res.status === 401) {
329
+ return { status: 'error', reason: 'API key invalid or revoked' };
330
+ }
331
+ if (!res.ok)
332
+ return { status: 'error', reason: `HTTP ${res.status}` };
333
+ let data;
334
+ try {
335
+ data = (await res.json());
336
+ }
337
+ catch {
338
+ return { status: 'error', reason: 'invalid response body' };
339
+ }
340
+ const rows = Array.isArray(data.crossings) ? data.crossings : [];
341
+ return { status: 'ok', crossing: rows.find(c => c.id === crossingId) ?? null };
342
+ }
@@ -4,14 +4,16 @@ exports.crossingExplain = crossingExplain;
4
4
  const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const sanitize_1 = require("../lib/sanitize");
7
+ const bound_task_1 = require("../lib/bound-task");
7
8
  /**
8
9
  * `lumo crossing explain <id> --note "…"` — append an agent self-explanation
9
10
  * ("申辩") to a boundary crossing (LUM-542).
10
11
  *
11
12
  * This is the AGENT side of the boundary-crossing review loop and the deliberate
12
13
  * inverse of dispositioning: it can only ADD an append-only note for the human
13
- * reviewer to weigh — it never clears the crossing or unblocks Done (a human
14
- * dispositions that, in the web acceptance panel). The crossing must belong to
14
+ * reviewer to weigh — it never clears the crossing or unblocks Done (the user
15
+ * rules on that: `lumo crossing disposition` through the exit-4 confirmation
16
+ * protocol, or the web acceptance panel). The crossing must belong to
15
17
  * the task this session is bound to; the binding is how the target task is
16
18
  * resolved, so run it inside a session attached via `lumo session attach`.
17
19
  */
@@ -39,24 +41,9 @@ async function crossingExplain(crossingId, options = {}) {
39
41
  headers['X-Lumo-Session-Id'] = sessionId;
40
42
  // The crossing is addressed by id, but the route is task-scoped — resolve the
41
43
  // bound task from the session so the server can verify the crossing is on it.
42
- if (!sessionId) {
43
- console.error('Error: $CLAUDE_CODE_SESSION_ID is not set — run inside a session bound via `lumo session attach <LUM-N>`.');
44
- return 1;
45
- }
46
- let bound;
47
- try {
48
- const res = await fetch(`${base}/api/sessions/${encodeURIComponent(sessionId)}`, { headers });
49
- bound = res.ok
50
- ? (await res.json())
51
- : null;
52
- }
53
- catch (err) {
54
- const msg = err instanceof Error ? err.message : String(err);
55
- console.error(`Error: could not reach Lumo API (${msg})`);
56
- return 1;
57
- }
58
- if (!bound?.taskIdentifier) {
59
- console.error('Error: this session is not bound to a task. Run `lumo session attach <LUM-N>` first.');
44
+ const bound = await (0, bound_task_1.resolveBoundTask)({ base, headers, sessionId });
45
+ if (!bound.ok) {
46
+ console.error(bound.message);
60
47
  return 1;
61
48
  }
62
49
  const taskId = bound.taskIdentifier;
@@ -88,6 +75,8 @@ async function crossingExplain(crossingId, options = {}) {
88
75
  const outcome = (await res.json());
89
76
  process.stdout.write(`✓ Recorded an explanation on crossing ${(0, sanitize_1.sanitizeField)(outcome.crossingId)}.\n` +
90
77
  ' This is an append-only note for the human reviewer — it does not clear ' +
91
- 'the crossing or unblock Done.\n');
78
+ 'the crossing or unblock Done. Once the user rules, record it with ' +
79
+ '`lumo crossing disposition <id> --false-positive | --confirmed` (exit-4 envelope) ' +
80
+ 'or in the web panel.\n');
92
81
  return;
93
82
  }
@@ -1,18 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeDocDelete = describeDocDelete;
3
4
  exports.docDelete = docDelete;
4
5
  const config_1 = require("../lib/config");
5
6
  const api_1 = require("../lib/api");
6
- const resolve_doc_1 = require("../lib/resolve-doc");
7
7
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
8
8
  const sanitize_1 = require("../lib/sanitize");
9
+ const confirmation_1 = require("../lib/confirmation");
10
+ /** The changes[] block for the confirmation envelope. */
11
+ function describeDocDelete(id, title) {
12
+ const escaped = (0, sanitize_1.sanitizeField)(title).replace(/"/g, '\\"');
13
+ return [
14
+ `Will delete document ${id} "${escaped}"`,
15
+ 'Every document nested under it is deleted with it; task bindings and shares are dropped',
16
+ ];
17
+ }
9
18
  async function docDelete(reference, opts) {
10
19
  if (!reference) {
11
- console.error('Error: missing <doc>. Usage: lumo doc delete <doc> --yes');
12
- return 1;
13
- }
14
- if (!opts.yes) {
15
- console.error('Error: Refusing to delete without --yes');
20
+ console.error('Error: missing <doc>. Usage: lumo doc delete <doc> --confirm');
16
21
  return 1;
17
22
  }
18
23
  const creds = (0, config_1.readCredentials)();
@@ -21,26 +26,33 @@ async function docDelete(reference, opts) {
21
26
  return 1;
22
27
  }
23
28
  const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
24
- // For a cuid reference we don't have the title; for a title we look up id+title at once.
25
- let id;
26
- let title = '';
27
- if ((0, resolve_doc_1.isLikelyCuid)(reference)) {
28
- id = reference;
29
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
30
+ const headers = { Authorization: `Bearer ${creds.token}` };
31
+ const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
32
+ if (!id) {
33
+ console.error(`Error: Document not found: ${reference}`);
34
+ return 1;
35
+ }
36
+ // Always fetch the doc first: the envelope needs its real title, and the
37
+ // same GET confirms it exists before anything destructive happens.
38
+ const showRes = await fetch(`${base}/api/documents/${id}`, { headers });
39
+ if (!showRes.ok) {
40
+ const text = await showRes.text();
41
+ console.error(`Error: ${showRes.status} ${showRes.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
42
+ return 1;
29
43
  }
30
- else {
31
- const fetched = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
32
- if (!fetched) {
33
- console.error(`Error: Document not found: ${reference}`);
34
- return 1;
35
- }
36
- id = fetched;
37
- // Pick up the title from the lookup payload by repeating the GET — small extra cost.
38
- // (Alternative: change lookupDocId to return the full DocLike. For now keep it simple.)
39
- title = reference;
44
+ const { document } = (await showRes.json());
45
+ const title = document.title ?? '';
46
+ // LUM-755: no --confirm → envelope, no DELETE.
47
+ if (!(0, confirmation_1.isConfirmed)(opts)) {
48
+ return (0, confirmation_1.emitConfirmation)({
49
+ command: 'doc delete',
50
+ changes: describeDocDelete(id, title),
51
+ });
40
52
  }
41
- const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}`, {
53
+ const res = await fetch(`${base}/api/documents/${id}`, {
42
54
  method: 'DELETE',
43
- headers: { Authorization: `Bearer ${creds.token}` },
55
+ headers,
44
56
  });
45
57
  if (!res.ok) {
46
58
  const text = await res.text();
@@ -5,6 +5,7 @@ const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
7
7
  const sanitize_1 = require("../lib/sanitize");
8
+ const confirmation_1 = require("../lib/confirmation");
8
9
  /**
9
10
  * `lumo doc rebuild-source <doc>` (LUM-446).
10
11
  *
@@ -13,7 +14,9 @@ const sanitize_1 = require("../lib/sanitize");
13
14
  * re-enabling `doc show --raw` / `diff` / `patch` / `append`. The rebuilt source
14
15
  * is structure-guarded server-side: any table/tr/heading shrink is rejected 422
15
16
  * (zero silent flattening — LUM-410 口径) unless --allow-shrink is passed. A doc
16
- * that already has a source is refused 409 unless --force re-derives it.
17
+ * that already has a source is refused 409 unless --force re-derives it
18
+ * that refusal is surfaced as a confirmation envelope (LUM-755): exit 4 with
19
+ * the server's reason and a confirmCommand carrying `--force --confirm`.
17
20
  */
18
21
  async function docRebuildSource(reference, opts) {
19
22
  if (!reference) {
@@ -57,11 +60,20 @@ async function docRebuildSource(reference, opts) {
57
60
  if (!res.ok) {
58
61
  const text = await res.text();
59
62
  const message = (0, api_1.extractErrorMessage)(text);
63
+ if (res.status === 409 && !opts.force) {
64
+ // The --force gate: nothing was written. Hand back the protocol
65
+ // envelope so the agent shows the user what --force would replace.
66
+ return (0, confirmation_1.emitConfirmation)({
67
+ command: 'doc rebuild-source',
68
+ changes: [
69
+ (0, sanitize_1.sanitizeField)(message),
70
+ `Will replace the existing (byte-faithful) markdown source of ${id} with a freshly serialized one — inspect it first with \`lumo doc show ${reference} --raw\``,
71
+ ],
72
+ extraFlags: ['--force'],
73
+ });
74
+ }
60
75
  if (res.status === 409) {
61
76
  console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
62
- console.error('Hint: the doc already has a markdown source. Inspect it with ' +
63
- `\`lumo doc show ${reference} --raw\`; pass --force only if you intend ` +
64
- 'to replace it with a freshly serialized one.');
65
77
  return 1;
66
78
  }
67
79
  if (res.status === 422) {
@@ -1,15 +1,44 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.summarizeMemoryContent = summarizeMemoryContent;
4
+ exports.describeMemoryRm = describeMemoryRm;
3
5
  exports.memoryRm = memoryRm;
4
6
  const config_1 = require("../lib/config");
5
7
  const api_1 = require("../lib/api");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ const confirmation_1 = require("../lib/confirmation");
10
+ const SUMMARY_MAX = 120;
11
+ /**
12
+ * One-line summary of a memory's structured content for the envelope: the
13
+ * first string field (memory cards lead with their headline field), trimmed.
14
+ */
15
+ function summarizeMemoryContent(content) {
16
+ if (typeof content === 'string')
17
+ return truncate(content);
18
+ if (content && typeof content === 'object') {
19
+ for (const value of Object.values(content)) {
20
+ if (typeof value === 'string' && value.trim())
21
+ return truncate(value);
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function truncate(s) {
27
+ const flat = s.replace(/\s+/g, ' ').trim();
28
+ return flat.length > SUMMARY_MAX ? `${flat.slice(0, SUMMARY_MAX - 1)}…` : flat;
29
+ }
30
+ /** The changes[] block for the confirmation envelope. */
31
+ function describeMemoryRm(memory) {
32
+ const lines = [`Will hard-delete memory ${memory.id} [${memory.category}]`];
33
+ const summary = summarizeMemoryContent(memory.content);
34
+ if (summary)
35
+ lines.push(`Content: ${summary}`);
36
+ lines.push('The memory is removed for the whole team; there is no undo');
37
+ return lines.map(l => (0, sanitize_1.sanitizeField)(l));
38
+ }
6
39
  async function memoryRm(memoryId, options) {
7
40
  if (!memoryId) {
8
- console.error('Error: missing <memoryId>. Usage: lumo memory rm <memoryId> --yes');
9
- return 1;
10
- }
11
- if (!options.yes) {
12
- console.error('Error: refusing to delete without --yes. Re-run with --yes to confirm.');
41
+ console.error('Error: missing <memoryId>. Usage: lumo memory rm <memoryId> --confirm');
13
42
  return 1;
14
43
  }
15
44
  const creds = (0, config_1.readCredentials)();
@@ -19,19 +48,48 @@ async function memoryRm(memoryId, options) {
19
48
  }
20
49
  const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
21
50
  const base = (0, api_1.trimTrailingSlash)(apiUrl);
51
+ const url = `${base}/api/memories/${encodeURIComponent(memoryId)}`;
52
+ const headers = { Authorization: `Bearer ${creds.token}` };
53
+ const notFound = `Error: memory ${memoryId} not found — pass the full memory id (cuid) from \`lumo task memory list\` / \`lumo project memory list\`; truncated id prefixes are not resolved`;
54
+ // LUM-755: without --confirm, fetch the card so the envelope shows what
55
+ // would be deleted, then stop — no DELETE is sent.
56
+ if (!(0, confirmation_1.isConfirmed)(options)) {
57
+ let showRes;
58
+ try {
59
+ showRes = await fetch(url, { headers });
60
+ }
61
+ catch (err) {
62
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
63
+ return 1;
64
+ }
65
+ if (showRes.status === 401) {
66
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
67
+ return 1;
68
+ }
69
+ if (showRes.status === 404) {
70
+ console.error(notFound);
71
+ return 1;
72
+ }
73
+ if (!showRes.ok) {
74
+ console.error(`Error: memory lookup failed (HTTP ${showRes.status})`);
75
+ return 1;
76
+ }
77
+ const { memory } = (await showRes.json());
78
+ return (0, confirmation_1.emitConfirmation)({
79
+ command: 'memory rm',
80
+ changes: describeMemoryRm(memory),
81
+ });
82
+ }
22
83
  let res;
23
84
  try {
24
- res = await fetch(`${base}/api/memories/${encodeURIComponent(memoryId)}`, {
25
- method: 'DELETE',
26
- headers: { Authorization: `Bearer ${creds.token}` },
27
- });
85
+ res = await fetch(url, { method: 'DELETE', headers });
28
86
  }
29
87
  catch (err) {
30
88
  console.error(`Error: could not reach Lumo API at ${apiUrl} (${err instanceof Error ? err.message : String(err)})`);
31
89
  return 1;
32
90
  }
33
91
  if (res.status === 404) {
34
- console.error(`Error: memory ${memoryId} not found — pass the full memory id (cuid) from \`lumo task memory list\` / \`lumo project memory list\`; truncated id prefixes are not resolved`);
92
+ console.error(notFound);
35
93
  return 1;
36
94
  }
37
95
  if (res.status !== 204) {
@@ -1,17 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatDeleteRefusal = formatDeleteRefusal;
3
+ exports.describeMilestoneDelete = describeMilestoneDelete;
4
4
  exports.milestoneDelete = milestoneDelete;
5
5
  const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
7
  const resolve_1 = require("../lib/resolve");
8
8
  const sanitize_1 = require("../lib/sanitize");
9
- function formatDeleteRefusal(name, taskCount) {
10
- const head = `Refusing to delete milestone "${(0, sanitize_1.sanitizeField)(name)}" without --yes.`;
11
- const tail = `Re-run with --yes to confirm.`;
12
- if (taskCount === 0)
13
- return `${head}\n${tail}`;
14
- return `${head}\n${taskCount} tasks under it will keep their data; only milestoneId is cleared.\n${tail}`;
9
+ const confirmation_1 = require("../lib/confirmation");
10
+ /** The changes[] block for the confirmation envelope (LUM-755). */
11
+ function describeMilestoneDelete(name, taskCount) {
12
+ const lines = [`Will delete milestone "${(0, sanitize_1.sanitizeField)(name)}"`];
13
+ if (taskCount > 0)
14
+ lines.push(`${taskCount} tasks under it keep their data; only milestoneId is cleared`);
15
+ return lines;
15
16
  }
16
17
  function totalTasks(counts) {
17
18
  return counts.TODO + counts.IN_PROGRESS + counts.IN_REVIEW + counts.DONE;
@@ -59,9 +60,11 @@ async function milestoneDelete(identifier, opts) {
59
60
  const { milestone } = (await showRes.json());
60
61
  const name = resolvedName || milestone.name;
61
62
  const total = totalTasks(milestone.taskCounts);
62
- if (!opts.yes) {
63
- console.error(formatDeleteRefusal(name, total));
64
- return 1;
63
+ if (!(0, confirmation_1.isConfirmed)(opts)) {
64
+ return (0, confirmation_1.emitConfirmation)({
65
+ command: 'milestone delete',
66
+ changes: describeMilestoneDelete(name, total),
67
+ });
65
68
  }
66
69
  let res;
67
70
  try {