@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.
- package/assets/skill/SKILL.md +15 -17
- package/assets/skill/references/artifacts-figma.md +4 -3
- package/assets/skill/references/confirmation.md +133 -0
- package/assets/skill/references/criteria.md +12 -21
- package/assets/skill/references/doc-editing.md +11 -9
- package/assets/skill/references/docs.md +4 -3
- package/assets/skill/references/memory.md +4 -2
- package/assets/skill/references/milestones.md +3 -2
- package/assets/skill/references/outcome.md +1 -14
- package/assets/skill/references/plan-runs.md +5 -0
- package/assets/skill/references/sessions.md +4 -4
- package/assets/skill/references/sprints.md +18 -17
- package/assets/skill/references/task-deps.md +4 -3
- package/assets/skill/references/tasks.md +34 -2
- package/assets/skill/references/verify.md +71 -71
- package/assets/skill/references/worktree.md +13 -7
- package/dist/cli/src/commands/crossing-disposition.js +342 -0
- package/dist/cli/src/commands/crossing-explain.js +10 -21
- package/dist/cli/src/commands/doc-delete.js +35 -23
- package/dist/cli/src/commands/doc-rebuild-source.js +16 -4
- package/dist/cli/src/commands/memory-rm.js +68 -10
- package/dist/cli/src/commands/milestone-delete.js +13 -10
- package/dist/cli/src/commands/outcome.js +0 -77
- package/dist/cli/src/commands/session-attach.js +8 -2
- package/dist/cli/src/commands/sprint-close.js +29 -9
- package/dist/cli/src/commands/sprint-delete.js +13 -10
- package/dist/cli/src/commands/sprint-show.js +3 -9
- package/dist/cli/src/commands/task-artifact-rm.js +58 -28
- package/dist/cli/src/commands/task-criteria-list.js +1 -4
- package/dist/cli/src/commands/task-criteria-set.js +3 -12
- package/dist/cli/src/commands/task-deps.js +20 -6
- package/dist/cli/src/commands/task-status.js +196 -111
- package/dist/cli/src/commands/task-update.js +129 -0
- package/dist/cli/src/commands/verify.js +22 -13
- package/dist/cli/src/commands/worktree-rm.js +35 -7
- package/dist/cli/src/index.js +60 -48
- package/dist/cli/src/lib/blocked-error.js +183 -0
- package/dist/cli/src/lib/bound-task.js +32 -0
- package/dist/cli/src/lib/confirmation.js +119 -0
- package/dist/cli/src/lib/hook-runner.js +23 -11
- package/dist/cli/src/lib/open-crossings.js +6 -6
- package/dist/shared/src/referent-kind.js +31 -1
- package/dist/shared/src/security-scan.js +125 -0
- package/package.json +1 -1
- package/assets/skill/references/fidelity.md +0 -32
- package/dist/cli/src/commands/fidelity.js +0 -108
- 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 (
|
|
14
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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> --
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(`${
|
|
53
|
+
const res = await fetch(`${base}/api/documents/${id}`, {
|
|
42
54
|
method: 'DELETE',
|
|
43
|
-
headers
|
|
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> --
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 (!
|
|
63
|
-
|
|
64
|
-
|
|
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 {
|