@bli-cockpit/cli 0.2.29 → 0.2.30

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/README.md CHANGED
@@ -13,9 +13,9 @@ added <packages> in <seconds>s
13
13
 
14
14
  ```bash
15
15
  $ cockpit do-everything
16
- Cockpit harvest onboarding
16
+ Setting up Cockpit
17
17
  Dashboard: https://bli-cockpit-dashboard.vercel.app
18
- Ticket: general ambient
18
+ Ticket: none (general work)
19
19
  What's your @buildlaunchiterate.ca email? (press enter to skip): ian@buildlaunchiterate.ca
20
20
  Signing in as ian@buildlaunchiterate.ca.
21
21
  Code sent; valid 1h, resend in 60s by rerunning this command.
@@ -28,7 +28,7 @@ What you can do:
28
28
  Code: 482913
29
29
  Signed in as ian@buildlaunchiterate.ca.
30
30
  2/5 Device paired.
31
- PASS: Cockpit collector is ready for harvest.
31
+ PASS: Cockpit is set up and collecting.
32
32
  You're live.
33
33
  Dashboard: https://bli-cockpit-dashboard.vercel.app/my-work
34
34
  Next: cockpit status
@@ -36,9 +36,15 @@ Next: cockpit status
36
36
 
37
37
  ```bash
38
38
  $ cockpit status
