@bli-cockpit/cli 0.2.237 → 0.2.239
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/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-direct.js +119 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +62 -25
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/docs.js +38 -6
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-versions.js +93 -0
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +73 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +69 -54
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +79 -51
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +24 -6
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +74 -37
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +20 -7
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg.js +45 -3
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/setup.js +13 -0
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +94 -68
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/spool/local-spool-parse.js +13 -12
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +4 -4
|
@@ -19,25 +19,23 @@
|
|
|
19
19
|
* The path itself never leaves the machine, exactly as with `cwd_hash` on the
|
|
20
20
|
* session rows.
|
|
21
21
|
*/
|
|
22
|
-
import { noteDoctorRequest } from "./sync-doctor-request.js";
|
|
23
22
|
import crypto from "node:crypto";
|
|
24
23
|
import { collectAgentSetup } from "./agent-setup.js";
|
|
25
24
|
import fs from "node:fs";
|
|
26
25
|
import os from "node:os";
|
|
27
26
|
import path from "node:path";
|
|
28
|
-
import { COLLECTOR_HEARTBEAT_SCHEMA_VERSION, MemoryInstallReceiptSchema,
|
|
27
|
+
import { COLLECTOR_HEARTBEAT_SCHEMA_VERSION, MemoryInstallReceiptSchema, setupReceiptGaps, } from "@bli-cockpit/telemetry-core";
|
|
29
28
|
import { readCachedSetupReceipt } from "./setup-receipt.js";
|
|
30
29
|
import { readStagingInventory } from "../disk-usage.js";
|
|
31
30
|
import { describeError } from "../health-detail.js";
|
|
32
31
|
import { shouldSuppressFleetReceipts, } from "../dev-build.js";
|
|
33
32
|
import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, } from "../local-state.js";
|
|
34
|
-
import {
|
|
33
|
+
import { postHeartbeat } from "./heartbeat-post.js";
|
|
34
|
+
import { readHeartbeatSession } from "./heartbeat-token.js";
|
|
35
35
|
import { withTokenRotationLock } from "./heartbeat-token-lock.js";
|
|
36
|
-
import { classifySyncHealthError } from "../sync-health-class.js";
|
|
37
36
|
import { readMemoryReceiptFile } from "./memory-install-receipt.js";
|
|
38
37
|
import { readMemoryHookCounts } from "./memory-hook-counts.js";
|
|
39
38
|
import { readMemoryHookPerformance } from "./memory-hook-performance.js";
|
|
40
|
-
const HEARTBEAT_TIMEOUT_MS = 5_000;
|
|
41
39
|
/**
|
|
42
40
|
* The label form of one approved root.
|
|
43
41
|
*
|
|
@@ -226,23 +224,8 @@ export async function sendCollectorHeartbeatBestEffort(options) {
|
|
|
226
224
|
return withTokenRotationLock(options.homeDir, options.lock, false, () => sendCollectorHeartbeatLocked(options));
|
|
227
225
|
}
|
|
228
226
|
async function sendCollectorHeartbeatLocked(options) {
|
|
229
|
-
|
|
230
|
-
// device row, so an agent's worktree could keep a genuinely dead machine
|
|
231
|
-
// looking alive to the BLI-3550 reader.
|
|
232
|
-
const suppression = shouldSuppressFleetReceipts({
|
|
233
|
-
dashboardUrl: options.dashboardUrl,
|
|
234
|
-
...(options.devBuildProbe ? { probe: options.devBuildProbe } : {}),
|
|
235
|
-
});
|
|
236
|
-
if (suppression.suppressed) {
|
|
237
|
-
console.error("[heartbeat] dev build; this tick is not checking in with the fleet", JSON.stringify({
|
|
238
|
-
reason: "receipt_suppressed:dev_build",
|
|
239
|
-
detected_by: suppression.reason,
|
|
240
|
-
root_count: options.roots.length,
|
|
241
|
-
sync_status: options.facts.status,
|
|
242
|
-
fix: "set COCKPIT_DEV=0 to heartbeat from a checkout deliberately",
|
|
243
|
-
}));
|
|
227
|
+
if (isDevBuildSuppressed(options))
|
|
244
228
|
return false;
|
|
245
|
-
}
|
|
246
229
|
// BLI-4019. Whether this machine may knock at all, and the two log lines
|
|
247
230
|
// that go with it, live in `heartbeat-token.ts` — the check-in goes out even
|
|
248
231
|
// when the local copy says `expired`, because that is the door the server
|
|
@@ -255,12 +238,46 @@ async function sendCollectorHeartbeatLocked(options) {
|
|
|
255
238
|
...(options.homeDir ? { homeDir: options.homeDir } : {}),
|
|
256
239
|
...(options.now ? { now: options.now } : {}),
|
|
257
240
|
}).catch(() => ({ receipt: null, reason: "receipt_read_threw" }));
|
|
258
|
-
// BLI-3731. Read the CACHE, never re-probe: computing the receipt costs five
|
|
259
|
-
// file reads and a `--print-config` spawn, and these facts change about
|
|
260
|
-
// monthly. `cockpit memory install`, `onboard` and `doctor` refresh it; a
|
|
261
|
-
// tick only carries it. Both branches say something, because "the ops board
|
|
262
|
-
// shows no receipt for this machine" has two very different causes.
|
|
263
241
|
const setupReceipt = await readCachedSetupReceipt(options.homeDir, options.now ?? new Date());
|
|
242
|
+
logSetupReceiptCarried(setupReceipt);
|
|
243
|
+
const heartbeat = buildCollectorHeartbeat({
|
|
244
|
+
roots: options.roots,
|
|
245
|
+
facts: options.facts,
|
|
246
|
+
setupReceipt: setupReceipt ? { ...setupReceipt, agent_setup: await collectAgentSetup(options.homeDir ?? os.homedir()) } : null,
|
|
247
|
+
...(options.now ? { now: options.now } : {}),
|
|
248
|
+
memoryInstall: memory.receipt,
|
|
249
|
+
});
|
|
250
|
+
return postHeartbeat(options, session.device_token, heartbeat, memory);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* BLI-3554. A checkout's heartbeat would move `last_seen_at` on a real fleet
|
|
254
|
+
* device row, so an agent's worktree could keep a genuinely dead machine
|
|
255
|
+
* looking alive to the BLI-3550 reader.
|
|
256
|
+
*/
|
|
257
|
+
function isDevBuildSuppressed(options) {
|
|
258
|
+
const suppression = shouldSuppressFleetReceipts({
|
|
259
|
+
dashboardUrl: options.dashboardUrl,
|
|
260
|
+
...(options.devBuildProbe ? { probe: options.devBuildProbe } : {}),
|
|
261
|
+
});
|
|
262
|
+
if (!suppression.suppressed)
|
|
263
|
+
return false;
|
|
264
|
+
console.error("[heartbeat] dev build; this tick is not checking in with the fleet", JSON.stringify({
|
|
265
|
+
reason: "receipt_suppressed:dev_build",
|
|
266
|
+
detected_by: suppression.reason,
|
|
267
|
+
root_count: options.roots.length,
|
|
268
|
+
sync_status: options.facts.status,
|
|
269
|
+
fix: "set COCKPIT_DEV=0 to heartbeat from a checkout deliberately",
|
|
270
|
+
}));
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* BLI-3731. Read the CACHE, never re-probe: computing the receipt costs five
|
|
275
|
+
* file reads and a `--print-config` spawn, and these facts change about
|
|
276
|
+
* monthly. `cockpit memory install`, `onboard` and `doctor` refresh it; a
|
|
277
|
+
* tick only carries it. Both branches say something, because "the ops board
|
|
278
|
+
* shows no receipt for this machine" has two very different causes.
|
|
279
|
+
*/
|
|
280
|
+
function logSetupReceiptCarried(setupReceipt) {
|
|
264
281
|
console.error(setupReceipt
|
|
265
282
|
? "[heartbeat] carrying this machine's setup receipt"
|
|
266
283
|
: "[heartbeat] no usable setup receipt to carry; the ops board keeps the last one it has", JSON.stringify(setupReceipt
|
|
@@ -273,103 +290,4 @@ async function sendCollectorHeartbeatLocked(options) {
|
|
|
273
290
|
reason: "setup_receipt_absent_or_stale",
|
|
274
291
|
next_action: "run `cockpit doctor` to refresh it",
|
|
275
292
|
}));
|
|
276
|
-
const heartbeat = buildCollectorHeartbeat({
|
|
277
|
-
roots: options.roots,
|
|
278
|
-
facts: options.facts,
|
|
279
|
-
setupReceipt: setupReceipt ? { ...setupReceipt, agent_setup: await collectAgentSetup(options.homeDir ?? os.homedir()) } : null,
|
|
280
|
-
...(options.now ? { now: options.now } : {}),
|
|
281
|
-
memoryInstall: memory.receipt,
|
|
282
|
-
});
|
|
283
|
-
const controller = new AbortController();
|
|
284
|
-
const timeout = setTimeout(() => controller.abort(), HEARTBEAT_TIMEOUT_MS);
|
|
285
|
-
try {
|
|
286
|
-
const response = await options.io.fetch(`${options.dashboardUrl}/api/ambient/heartbeat`, {
|
|
287
|
-
method: "POST",
|
|
288
|
-
headers: {
|
|
289
|
-
"Content-Type": "application/json",
|
|
290
|
-
Authorization: `Bearer ${session.device_token}`,
|
|
291
|
-
// BLI-17994. "I store a renewed token if you send one": the dashboard
|
|
292
|
-
// only rotates for a collector that says so.
|
|
293
|
-
[TOKEN_ROTATION_HEADER]: "1",
|
|
294
|
-
},
|
|
295
|
-
body: JSON.stringify(heartbeat),
|
|
296
|
-
signal: controller.signal,
|
|
297
|
-
});
|
|
298
|
-
noteHeartbeatRefusal(response.ok ? null : response.status);
|
|
299
|
-
if (!response.ok) {
|
|
300
|
-
// A heartbeat is not retried, so the reason has to be said once, here.
|
|
301
|
-
// BLI-4019: the CLASS comes from `sync-health-class.ts`, the one place
|
|
302
|
-
// that decides what a failure is — a 401/403 is `auth_failed` and
|
|
303
|
-
// anything else is not, read off the observed status and never off the
|
|
304
|
-
// words in a message (BLI-3551's rule).
|
|
305
|
-
// BLI-17994. The door names why it refused (`device_token_expired`,
|
|
306
|
-
// `device_token_replaced`) in a `reason` field; say it here rather than
|
|
307
|
-
// leaving a bare status.
|
|
308
|
-
const refusal = await Promise.resolve().then(() => response.clone().json()).catch(() => null);
|
|
309
|
-
console.error("[heartbeat] the dashboard refused this tick's heartbeat", JSON.stringify({
|
|
310
|
-
reason: "heartbeat_rejected",
|
|
311
|
-
health_class: classifySyncHealthError({ httpStatus: response.status }),
|
|
312
|
-
http_status: response.status,
|
|
313
|
-
root_count: heartbeat.roots.length,
|
|
314
|
-
...(typeof refusal?.["reason"] === "string" ? { server_reason: refusal["reason"] } : {}),
|
|
315
|
-
...(response.status === 401 || response.status === 403
|
|
316
|
-
? {
|
|
317
|
-
next_action: "this token expired, was replaced or was revoked: run `cockpit doctor` to pair this machine again",
|
|
318
|
-
}
|
|
319
|
-
: {}),
|
|
320
|
-
}));
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
// The reply carries `token_expires_at` (and a `pending_device_token` when
|
|
324
|
-
// the token was near its end); the sibling records both, so this machine's
|
|
325
|
-
// own copy follows the server rather than drifting.
|
|
326
|
-
const reply = await response.clone().json().catch(() => null);
|
|
327
|
-
noteDoctorRequest(reply?.device_action);
|
|
328
|
-
// BLI-17994. Store a pending successor if the door handed one out (the
|
|
329
|
-
// date does not move: a pending token has not been used), then the date the
|
|
330
|
-
// door reports, which moves only when this request promoted a token.
|
|
331
|
-
await recordHeartbeatPendingToken(options.homeDir, reply);
|
|
332
|
-
await recordHeartbeatTokenExpiry(options.homeDir, response);
|
|
333
|
-
console.error("[heartbeat] checked in", JSON.stringify({
|
|
334
|
-
reason: "heartbeat_recorded",
|
|
335
|
-
http_status: response.status,
|
|
336
|
-
root_count: heartbeat.roots.length,
|
|
337
|
-
sync_status: heartbeat.last_sync_status,
|
|
338
|
-
sync_reason: heartbeat.last_sync_reason ?? null,
|
|
339
|
-
sessions_observed: heartbeat.sessions_observed ?? null,
|
|
340
|
-
sessions_outside_root: heartbeat.sessions_outside_root ?? null,
|
|
341
|
-
sessions_new_this_tick: heartbeat.sessions_new_this_tick ?? null,
|
|
342
|
-
sessions_pending_upload: heartbeat.sessions_pending_upload ?? null,
|
|
343
|
-
// BLI-3729. Both branches: a tick that reports no receipt says WHY,
|
|
344
|
-
// and a tick that reports one says whether memory is fully on. "Is BLI
|
|
345
|
-
// Memory switched on for this machine?" is now answerable from
|
|
346
|
-
// sync.err.log alone.
|
|
347
|
-
memory_receipt: memory.reason,
|
|
348
|
-
memory_gaps: memory.receipt ? memoryInstallGaps(memory.receipt) : null,
|
|
349
|
-
// BLI-3788. The hooks are registered (above) AND they either worked or
|
|
350
|
-
// did not (here). A prompt hook that misses its deadline prints
|
|
351
|
-
// nothing to the person, so these counts are the only trace one leaves
|
|
352
|
-
// on this machine; `null` means no counter file, never zero misses.
|
|
353
|
-
memory_hook_runs_24h: memory.receipt?.hook_runs_24h ?? null,
|
|
354
|
-
memory_hook_timeouts_24h: memory.receipt?.hook_timeouts_24h ?? null,
|
|
355
|
-
memory_hook_stats: memory.receipt?.hook_stats_reason ?? null,
|
|
356
|
-
// BLI-3884. Which route answered those recalls. `null` on a machine
|
|
357
|
-
// whose memory-mcp does not name one yet — never zero, which would
|
|
358
|
-
// read as "the daemon answered none".
|
|
359
|
-
memory_hook_via_daemon_24h: memory.receipt?.hook_via_daemon_24h ?? null,
|
|
360
|
-
memory_hook_via_direct_24h: memory.receipt?.hook_via_direct_24h ?? null,
|
|
361
|
-
}));
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
console.error("[heartbeat] could not reach the dashboard this tick", JSON.stringify({
|
|
366
|
-
reason: "heartbeat_transport_failed",
|
|
367
|
-
root_count: heartbeat.roots.length,
|
|
368
|
-
...describeError(error),
|
|
369
|
-
}));
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
finally {
|
|
373
|
-
clearTimeout(timeout);
|
|
374
|
-
}
|
|
375
293
|
}
|
|
@@ -102,21 +102,7 @@ export async function runUpdate(command, io) {
|
|
|
102
102
|
addInstallEvent(installEvents, "npm_install", "fail", error.eacces
|
|
103
103
|
? "npm_install_eacces"
|
|
104
104
|
: "npm_install_failed");
|
|
105
|
-
|
|
106
|
-
writeLine(io.stdout, JSON.stringify({
|
|
107
|
-
status: "blocked",
|
|
108
|
-
step: "npm_install",
|
|
109
|
-
command: `npm ${selfUpdateInstallArgs().join(" ")}`,
|
|
110
|
-
exit_code: error.result.code,
|
|
111
|
-
}, null, 2));
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
writeLine(io.stderr, "BLOCKED: npm install failed; Tower CLI was not refreshed.");
|
|
115
|
-
if (error.eacces) {
|
|
116
|
-
writeLine(io.stderr, "Fix Homebrew npm ownership once: sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit");
|
|
117
|
-
writeLine(io.stderr, "Do not use `sudo npm i -g`; it makes the ownership problem come back.");
|
|
118
|
-
}
|
|
119
|
-
}
|
|
105
|
+
writeSelfUpdateBlocked(command, io, error);
|
|
120
106
|
return finish(error.result.code || 1);
|
|
121
107
|
}
|
|
122
108
|
addInstallEvent(installEvents, "npm_install", "ok");
|
|
@@ -131,6 +117,22 @@ export async function runUpdate(command, io) {
|
|
|
131
117
|
addInstallEvent(installEvents, "onboard_rerun", onboard.code === 0 ? "ok" : "fail", onboard.code === 0 ? undefined : updateOnboardFailureCode(onboard));
|
|
132
118
|
return finish(onboard.code);
|
|
133
119
|
}
|
|
120
|
+
function writeSelfUpdateBlocked(command, io, error) {
|
|
121
|
+
if (command.json) {
|
|
122
|
+
writeLine(io.stdout, JSON.stringify({
|
|
123
|
+
status: "blocked",
|
|
124
|
+
step: "npm_install",
|
|
125
|
+
command: `npm ${selfUpdateInstallArgs().join(" ")}`,
|
|
126
|
+
exit_code: error.result.code,
|
|
127
|
+
}, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
writeLine(io.stderr, "BLOCKED: npm install failed; Tower CLI was not refreshed.");
|
|
131
|
+
if (error.eacces) {
|
|
132
|
+
writeLine(io.stderr, "Fix Homebrew npm ownership once: sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit");
|
|
133
|
+
writeLine(io.stderr, "Do not use `sudo npm i -g`; it makes the ownership problem come back.");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
134
136
|
export async function runSelfUpdate(io, options = {}) {
|
|
135
137
|
const exec = io.exec ?? defaultExec();
|
|
136
138
|
const tag = options.tag ?? "latest";
|
|
@@ -91,40 +91,39 @@ export async function showIssue(command, door) {
|
|
|
91
91
|
results.push(await fetchOneIssue(door, ref));
|
|
92
92
|
// BLI-10450: one directory call for the whole batch, never one per issue.
|
|
93
93
|
const directory = await resolveAssigneeDirectory(door, results.filter((result) => result.ok).map((result) => result.issue.assignee_id));
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
94
|
+
if (refs.length === 1)
|
|
95
|
+
return reportSingleIssue(door, results[0], directory);
|
|
96
|
+
return reportSeveralIssues(door, results, directory);
|
|
97
|
+
}
|
|
98
|
+
/** The JSON body for one issue that was read: the issue, its comments and any Linear link. */
|
|
99
|
+
function readIssueJson(result, directory) {
|
|
100
|
+
return {
|
|
101
|
+
issue: withSizeLabel(withAssigneeIdentity(result.issue, directory)),
|
|
102
|
+
comments: result.comments,
|
|
103
|
+
...(result.linearTicketId ? { linear_ticket_id: result.linearTicketId } : {}),
|
|
104
|
+
...(result.commentsUnreadReason ? { comments_unread_reason: result.commentsUnreadReason } : {}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// A single ref keeps the pre-BLI-10450 shape and failure path exactly.
|
|
108
|
+
function reportSingleIssue(door, result, directory) {
|
|
109
|
+
if (!result || !result.ok) {
|
|
110
|
+
return failAgentDoor(door, TAG, result?.reason ?? "issue_not_found_or_unreadable", result?.detail ?? "That issue does not exist, or you cannot read it.");
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
if (door.json)
|
|
113
|
+
return emitAgentDoor(door, { ok: true, ...readIssueJson(result, directory) });
|
|
114
|
+
renderOneIssue(door, result, directory);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
// Several refs: `{ ok, issues: [...] }`, one entry per ref in the order
|
|
118
|
+
// given, each carrying `ref` so a caller can match an answer back to what
|
|
119
|
+
// it asked for. One bad id never loses the rest of the batch.
|
|
120
|
+
function reportSeveralIssues(door, results, directory) {
|
|
115
121
|
const failed = results.filter((result) => !result.ok);
|
|
116
122
|
if (door.json) {
|
|
117
123
|
return emitAgentDoor(door, {
|
|
118
124
|
ok: failed.length === 0,
|
|
119
125
|
issues: results.map((result) => result.ok
|
|
120
|
-
? {
|
|
121
|
-
ref: result.ref,
|
|
122
|
-
ok: true,
|
|
123
|
-
issue: withSizeLabel(withAssigneeIdentity(result.issue, directory)),
|
|
124
|
-
comments: result.comments,
|
|
125
|
-
...(result.linearTicketId ? { linear_ticket_id: result.linearTicketId } : {}),
|
|
126
|
-
...(result.commentsUnreadReason ? { comments_unread_reason: result.commentsUnreadReason } : {}),
|
|
127
|
-
}
|
|
126
|
+
? { ref: result.ref, ok: true, ...readIssueJson(result, directory) }
|
|
128
127
|
: { ref: result.ref, ok: false, error: result.reason, detail: result.detail }),
|
|
129
128
|
}, failed.length === 0 ? 0 : 1);
|
|
130
129
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* `cockpit ticket versions <id>` and `cockpit ticket restore <id> --version <n>`
|
|
4
|
+
* (BLI-21300): the saved descriptions of one ticket, and the write that puts an
|
|
5
|
+
* older one back as a NEW version. Same doors the browser uses
|
|
6
|
+
* (`GET .../versions`, `POST .../versions/restore`).
|
|
7
|
+
*
|
|
8
|
+
* `restore` takes the version NUMBER the person read in `versions`, looks up
|
|
9
|
+
* that version's id in the same list, and sends the id: the door never guesses
|
|
10
|
+
* "the previous one". A number the list does not carry is refused here with
|
|
11
|
+
* the numbers that do exist, before anything is written.
|
|
12
|
+
*/
|
|
13
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
14
|
+
import { writeLine } from "./cli-io.js";
|
|
15
|
+
import { READ_DEADLINE_MS, TAG, WRITE_DEADLINE_MS } from "./issue-contracts.js";
|
|
16
|
+
export async function listIssueVersions(command, door) {
|
|
17
|
+
const ref = command.issueRef ?? "";
|
|
18
|
+
const query = command.limit === undefined ? "" : `?limit=${command.limit}`;
|
|
19
|
+
const answer = await askAgentDoor(door, {
|
|
20
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}/versions${query}`,
|
|
21
|
+
method: "GET",
|
|
22
|
+
label: "issue versions",
|
|
23
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
24
|
+
});
|
|
25
|
+
if (!answer.ok)
|
|
26
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
27
|
+
const versions = answer.body.versions ?? [];
|
|
28
|
+
console.error(`${TAG} versions ${JSON.stringify({ count: versions.length })}`);
|
|
29
|
+
if (door.json)
|
|
30
|
+
return emitAgentDoor(door, { ok: true, versions });
|
|
31
|
+
if (versions.length === 0) {
|
|
32
|
+
writeLine(door.io.stdout, `No saved description versions for ${ref}.`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
for (const row of versions) {
|
|
36
|
+
const from = row.restored_from_version ? ` (restored from ${row.restored_from_version})` : "";
|
|
37
|
+
writeLine(door.io.stdout, `v${row.version} ${row.id} ${row.created_at} ${row.source}${from} ${row.markdown.length} chars`);
|
|
38
|
+
}
|
|
39
|
+
writeLine(door.io.stdout, "");
|
|
40
|
+
writeLine(door.io.stdout, `${versions.length} version(s), newest first. Restore one with: cockpit ticket restore ${ref} --version <n>`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
export async function restoreIssueVersion(command, door) {
|
|
44
|
+
const ref = command.issueRef ?? "";
|
|
45
|
+
const wanted = command.versionNumber;
|
|
46
|
+
const listed = await askAgentDoor(door, {
|
|
47
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}/versions?limit=500`,
|
|
48
|
+
method: "GET",
|
|
49
|
+
label: "issue versions",
|
|
50
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
51
|
+
});
|
|
52
|
+
if (!listed.ok)
|
|
53
|
+
return failAgentDoor(door, TAG, listed.reason, listed.detail);
|
|
54
|
+
const versions = listed.body.versions ?? [];
|
|
55
|
+
const chosen = versions.find(row => row.version === wanted);
|
|
56
|
+
if (!chosen) {
|
|
57
|
+
const have = versions.map(row => row.version).join(", ") || "none";
|
|
58
|
+
console.error(`${TAG} restore refused ${JSON.stringify({ reason: "version_not_found", requested: wanted ?? null, available: versions.length })}`);
|
|
59
|
+
return failAgentDoor(door, TAG, "version_not_found", `${ref} has no version ${wanted}. Versions: ${have}.`);
|
|
60
|
+
}
|
|
61
|
+
const requestId = command.requestId ?? randomUUID();
|
|
62
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision, version: chosen.version })}`);
|
|
63
|
+
let answer;
|
|
64
|
+
try {
|
|
65
|
+
answer = await askAgentDoor(door, {
|
|
66
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}/versions/restore`,
|
|
67
|
+
method: "POST",
|
|
68
|
+
label: "issue restore",
|
|
69
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
70
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision, version_id: chosen.id },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
console.error(`${TAG} response lost`, JSON.stringify({ reason: "mutation_unknown" }));
|
|
75
|
+
writeLine(door.io.stderr, `${TAG} check local commit with cockpit ticket mutation-status ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
76
|
+
return failAgentDoor(door, TAG, "mutation_unknown", "Result unknown. Check this request ID before retrying with identical input.");
|
|
77
|
+
}
|
|
78
|
+
if (!answer.ok) {
|
|
79
|
+
writeLine(door.io.stderr, `${TAG} check local commit with cockpit ticket mutation-status ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
80
|
+
const mutation = answer.body?.mutation ?? { request_id: requestId, outcome: "unknown_check_again" };
|
|
81
|
+
if (door.json)
|
|
82
|
+
return emitAgentDoor(door, { ok: false, error: answer.reason, detail: answer.detail, mutation }, 1);
|
|
83
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
84
|
+
}
|
|
85
|
+
const body = answer.body;
|
|
86
|
+
const newVersion = body.mutation?.description_version;
|
|
87
|
+
writeLine(door.io.stderr, `${TAG} restored ${JSON.stringify({ restored_version: chosen.version, new_version: newVersion ?? null })}`);
|
|
88
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
89
|
+
if (door.json)
|
|
90
|
+
return emitAgentDoor(door, { ok: true, issue: body.issue, mutation: body.mutation });
|
|
91
|
+
writeLine(door.io.stdout, `Restored version ${chosen.version} of ${ref} as new version ${newVersion ?? "?"}.`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
/**
|
|
2
3
|
* The verbs that CHANGE something: `cockpit issue create|update|move|comment`
|
|
3
4
|
* (BLI-3716). The reads and the router live in `issue.ts`, the shared shapes
|
|
@@ -19,12 +20,14 @@ export async function createIssue(command, door) {
|
|
|
19
20
|
const body = await readBody(command, door.io);
|
|
20
21
|
if (!body.ok)
|
|
21
22
|
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
22
|
-
const
|
|
23
|
+
const requestId = command.requestId ?? randomUUID();
|
|
24
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
25
|
+
const answer = await askTicketWrite(door, {
|
|
23
26
|
path: "/api/work/issues",
|
|
24
27
|
method: "POST",
|
|
25
28
|
label: "issue create",
|
|
26
29
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
27
|
-
body: {
|
|
30
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision,
|
|
28
31
|
title: command.title,
|
|
29
32
|
...(body.text ? { description: body.text } : {}),
|
|
30
33
|
...(refs.projectId ? { project_id: refs.projectId } : {}),
|
|
@@ -35,11 +38,12 @@ export async function createIssue(command, door) {
|
|
|
35
38
|
},
|
|
36
39
|
});
|
|
37
40
|
if (!answer.ok)
|
|
38
|
-
return
|
|
41
|
+
return refuseWrite(door, answer, requestId);
|
|
39
42
|
const issue = maybeWithSizeLabel(answer.body.issue);
|
|
40
43
|
writeLine(door.io.stderr, `${TAG} created ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null, description_bytes: Buffer.byteLength(body.text, "utf8") })}`);
|
|
44
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
41
45
|
if (door.json)
|
|
42
|
-
return emitAgentDoor(door, { ok: true, issue });
|
|
46
|
+
return emitAgentDoor(door, { ok: true, issue, mutation: answer.body.mutation });
|
|
43
47
|
writeLine(door.io.stdout, `Created ${issue?.identifier ?? ""} — ${issue?.title ?? ""} (${issue?.id ?? ""}).`);
|
|
44
48
|
return 0;
|
|
45
49
|
}
|
|
@@ -55,21 +59,17 @@ export async function updateIssue(command, door) {
|
|
|
55
59
|
return failAgentDoor(door, TAG, body.reason, body.detail);
|
|
56
60
|
description = body.text;
|
|
57
61
|
}
|
|
58
|
-
if (description
|
|
59
|
-
&& command.title === undefined
|
|
60
|
-
&& command.priority === undefined
|
|
61
|
-
&& command.estimate === undefined
|
|
62
|
-
&& command.assignee === undefined
|
|
63
|
-
&& refs.projectId === undefined
|
|
64
|
-
&& refs.parentId === undefined) {
|
|
62
|
+
if (!changesAnything(command, description, refs)) {
|
|
65
63
|
return failAgentDoor(door, TAG, "invalid_body", "issue update needs at least one of --title, --priority, --size, --assignee, --project, --parent, or a description on --body-stdin/--file.");
|
|
66
64
|
}
|
|
67
|
-
const
|
|
65
|
+
const requestId = command.requestId ?? randomUUID();
|
|
66
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
67
|
+
const answer = await askTicketWrite(door, {
|
|
68
68
|
path: `/api/work/issues/${encodeURIComponent(ref)}`,
|
|
69
69
|
method: "PATCH",
|
|
70
70
|
label: "issue update",
|
|
71
71
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
72
|
-
body: {
|
|
72
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision,
|
|
73
73
|
...(command.title !== undefined ? { title: command.title } : {}),
|
|
74
74
|
...(description !== undefined ? { description } : {}),
|
|
75
75
|
...(command.priority === undefined ? {} : { priority: command.priority }),
|
|
@@ -80,29 +80,33 @@ export async function updateIssue(command, door) {
|
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
82
|
if (!answer.ok)
|
|
83
|
-
return
|
|
83
|
+
return refuseWrite(door, answer, requestId);
|
|
84
84
|
const issue = maybeWithSizeLabel(answer.body.issue);
|
|
85
85
|
writeLine(door.io.stderr, `${TAG} updated ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null })}`);
|
|
86
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
86
87
|
if (door.json)
|
|
87
|
-
return emitAgentDoor(door, { ok: true, issue });
|
|
88
|
+
return emitAgentDoor(door, { ok: true, issue, mutation: answer.body.mutation });
|
|
88
89
|
writeLine(door.io.stdout, `Updated ${issue?.identifier ?? ref} — ${issue?.title ?? ""}.`);
|
|
89
90
|
return 0;
|
|
90
91
|
}
|
|
91
92
|
export async function moveIssue(command, door) {
|
|
92
93
|
const ref = command.issueRef ?? "";
|
|
93
|
-
const
|
|
94
|
+
const requestId = command.requestId ?? randomUUID();
|
|
95
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
96
|
+
const answer = await askTicketWrite(door, {
|
|
94
97
|
path: `/api/work/issues/${encodeURIComponent(ref)}/state`,
|
|
95
98
|
method: "POST",
|
|
96
99
|
label: "issue move",
|
|
97
100
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
98
|
-
body: { state: command.moveState },
|
|
101
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision, state: command.moveState },
|
|
99
102
|
});
|
|
100
103
|
if (!answer.ok)
|
|
101
|
-
return
|
|
104
|
+
return refuseWrite(door, answer, requestId);
|
|
102
105
|
const issue = answer.body.issue;
|
|
103
106
|
writeLine(door.io.stderr, `${TAG} moved ${JSON.stringify({ issue_id: issue?.id ?? null, identifier: issue?.identifier ?? null, to_state: command.moveState })}`);
|
|
107
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
104
108
|
if (door.json)
|
|
105
|
-
return emitAgentDoor(door, { ok: true, issue });
|
|
109
|
+
return emitAgentDoor(door, { ok: true, issue, mutation: answer.body.mutation });
|
|
106
110
|
writeLine(door.io.stdout, `${issue?.identifier ?? ref} is now ${issue?.state ?? command.moveState}.`);
|
|
107
111
|
return 0;
|
|
108
112
|
}
|
|
@@ -114,19 +118,62 @@ export async function commentIssue(command, door) {
|
|
|
114
118
|
if (body.text.trim() === "") {
|
|
115
119
|
return failAgentDoor(door, TAG, "invalid_body", 'A comment is never taken on the command line — pipe it in: `echo "..." | cockpit issue comment BLI-3654`.');
|
|
116
120
|
}
|
|
117
|
-
const
|
|
121
|
+
const requestId = command.requestId ?? randomUUID();
|
|
122
|
+
writeLine(door.io.stderr, `${TAG} request ${JSON.stringify({ request_id: requestId, expected_revision: command.expectedRevision })}`);
|
|
123
|
+
const answer = await askTicketWrite(door, {
|
|
118
124
|
path: `/api/work/issues/${encodeURIComponent(ref)}/comments`,
|
|
119
125
|
method: "POST",
|
|
120
126
|
label: "issue comment",
|
|
121
127
|
timeoutMs: WRITE_DEADLINE_MS,
|
|
122
|
-
body: { body_markdown: body.text },
|
|
128
|
+
body: { request_id: requestId, expected_revision: command.expectedRevision, body_markdown: body.text },
|
|
123
129
|
});
|
|
124
130
|
if (!answer.ok)
|
|
125
|
-
return
|
|
131
|
+
return refuseWrite(door, answer, requestId);
|
|
126
132
|
const comment = answer.body.comment;
|
|
127
133
|
writeLine(door.io.stderr, `${TAG} commented ${JSON.stringify({ comment_id: comment?.id ?? null, byte_size: Buffer.byteLength(body.text, "utf8") })}`);
|
|
134
|
+
writeLine(door.io.stderr, `${TAG} local commit receipt ${JSON.stringify(answer.body.mutation ?? { outcome: "unknown_check_again", request_id: requestId })}`);
|
|
128
135
|
if (door.json)
|
|
129
|
-
return emitAgentDoor(door, { ok: true, comment });
|
|
136
|
+
return emitAgentDoor(door, { ok: true, comment, mutation: answer.body.mutation });
|
|
130
137
|
writeLine(door.io.stdout, `Commented on ${ref} (${comment?.id ?? ""}).`);
|
|
131
138
|
return 0;
|
|
139
|
+
}
|
|
140
|
+
/** A truncated or missing response body cannot be called a successful ticket write. */
|
|
141
|
+
async function askTicketWrite(door, options) {
|
|
142
|
+
try {
|
|
143
|
+
const answer = await askAgentDoor(door, options);
|
|
144
|
+
if (!answer.ok)
|
|
145
|
+
return answer;
|
|
146
|
+
const body = answer.body;
|
|
147
|
+
if (body && (body.issue || body.comment))
|
|
148
|
+
return answer;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.error(`${TAG} response lost`, JSON.stringify({ reason: "mutation_unknown" }));
|
|
152
|
+
}
|
|
153
|
+
const requestId = options.body.request_id;
|
|
154
|
+
return { ok: false, reason: "mutation_unknown", detail: "Result unknown. Check this request ID before retrying with identical input.",
|
|
155
|
+
body: { mutation: { request_id: requestId, outcome: "unknown_check_again" } } };
|
|
156
|
+
}
|
|
157
|
+
/** Did the person ask for at least one field to change? */
|
|
158
|
+
function changesAnything(command, description, refs) {
|
|
159
|
+
return description !== undefined
|
|
160
|
+
|| command.title !== undefined
|
|
161
|
+
|| command.priority !== undefined
|
|
162
|
+
|| command.estimate !== undefined
|
|
163
|
+
|| command.assignee !== undefined
|
|
164
|
+
|| refs.projectId !== undefined
|
|
165
|
+
|| refs.parentId !== undefined;
|
|
166
|
+
}
|
|
167
|
+
/** Says where the local commit stands, then fails with the door's own reason. */
|
|
168
|
+
function refuseWrite(door, answer, requestId) {
|
|
169
|
+
writeLine(door.io.stderr, `${TAG} check local commit with cockpit ticket mutation-status ${requestId}; retry unchanged input with --request-id ${requestId}.`);
|
|
170
|
+
const mutation = answer.body?.mutation ?? { request_id: requestId, outcome: "unknown_check_again" };
|
|
171
|
+
if (door.json) {
|
|
172
|
+
writeLine(door.io.stderr, `${TAG} refused ${JSON.stringify({ reason: answer.reason, request_id: requestId })}`);
|
|
173
|
+
return emitAgentDoor(door, { ok: false, error: answer.reason, detail: answer.detail, mutation }, 1);
|
|
174
|
+
}
|
|
175
|
+
const receipt = mutation;
|
|
176
|
+
if (receipt.current)
|
|
177
|
+
writeLine(door.io.stderr, `${TAG} conflict ${JSON.stringify({ request_id: requestId, outcome: receipt.outcome, current_state: receipt.current.state, current_revision: receipt.current.revision })}`);
|
|
178
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
132
179
|
}
|