39
- Install: ready
40
- Auth: paired
41
- Upload state: ready
39
+ Cockpit status
40
+ Installed: yes
41
+ Signed in: yes
42
+ Ticket: none (general work)
43
+ Last collected: recently
44
+ Uploads: ready
45
+ Waiting to upload: 0
46
+ Old sessions: done
47
+ Stuck files: none
42
48
  ```
43
49
 
44
50
  The OTP proves you own an approved BLI mailbox. The JWT is used once to register this device and is never saved; the durable local credential remains the existing Cockpit device token.
@@ -47,13 +53,14 @@ The OTP proves you own an approved BLI mailbox. The JWT is used once to register
47
53
 
48
54
  On a blank Mac or Windows PC, `npm i -g @bli-cockpit/cli && cockpit do-everything` is enough to sign in, choose roots, and converge the machine. `onboard` remains the named setup subset, and the rest exist for recovery and maintenance.
49
55
 
50
- The core collection contract is the same on Apple Silicon macOS and native
51
- Windows: every eligible Codex and Claude session attributable to an approved
52
- root is collected, redacted before ingest, and either uploaded with a durable
53
- receipt or surfaced with an explicit ambiguity, skip, or retry reason. A green
54
- machine must not hide silently missing sessions. This support claim remains
55
- release-gated: the packed candidate must pass normal-user canaries on both host
56
- families before its exact version is promoted to npm `latest`.
56
+ Cockpit works the same way on an Apple Silicon Mac and on a normal Windows
57
+ PC. Every Codex and Claude session in a folder you have approved is collected,
58
+ stripped of anything sensitive before it leaves your machine, and then either
59
+ uploaded with a receipt or listed with the exact reason it was not. If a
60
+ session is missing, Cockpit says so — a green screen never hides one.
61
+
62
+ We only publish a version to npm after it has been tested end-to-end, as a
63
+ normal user, on both macOS and Windows.
57
64
 
58
65
  | Command | What it does | Why it exists / why this name |
59
66
  |---|---|---|
@@ -171,11 +178,11 @@ Local files:
171
178
 
172
179
  - `~/.config/bli-cockpit/config.json`: dashboard URL, device label, collection roots.
173
180
  - `~/.config/bli-cockpit/session.json`: paired device token and owner metadata.
174
- - `~/.local/state/bli-cockpit/spool/`: safe retry records.
181
+ - `~/.local/state/bli-cockpit/spool/`: uploads waiting to be retried.
175
182
  - `~/.local/state/bli-cockpit/spool/install-events/`: private atomic collector
176
183
  health receipts waiting for authenticated delivery. Receipts contain only
177
184
  sanitized operation metadata, never command output or transcript content.
178
- - `~/.local/state/bli-cockpit/cursors/`: upload/backfill cursors, no raw content.
185
+ - `~/.local/state/bli-cockpit/cursors/`: bookmarks for how far uploading has got, no raw content.
179
186
  - `.codex-autorunner/contextspace/active_context.md`: current work context inside a repo.
180
187
 
181
188
  Remote data:
@@ -4,7 +4,7 @@ export function shortSha(value) {
4
4
  return value ? value.slice(0, 12) : "unknown";
5
5
  }
6
6
  export function displayTicketId(ticketId) {
7
- return ticketId ?? "general ambient";
7
+ return ticketId ?? "none (general work)";
8
8
  }
9
9
  export function displayWorkLabel(status) {
10
10
  if (status.work_label && status.work_id)
@@ -12,13 +12,16 @@ export function displayWorkLabel(status) {
12
12
  return status.work_label ?? status.work_id ?? "no active work context";
13
13
  }
14
14
  function sourceFunnelLine(label, counts) {
15
- const readFailures = counts.read_failures > 0 ? `, read_failures ${counts.read_failures}` : "";
16
- return `${label} sessions: attributed ${counts.attributed}, fallback ${counts.attributed_fallback}, ambiguous ${counts.ambiguous}, unattributed ${counts.unattributed}, skipped ${counts.skipped}, stale ${counts.stale}${readFailures}`;
15
+ // Same counters, plain words. Every counter still prints, including the ones
16
+ // that are zero: a funnel that hides a bucket cannot show where sessions go.
17
+ const readFailures = counts.read_failures > 0 ? `, ${counts.read_failures} could not be read` : "";
18
+ return `${label} sessions: ${counts.attributed} matched to a repo, ${counts.attributed_fallback} by best guess, ${counts.ambiguous} unclear which repo, ${counts.unattributed} could not be matched, ${counts.stale} too old, ${counts.skipped} skipped${readFailures}`;
17
19
  }
18
20
  function attributionReportLine(summary) {
21
+ // The parenthesised value is the machine reason label and never changes.
19
22
  return summary.report_posted
20
- ? "Attribution report: recorded"
21
- : `Attribution report: skipped (${summary.report_reason})`;
23
+ ? "Session report: saved"
24
+ : `Session report: skipped (${summary.report_reason})`;
22
25
  }
23
26
  /**
24
27
  * One funnel line per source, an anomaly diagnostics line only when something
@@ -41,30 +44,31 @@ function claudeDiagnosticsLine(summary) {
41
44
  const claude = summary.claude;
42
45
  const parts = [];
43
46
  if (claude.sidecars_skipped)
44
- parts.push(`sidecars_skipped ${claude.sidecars_skipped}`);
47
+ parts.push(`${claude.sidecars_skipped} helper sessions skipped`);
45
48
  if (claude.sidecars_capped)
46
- parts.push(`sidecars_capped ${claude.sidecars_capped}`);
49
+ parts.push(`${claude.sidecars_capped} helper sessions cut short`);
47
50
  if (claude.sidecars_failed)
48
- parts.push(`sidecars_failed ${claude.sidecars_failed}`);
51
+ parts.push(`${claude.sidecars_failed} helper sessions could not be read`);
49
52
  if (claude.mains_oversized)
50
- parts.push(`mains_oversized ${claude.mains_oversized}`);
53
+ parts.push(`${claude.mains_oversized} sessions too big`);
51
54
  if (claude.oversized_lines_skipped)
52
- parts.push(`oversized_lines_skipped ${claude.oversized_lines_skipped}`);
55
+ parts.push(`${claude.oversized_lines_skipped} oversized lines skipped`);
53
56
  if (claude.project_dirs_skipped)
54
- parts.push(`project_dirs_skipped ${claude.project_dirs_skipped}`);
57
+ parts.push(`${claude.project_dirs_skipped} folders skipped`);
55
58
  if (claude.sessions_schema_drift)
56
- parts.push(`schema_drift ${claude.sessions_schema_drift}`);
59
+ parts.push(`${claude.sessions_schema_drift} sessions in an unexpected format`);
57
60
  if (claude.growth_damped)
58
- parts.push(`growth_damped ${claude.growth_damped}`);
61
+ parts.push(`${claude.growth_damped} fast-growing sessions slowed down`);
59
62
  if (claude.first_run_backfill)
60
- parts.push("first_run_backfill");
63
+ parts.push("first run, catching up on history");
61
64
  if (summary.files_deferred_byte_budget)
62
- parts.push(`deferred_byte_budget ${summary.files_deferred_byte_budget}`);
65
+ parts.push(`${summary.files_deferred_byte_budget} files held back, size limit`);
63
66
  if (summary.files_deferred_object_budget)
64
- parts.push(`deferred_object_budget ${summary.files_deferred_object_budget}`);
65
- return parts.length > 0 ? `Claude diagnostics: ${parts.join(", ")}` : null;
67
+ parts.push(`${summary.files_deferred_object_budget} files held back, file-count limit`);
68
+ return parts.length > 0 ? `Claude problems: ${parts.join(", ")}` : null;
66
69
  }
67
70
  export function rawEvidenceSyncLine(sync) {
71
+ // The reason lists themselves are machine labels and stay verbatim.
68
72
  const failures = sync.raw_evidence_failure_reasons.length > 0
69
73
  ? ` failures: ${sync.raw_evidence_failure_reasons.join(",")}`
70
74
  : "";
@@ -75,15 +79,15 @@ export function rawEvidenceSyncLine(sync) {
75
79
  // Neither used to appear anywhere, which is how a 1,030-attempt loop stayed
76
80
  // invisible (BLI-3066).
77
81
  const held = sync.raw_evidence_delivery_held_count > 0
78
- ? ` held: ${sync.raw_evidence_delivery_held_count}`
82
+ ? `, ${sync.raw_evidence_delivery_held_count} waiting`
79
83
  : "";
80
84
  const stuck = sync.raw_evidence_stuck_object_count > 0
81
- ? ` stuck: ${sync.raw_evidence_stuck_object_count} (worst ${sync.raw_evidence_max_delivery_attempts} attempt(s) since ${sync.raw_evidence_oldest_delivery_failure_at ?? "unknown"})`
85
+ ? `, ${sync.raw_evidence_stuck_object_count} stuck (${sync.raw_evidence_max_delivery_attempts} tries since ${sync.raw_evidence_oldest_delivery_failure_at ?? "unknown"})`
82
86
  : "";
83
- return `Raw evidence: uploaded ${sync.raw_evidence_uploaded_object_count} object(s) in ${sync.raw_evidence_uploaded_chunk_count} chunk(s), reused ${sync.raw_evidence_reused_count}, failed ${sync.raw_evidence_failed_count}${held}${stuck}${failures}${retries}`;
87
+ return `Files: ${sync.raw_evidence_uploaded_object_count} uploaded, ${sync.raw_evidence_reused_count} already there, ${sync.raw_evidence_failed_count} failed${held}${stuck}${failures}${retries}`;
84
88
  }
85
89
  export function cursorStatusLine(sync) {
86
- return `Cursor: ${sync.cursor_tracked_object_count} durable object(s) tracked`;
90
+ return `Tracked so far: ${sync.cursor_tracked_object_count} uploaded item(s)`;
87
91
  }
88
92
  /**
89
93
  * One line that cannot say "fine" while an object has never been accepted.
@@ -175,7 +175,7 @@ async function readAuthState(context) {
175
175
  if (session?.session_state === "valid" &&
176
176
  typeof session.device_token === "string" &&
177
177
  session.device_token) {
178
- return ok("authed", "device_token_present", "device token present");
178
+ return ok("authed", "device_token_present", "this machine is signed in");
179
179
  }
180
180
  return hardStop("authed", "pairing_required", [
181
181
  "device is not signed in.",
@@ -216,12 +216,12 @@ async function checkAutostartState(context) {
216
216
  exec,
217
217
  });
218
218
  if (result.status === "loaded") {
219
- return ok("autostart-alive", "already_installed", "autostart scheduler loaded");
219
+ return ok("autostart-alive", "already_installed", "background sync is running");
220
220
  }
221
221
  if (result.status === "unsupported") {
222
222
  return skipped("autostart-alive", "unsupported", result.message ?? "unsupported");
223
223
  }
224
- return needsFix("autostart-alive", result.status === "not_loaded" ? "not_loaded" : "absent", "autostart is not loaded");
224
+ return needsFix("autostart-alive", result.status === "not_loaded" ? "not_loaded" : "absent", "background sync is not running");
225
225
  }
226
226
  async function fixAutostartState(context) {
227
227
  const exec = context.io.exec;
@@ -241,7 +241,7 @@ async function fixAutostartState(context) {
241
241
  if (result.loaded === false) {
242
242
  return fail("autostart-alive", "autostart_load_failed", result.message ?? "operating-system scheduler load failed");
243
243
  }
244
- return ok("autostart-alive", "installed", "autostart installed and loaded");
244
+ return ok("autostart-alive", "installed", "background sync installed and running");
245
245
  }
246
246
  /**
247
247
  * Pure so it can be unit-tested without touching the real machine's home
@@ -261,10 +261,10 @@ export function backfillCompletionStepState(marker, roots) {
261
261
  }
262
262
  const oversized = marker?.oversized_skips;
263
263
  if (oversized && oversized.count > 0) {
264
- return ok("backfill-complete", "complete_with_oversized_skips", `backfill completion covers the current saved roots and both session sources ` +
265
- `(complete_with_oversized_skips · ${oversized.count} file${oversized.count === 1 ? "" : "s"} over the upload cap)`);
264
+ return ok("backfill-complete", "complete_with_oversized_skips", `caught up on old Codex and Claude sessions in every saved folder ` +
265
+ `(complete_with_oversized_skips · ${oversized.count} file${oversized.count === 1 ? "" : "s"} too big to upload)`);
266
266
  }
267
- return ok("backfill-complete", "complete", "backfill completion covers the current saved roots and both session sources");
267
+ return ok("backfill-complete", "complete", "caught up on old Codex and Claude sessions in every saved folder");
268
268
  }
269
269
  async function checkBackfillState(context) {
270
270
  const paths = getCollectorRuntimePaths();
@@ -275,10 +275,10 @@ async function checkBackfillState(context) {
275
275
  return covered;
276
276
  const lock = await inspectBackfillLock(paths);
277
277
  if (lock.held) {
278
- return needsFix("backfill-complete", "backfill_already_running", `backfill completion is not yet proven; another run holds the lock since ${lock.held_since ?? "unknown"}`);
278
+ return needsFix("backfill-complete", "backfill_already_running", `another catch-up run is still going, and this one has not finished yet (running since ${lock.held_since ?? "unknown"})`);
279
279
  }
280
280
  const cursor = await readBackfillCursor(paths);
281
- return needsFix("backfill-complete", cursor.updated_at ? "partial" : "never_run", "backfill completion marker missing");
281
+ return needsFix("backfill-complete", cursor.updated_at ? "partial" : "never_run", "the catch-up over your old sessions has not finished");
282
282
  }
283
283
  async function fixBackfillState(context) {
284
284
  const capture = capturedIo(context.io, !context.command.json);
@@ -308,20 +308,20 @@ async function fixBackfillState(context) {
308
308
  }
309
309
  async function checkGcState(context) {
310
310
  if (context.io.env["COCKPIT_DISABLE_GC"] === "1") {
311
- return skipped("gc-checked", "skipped_disabled", "raw-evidence GC disabled");
311
+ return skipped("gc-checked", "skipped_disabled", "cleanup is switched off");
312
312
  }
313
313
  const paths = getCollectorRuntimePaths();
314
314
  const marker = path.join(paths.state_dir, ".last-raw-evidence-gc");
315
315
  const info = await fs.stat(marker).catch(() => null);
316
316
  if (info && Date.now() - info.mtimeMs < GC_MIN_INTERVAL_MS) {
317
- return skipped("gc-checked", "skipped_throttled", "raw-evidence GC ran within 24h");
317
+ return skipped("gc-checked", "skipped_throttled", "cleanup already ran today");
318
318
  }
319
- return needsFix("gc-checked", "due", "raw-evidence GC is due");
319
+ return needsFix("gc-checked", "due", "cleanup is due");
320
320
  }
321
321
  async function fixGcState(context) {
322
322
  const result = await runRawEvidenceLocalGc(getCollectorRuntimePaths(), context.io.env);
323
323
  if (result.skipped) {
324
- return skipped("gc-checked", "skipped_throttled", "raw-evidence GC skipped");
324
+ return skipped("gc-checked", "skipped_throttled", "cleanup already ran today");
325
325
  }
326
326
  if (result.removed_dirs === 0) {
327
327
  return ok("gc-checked", "nothing_eligible", rawEvidenceGcSummary(result));
@@ -422,8 +422,8 @@ async function fixSyncState(context) {
422
422
  if (result.code !== 0) {
423
423
  const draining = syncBacklogDrainingVerdict(parsed);
424
424
  if (draining) {
425
- return needsFix("sync-fresh", "backlog_draining", `${repoRoot}: raw-evidence backlog is still draining (${draining.remainingObjects} ` +
426
- `object${draining.remainingObjects === 1 ? "" : "s"} deferred this tick); ` +
425
+ return needsFix("sync-fresh", "backlog_draining", `${repoRoot}: still catching up (${draining.remainingObjects} ` +
426
+ `file${draining.remainingObjects === 1 ? "" : "s"} left for later this run); ` +
427
427
  "rerun `cockpit sync` to continue");
428
428
  }
429
429
  return fail("sync-fresh", status ?? "sync_failed", `sync failed for ${repoRoot}`);
@@ -464,9 +464,11 @@ function writeDoctorOutput(command, io, rows) {
464
464
  return;
465
465
  }
466
466
  writeLine(io.stdout, command.dryRun ? "Cockpit doctor dry-run" : "Cockpit doctor");
467
- writeLine(io.stdout, "state step code result");
467
+ // The machine `code` stays in `--json`; a human reading the table wants the
468
+ // sentence, not the label (BLI-3194).
469
+ writeLine(io.stdout, "state step result");
468
470
  for (const row of rows) {
469
- writeLine(io.stdout, `${doctorMark(row)} ${row.id.padEnd(20)} ${row.code.padEnd(23)} ${oneLine(row.message)}`);
471
+ writeLine(io.stdout, `${doctorMark(row)} ${row.id.padEnd(20)} ${oneLine(row.message)}`);
470
472
  }
471
473
  const explanations = rows.filter((row) => (row.hardStop || row.status === "fail") && row.message.includes("\n"));
472
474
  for (const row of explanations) {
@@ -64,7 +64,7 @@ function localSubcommandHelp(command) {
64
64
  "Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--allow-home-root] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
65
65
  "",
66
66
  "Installs, pairs, starts work context(s), syncs once, completes all-history",
67
- "Codex and Claude backfill for the saved roots, and then prints readiness proof.",
67
+ "Codex and Claude backfill for the saved roots, and then tells you it is working.",
68
68
  "If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
69
69
  "`--repo <path>` remains supported as a backward-compatible alias.",
70
70
  `Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
@@ -107,10 +107,10 @@ function localSubcommandHelp(command) {
107
107
  [
108
108
  "Usage: cockpit do-everything [--workspace <path>] [--dashboard-url <url>] [--update-tag <tag>] [--dry-run] [--json]",
109
109
  "",
110
- "Converges a blank, existing, or reused intern machine: latest CLI, interactive auth and collection-root recovery when needed, saved roots, autostart, all-history backfill, raw-evidence GC, and sync freshness.",
110
+ "Gets this machine fully set up, whether it is brand new, already set up, or handed down: latest CLI, sign-in and collection-root recovery when needed, saved folders, background sync, catching up on old sessions, cleaning up old files, and one fresh upload.",
111
111
  "`cockpit fix` is an alias.",
112
- "Maintainer canary: use `--update-tag next` so self-update and re-exec stay on the prerelease candidate.",
113
- "--dry-run prints the checks and would-fix steps without writing config, plists, cursors, or install telemetry.",
112
+ "Maintainers only: use `--update-tag next` so self-update and re-exec stay on the prerelease candidate.",
113
+ "--dry-run shows what it would do without changing anything.",
114
114
  ],
115
115
  ],
116
116
  [
@@ -144,9 +144,9 @@ function localSubcommandHelp(command) {
144
144
  [
145
145
  "Usage: cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--topic-summary <summary>] [--intent <intent>] [--phase <phase>] [--intent-confidence <0..1>] [--workspace <path>] [--branch <name>] [--json]",
146
146
  "",
147
- "Starts local ambient capture. Parent folders start each child git worktree.",
147
+ "Starts collecting your work in the background. If you point it at a parent folder it covers every repo inside.",
148
148
  "Add --ticket only when the work already has a visible ticket; omit it to preserve an existing binding.",
149
- "Use --clear-ticket to intentionally return the context to general ambient capture.",
149
+ "Use --clear-ticket to go back to collecting general work with no ticket attached.",
150
150
  "Use --topic/--intent/--phase for planning, discovery, and learning work that has no ticket yet.",
151
151
  "Supported intents: implementation, bug_fix, root_cause_analysis, planning, discovery, review, testing, documentation, release, learning, coordination, maintenance, analysis, unknown, other.",
152
152
  "Supported phases: planning, discovery, implementation, debugging, review, testing, documentation, release, handoff, analysis, unknown, other.",
@@ -158,7 +158,7 @@ function localSubcommandHelp(command) {
158
158
  [
159
159
  "Usage: cockpit sync [--workspace <path>] [--dashboard-url <url>] [--json]",
160
160
  "",
161
- "Uploads latest local ambient envelope(s), or spools safe retries if blocked.",
161
+ "Uploads your latest collected work. If it cannot reach Cockpit it saves a retry and tries again later.",
162
162
  "Omit --dashboard-url for normal production sync; pass it only for staging/custom dashboards or forced re-pairing.",
163
163
  "`--repo <path>` remains supported as a backward-compatible alias.",
164
164
  "Parent folders sync each child git worktree; Codex AND Claude Code JSONL",
@@ -166,7 +166,7 @@ function localSubcommandHelp(command) {
166
166
  "deterministically and ambiguous transcripts are retained as unattributed",
167
167
  "instead of being duplicated across repos. Use `cockpit sessions` to see why",
168
168
  "a session is or is not collected.",
169
- "Newly discovered repos get a general ambient work context automatically.",
169
+ "New repos start being collected automatically, with no ticket attached.",
170
170
  "Discovery scans 3 folder levels and up to 50 repos by default; tune with",
171
171
  "--max-depth and --max-repos.",
172
172
  "Also self-updates the CLI from npm latest once per day, strictly after",
@@ -179,7 +179,7 @@ function localSubcommandHelp(command) {
179
179
  [
180
180
  "Usage: cockpit analyze [--workspace <path>] [--dashboard-url <url>] [--json]",
181
181
  "",
182
- "Uploads the latest local ambient evidence, then queues one analysis job.",
182
+ "Uploads your latest collected work, then asks Cockpit to analyse it.",
183
183
  "The command returns after the batch is queued; view status and results in My Work.",
184
184
  "Omit --dashboard-url for production; pass it only for staging/custom dashboards.",
185
185
  "`--repo <path>` remains supported as a backward-compatible alias.",
@@ -306,9 +306,9 @@ function backgroundSyncLine(result) {
306
306
  return result.status;
307
307
  }
308
308
  function writeOnboardBanner(command, io) {
309
- writeLine(io.stdout, "Cockpit harvest onboarding");
309
+ writeLine(io.stdout, "Setting up Cockpit");
310
310
  writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
311
- writeLine(io.stdout, `Ticket: ${command.activeTicketId ?? "general ambient"}`);
311
+ writeLine(io.stdout, `Ticket: ${displayTicketId(command.activeTicketId)}`);
312
312
  }
313
313
  /**
314
314
  * Step 2 of onboarding: reuse the device session when it already belongs to
@@ -525,7 +525,7 @@ async function runOnboard(command, io) {
525
525
  }, null, 2));
526
526
  }
527
527
  if (onboardOk && !command.json) {
528
- writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
528
+ writeLine(io.stdout, "PASS: Cockpit is set up and collecting.");
529
529
  writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
530
530
  pair,
531
531
  status,
@@ -552,7 +552,7 @@ async function runOnboard(command, io) {
552
552
  writeLine(io.stdout, "3/5 Work context active.");
553
553
  writeLine(io.stdout, `Repo: ${context.repo}`);
554
554
  writeLine(io.stdout, `Branch: ${context.branch}`);
555
- writeLine(io.stdout, `Ticket: ${context.active_ticket_id ?? "general ambient"}`);
555
+ writeLine(io.stdout, `Ticket: ${displayTicketId(context.active_ticket_id)}`);
556
556
  writeLine(io.stdout, `Context: ${context.work_context_id}`);
557
557
  }
558
558
  addInstallEvent(installEvents, "work_context", "ok");
@@ -638,9 +638,9 @@ async function runOnboard(command, io) {
638
638
  }, null, 2));
639
639
  return finish(onboardOk ? 0 : 1);
640
640
  }
641
- writeLine(io.stdout, "4/5 Ambient metadata uploaded.");
641
+ writeLine(io.stdout, "4/5 Uploaded what you worked on.");
642
642
  writeLine(io.stdout, `HTTP: ${sync.http_status}`);
643
- writeLine(io.stdout, `Facts: ${sync.event_count}`);
643
+ writeLine(io.stdout, `Things recorded: ${sync.event_count}`);
644
644
  writeLine(io.stdout, `Sources: ${sync.source_scan_count}`);
645
645
  writeLine(io.stdout, `Risk flags: ${sync.risk_flag_count}`);
646
646
  writeLine(io.stdout, `Raw evidence files: ${sync.raw_evidence_file_count}`);
@@ -656,7 +656,7 @@ async function runOnboard(command, io) {
656
656
  writeOnboardAutostartBlocker(io, autostart);
657
657
  return finish(1);
658
658
  }
659
- writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
659
+ writeLine(io.stdout, "PASS: Cockpit is set up and collecting.");
660
660
  writeOnboardLiveStatus(io, resolvedCommand, collectionRoots, {
661
661
  pair,
662
662
  status,
@@ -839,7 +839,7 @@ function nextStepForOnboardBlocker(blocker, options = {}) {
839
839
  case "ticket_binding":
840
840
  return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
841
841
  case "device_pairing":
842
- return "Approve from Ambient -> Collector approvals, or paste the pairing code there, then rerun `cockpit onboard`.";
842
+ return "Ask Edward to approve this machine in the dashboard under Ambient -> Collector approvals, then run `cockpit onboard` again.";
843
843
  case "network_or_ingest":
844
844
  return "Check dashboard URL/network, then run `cockpit sync --json` or rerun `cockpit onboard`.";
845
845
  case "install":
@@ -1125,7 +1125,7 @@ async function runSyncWithHealthReceipt(command, io) {
1125
1125
  }, null, 2));
1126
1126
  }
1127
1127
  else {
1128
- writeLine(io.stdout, "live sync paused during backfill");
1128
+ writeLine(io.stdout, "Pausing normal sync while it catches up on old sessions.");
1129
1129
  }
1130
1130
  return {
1131
1131
  exitCode: 0,
@@ -1300,11 +1300,11 @@ async function runSyncLocked(command, io) {
1300
1300
  return syncResult(run);
1301
1301
  }
1302
1302
  if (run.ok) {
1303
- writeLine(io.stdout, "Cockpit ambient envelope uploaded.");
1303
+ writeLine(io.stdout, "Cockpit uploaded this session.");
1304
1304
  writeLine(io.stdout, `Ticket: ${displayTicketId(result.ticket_id)}`);
1305
1305
  writeLine(io.stdout, `Context: ${result.work_context_id}`);
1306
1306
  writeLine(io.stdout, `Head: ${shortSha(result.head_sha)}`);
1307
- writeLine(io.stdout, `Facts: ${result.event_count}`);
1307
+ writeLine(io.stdout, `Things recorded: ${result.event_count}`);
1308
1308
  writeLine(io.stdout, `Risk flags: ${result.risk_flag_count}`);
1309
1309
  writeLine(io.stdout, `Raw evidence files: ${result.raw_evidence_file_count}`);
1310
1310
  writeLine(io.stdout, rawEvidenceSyncLine(result));
@@ -1315,11 +1315,11 @@ async function runSyncLocked(command, io) {
1315
1315
  return syncResult(run);
1316
1316
  }
1317
1317
  if (result.status === "uploaded") {
1318
- writeLine(io.stderr, "Cockpit ambient upload was accepted, but session collection is partial; retry `cockpit sync`.");
1318
+ writeLine(io.stderr, "Cockpit uploaded, but some sessions did not make it. Run `cockpit sync` again.");
1319
1319
  writeAgentSessionSummary(io, run.summary);
1320
1320
  return syncResult(run);
1321
1321
  }
1322
- writeLine(io.stderr, "Cockpit ambient upload failed; safe retry metadata was spooled.");
1322
+ writeLine(io.stderr, "Cockpit could not upload. It saved a note to retry and will try again on the next sync.");
1323
1323
  writeLine(io.stderr, `Ticket: ${displayTicketId(result.ticket_id)}`);
1324
1324
  writeLine(io.stderr, `Failure: ${result.failure_reason}`);
1325
1325
  writeLine(io.stderr, `Retry: ${result.retry_command}`);
@@ -1419,7 +1419,7 @@ async function runAnalyze(command, io) {
1419
1419
  return 0;
1420
1420
  }
1421
1421
  replayCaptured(io.stderr, syncStderr);
1422
- writeLine(io.stdout, "Cockpit latest evidence uploaded.");
1422
+ writeLine(io.stdout, "Cockpit uploaded your latest work.");
1423
1423
  writeLine(io.stdout, "Cockpit analysis queued.");
1424
1424
  if (jobId)
1425
1425
  writeLine(io.stdout, `Job: ${jobId}`);
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.29");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.30");
19
19
  return 0;
20
20
  }
21
21
 
@@ -3,9 +3,11 @@
3
3
  * active work, upload state, retry backlog, and how far historical backfill
4
4
  * has got.
5
5
  *
6
- * Read-only. Split out of commands/local.ts (BLI-3104); moved verbatim, since
7
- * every line here is what an intern pastes into Slack when something looks
8
- * wrong.
6
+ * Read-only. Split out of commands/local.ts (BLI-3104). BLI-3194 rewrote the
7
+ * human block into plain English, because these lines are what an intern
8
+ * pastes into Slack when something looks wrong. The `--json` payload is the
9
+ * machine contract and is byte-for-byte unchanged — `status-json-shape.test.ts`
10
+ * holds it to that.
9
11
  */
10
12
  import { readdir, stat } from "node:fs/promises";
11
13
  import os from "node:os";
@@ -32,8 +34,8 @@ export async function runStatus(command, io) {
32
34
  writeLine(io.stdout, JSON.stringify({ mode: "multi_repo", statuses, backfill_cursor: backfillCursor }, null, 2));
33
35
  return 0;
34
36
  }
35
- writeLine(io.stdout, "Cockpit parent status");
36
- writeLine(io.stdout, `backfill: ${backfillCursorLine(backfillCursor)}`);
37
+ writeLine(io.stdout, "Cockpit status — every repo below this folder");
38
+ writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
37
39
  for (const status of statuses) {
38
40
  writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
39
41
  }
@@ -44,24 +46,26 @@ export async function runStatus(command, io) {
44
46
  writeLine(io.stdout, JSON.stringify({ ...status, backfill_cursor: backfillCursor }, null, 2));
45
47
  return 0;
46
48
  }
47
- writeLine(io.stdout, "Cockpit local status");
48
- writeLine(io.stdout, `installed: ${status.installed}`);
49
- writeLine(io.stdout, `session_state: ${status.session_state}`);
50
- writeLine(io.stdout, `repo: ${status.repo}`);
51
- writeLine(io.stdout, `branch: ${status.branch}`);
52
- writeLine(io.stdout, `ticket: ${displayTicketId(status.active_ticket_id)}`);
53
- writeLine(io.stdout, `work: ${displayWorkLabel(status)}`);
54
- writeLine(io.stdout, `collector_freshness: ${status.collector_freshness}`);
55
- writeLine(io.stdout, `collector_version: ${status.collector_version}`);
56
- writeLine(io.stdout, `upload_state: ${status.upload_state}`);
57
- writeLine(io.stdout, `last_upload_attempt: ${status.last_upload_attempt_at ?? "never"}`);
58
- writeLine(io.stdout, `last_upload_success: ${status.last_upload_success_at ?? "never"}`);
59
- writeLine(io.stdout, `last_upload_failure: ${status.last_upload_failure_reason ?? "none"}`);
60
- writeLine(io.stdout, `pending_uploads: ${status.pending_upload_count}`);
61
- writeLine(io.stdout, `pending_health_receipts: ${status.pending_health_receipt_count}`);
62
- writeLine(io.stdout, `last_health_receipt_failure: ${status.last_health_receipt_failure_reason ?? "none"}`);
63
- writeLine(io.stdout, `backfill: ${backfillCursorLine(backfillCursor)}`);
64
- writeLine(io.stdout, `stuck_evidence: ${stuckEvidenceLine(status)}`);
49
+ writeLine(io.stdout, "Cockpit status");
50
+ writeLine(io.stdout, `Installed: ${status.installed ? "yes" : "no"}`);
51
+ writeLine(io.stdout, `Signed in: ${signedInLine(status.session_state)}`);
52
+ writeLine(io.stdout, `Repo: ${status.repo}`);
53
+ writeLine(io.stdout, `Branch: ${status.branch}`);
54
+ writeLine(io.stdout, `Ticket: ${displayTicketId(status.active_ticket_id)}`);
55
+ writeLine(io.stdout, `Work: ${displayWorkLabel(status)}`);
56
+ writeLine(io.stdout, `Last collected: ${lastCollectedLine(status.collector_freshness)}`);
57
+ writeLine(io.stdout, `Version: ${status.collector_version}`);
58
+ writeLine(io.stdout, `Uploads: ${uploadsLine(status.upload_state)}`);
59
+ writeLine(io.stdout, `Last upload tried: ${status.last_upload_attempt_at ?? "never"}`);
60
+ writeLine(io.stdout, `Last upload worked: ${status.last_upload_success_at ?? "never"}`);
61
+ // The failure reason itself is a machine label and prints verbatim: a status
62
+ // that hides why the last upload failed is the silent-success failure mode.
63
+ writeLine(io.stdout, `Last upload problem: ${status.last_upload_failure_reason ?? "none"}`);
64
+ writeLine(io.stdout, `Waiting to upload: ${status.pending_upload_count}`);
65
+ writeLine(io.stdout, `Health notes waiting to send: ${status.pending_health_receipt_count}`);
66
+ writeLine(io.stdout, `Last health note problem: ${status.last_health_receipt_failure_reason ?? "none"}`);
67
+ writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
68
+ writeLine(io.stdout, `Stuck files: ${stuckEvidenceLine(status)}`);
65
69
  for (const detail of status.details)
66
70
  writeLine(io.stdout, `- ${detail}`);
67
71
  return 0;
@@ -122,11 +126,46 @@ function summarizeBackfillSource(source) {
122
126
  function backfillCursorLine(status) {
123
127
  switch (status.state) {
124
128
  case "done":
125
- return `done (${status.completed_at ?? "completion marker present"})`;
129
+ return `done (${status.completed_at ?? "finished, no date recorded"})`;
126
130
  case "never_run":
127
- return "never run";
131
+ return "not started yet";
128
132
  case "remaining":
129
- return `${status.remaining_count} remaining`;
133
+ return `${status.remaining_count} still to catch up on`;
134
+ }
135
+ }
136
+ /** Plain words for the four session states; `--json` still gets the raw value. */
137
+ function signedInLine(sessionState) {
138
+ switch (sessionState) {
139
+ case "valid":
140
+ return "yes";
141
+ case "expired":
142
+ return "no — the sign-in expired, run `cockpit login`";
143
+ case "missing":
144
+ return "no — run `cockpit login`";
145
+ case "unknown":
146
+ return "cannot tell";
147
+ }
148
+ }
149
+ function lastCollectedLine(freshness) {
150
+ switch (freshness) {
151
+ case "fresh":
152
+ return "recently";
153
+ case "stale":
154
+ return "a while ago";
155
+ case "missing":
156
+ return "never";
157
+ }
158
+ }
159
+ function uploadsLine(uploadState) {
160
+ switch (uploadState) {
161
+ case "ready":
162
+ return "ready";
163
+ case "retry_pending":
164
+ return "waiting to retry";
165
+ case "local_only_missing_auth":
166
+ return "staying on this machine — not signed in";
167
+ case "not_installed":
168
+ return "not set up yet — run `cockpit onboard`";
130
169
  }
131
170
  }
132
171
  async function countRemainingBackfillSessionFiles(homeDir, cursor) {
@@ -68,7 +68,7 @@ export async function runRawEvidenceLocalGc(paths, env = process.env, now = new
68
68
  };
69
69
  }
70
70
  export function rawEvidenceGcSummary(result) {
71
- return `raw-evidence GC: removed ${result.removed_dirs} dirs, freed ~${formatMb(result.freed_bytes)} MB`;
71
+ return `Cleaned up: removed ${result.removed_dirs} old folders, freed ~${formatMb(result.freed_bytes)} MB`;
72
72
  }
73
73
  /**
74
74
  * Collapse byte-identical staged packs down to one survivor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {