@bli-cockpit/cli 0.2.74 → 0.2.76

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.
@@ -66,6 +66,8 @@ export function parseOpsArgs(args) {
66
66
  "--skips",
67
67
  "--memory",
68
68
  "--memory-days",
69
+ // BLI-3798: the coverage half on its own, with the bucket table.
70
+ "--coverage",
69
71
  "--person",
70
72
  "--dry-run",
71
73
  "--json",
@@ -91,10 +93,19 @@ export function parseOpsArgs(args) {
91
93
  if (memoryDays !== undefined && !/^[0-9]{1,2}$/u.test(memoryDays)) {
92
94
  throw new Error("ops --memory-days takes a whole number of days, 1 to 30.");
93
95
  }
96
+ const coverageOnly = values.booleans.has("--coverage");
97
+ const job = optionalNonEmpty(values.flags.get("--job"));
98
+ if (coverageOnly && job !== undefined && job !== "collection-coverage") {
99
+ // Refused rather than silently preferring one: `--coverage --job
100
+ // daily-brief` is somebody asking two different questions in one
101
+ // command, and answering the wrong one is how a board loses trust.
102
+ throw new Error("ops --coverage IS --job collection-coverage. Pass one or the other, not a --job naming a different pipeline.");
103
+ }
94
104
  return {
95
105
  kind: "ops",
96
106
  action: "status",
97
- job: optionalNonEmpty(values.flags.get("--job")),
107
+ job: coverageOnly ? "collection-coverage" : job,
108
+ coverage: coverageOnly,
98
109
  skips: values.booleans.has("--skips"),
99
110
  memory: values.booleans.has("--memory") || memoryDays !== undefined,
100
111
  ...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
@@ -295,7 +295,7 @@ export function localSubcommandHelp(command) {
295
295
  [
296
296
  "ops",
297
297
  [
298
- "Usage: cockpit ops [status [--job <id>] [--skips] [--memory [--memory-days N]] | recompile --person <email|name|id> [--dry-run]] [--json]",
298
+ "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] | recompile --person <email|name|id> [--dry-run]] [--json]",
299
299
  "",
300
300
  " cockpit ops status",
301
301
  " One line per scheduled job: when it last produced something, and whether that",
@@ -306,6 +306,11 @@ export function localSubcommandHelp(command) {
306
306
  " week rather than a broken cron.",
307
307
  " --job <id> asks about one; --skips adds the open Slack and external-sync skips,",
308
308
  " grouped by reason. Exits 1 if anything is stale, empty or unreadable.",
309
+ " --coverage is the collection-coverage row on its own, with the BUCKET table:",
310
+ " which bucket the missing sessions are in (staged-not-committed with the upload",
311
+ " ledger's own reason, still pending upload, deliberately withheld, or no row at",
312
+ " all), whose machines they are on, which of their own days, and what to run for",
313
+ " each. That table also prints on the ordinary board whenever anything is missing.",
309
314
  " Every machine's line also carries its BLI Memory column — whether the MCP",
310
315
  " servers and the recall/save hooks are registered for Claude Code and Codex,",
311
316
  " and whether Codex has been told to TRUST its hooks. That column never colours",
@@ -61,7 +61,8 @@ async function listChannels(door) {
61
61
  }
62
62
  for (const channel of fetched.channels) {
63
63
  const label = channel.is_dm ? "dm" : channel.is_private ? "private" : "public";
64
- writeLine(door.io.stdout, `${channel.id} ${label.padEnd(7)} ${channel.name ?? "(unnamed)"}`);
64
+ const workspace = channel.is_dm ? "-" : (channel.slack_workspace ?? "bli");
65
+ writeLine(door.io.stdout, `${channel.id} ${label.padEnd(7)} ${workspace.padEnd(10)} ${channel.name ?? "(unnamed)"}`);
65
66
  }
66
67
  return 0;
67
68
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `cockpit ops --coverage` — WHICH sessions never arrived (BLI-3798).
3
+ *
4
+ * The board's COVERAGE block already prints a line per person. What it never
5
+ * printed was the BUCKET table: the reasons were computed on the server, sat
6
+ * on `people[].days[].reasons` in every response, and were never rendered
7
+ * anywhere — so `42 sessions seen and never arrived` was the whole of what a
8
+ * person saw, ten QA ticks running, and reading further meant writing a script
9
+ * against production.
10
+ *
11
+ * Same rule as the rest of this file's family: every sentence in here was
12
+ * written on the SERVER (`lib/ops/coverage-rollup.ts` and
13
+ * `lib/ops/coverage-buckets.ts`) and is printed VERBATIM. This module decides
14
+ * order, indentation and column widths and nothing else. A terminal that
15
+ * composed its own fix sentence would eventually tell somebody to run a
16
+ * command the board does not believe in, and the one that is wrong is always
17
+ * the one somebody is reading.
18
+ *
19
+ * A CLI too old to know this section simply does not print it: the buckets
20
+ * ride the ordinary `coverage` section of the same response, so nothing here
21
+ * is load-bearing for delivery. What is load-bearing is that the SERVER also
22
+ * puts the dominant bucket in the `collection-coverage` row's `detail`, which
23
+ * every version prints when the row is `failing`.
24
+ */
25
+ /**
26
+ * The bucket table, largest first, each with its people and its fix.
27
+ *
28
+ * Printed after the person lines, because "who" comes before "which bucket"
29
+ * when somebody is scanning — but the HEADLINE goes first, since it is the one
30
+ * line that answers the question the row has been failing to answer.
31
+ */
32
+ export function renderCoverageBuckets(table, dim) {
33
+ if (!table) {
34
+ // Never a silent gap where a section was asked for: an older Tower that
35
+ // does not send the table says so, rather than looking like a clean fleet.
36
+ return [
37
+ "",
38
+ "BUCKETS not sent by this Tower — it predates the coverage bucket table; the per-person lines above are all it knows",
39
+ ];
40
+ }
41
+ const buckets = table.buckets ?? [];
42
+ const lines = ["", `BUCKETS ${table.headline ?? "(no headline)"}`];
43
+ if (buckets.length === 0) {
44
+ lines.push(dim(" nothing is unaccounted for, so there are no buckets to show"));
45
+ return lines;
46
+ }
47
+ for (const bucket of buckets) {
48
+ lines.push(` ${String(bucket.count ?? 0).padStart(5)} ${bucket.reason ?? "unlabelled"}`);
49
+ for (const person of bucket.people ?? []) {
50
+ const days = (person.days ?? []).join(", ");
51
+ lines.push(dim(` ${String(person.count ?? 0).padStart(4)} ${person.displayName ?? "(person)"}` +
52
+ (days ? ` ${days}` : "")));
53
+ }
54
+ // The fix is NOT dimmed. It is the only line on this board that tells
55
+ // somebody to do something, and a board whose one action is grey is a
56
+ // board that gets scrolled past — which is what ten ticks of `failing`
57
+ // with no named bucket already proved.
58
+ if (bucket.owner)
59
+ lines.push(` fix: ${bucket.owner}`);
60
+ }
61
+ return lines;
62
+ }
@@ -12,7 +12,9 @@
12
12
  * artifact does and does not prove — because that is exactly the moment
13
13
  * somebody is about to conclude something from it.
14
14
  */
15
+ import { renderCoverageBuckets, } from "./ops-render-coverage.js";
15
16
  export { renderMemoryHooks, renderMemoryUsage } from "./ops-render-memory.js";
17
+ export { renderCoverageBuckets };
16
18
  /** The word a person reads. Short, fixed width, and never a bare colour. */
17
19
  export function verdictWord(verdict) {
18
20
  switch (verdict) {
@@ -64,7 +66,15 @@ export function intervalWord(row) {
64
66
  return `every ${Math.round(hours / 24)}d`;
65
67
  return `every ${Math.round(hours)}h`;
66
68
  }
67
- export function renderOpsStatus(payload, dim) {
69
+ export function renderOpsStatus(payload, dim,
70
+ /**
71
+ * BLI-3798. `alwaysShowBuckets` is what `cockpit ops --coverage` passes: the
72
+ * board hides the table on a clean fleet (it would be nineteen rows of
73
+ * nothing), but somebody who asked for the coverage view specifically is
74
+ * owed the answer "nothing is unaccounted for" out loud rather than an
75
+ * absent section they have to interpret.
76
+ */
77
+ options = {}) {
68
78
  const rows = payload.pipelines ?? [];
69
79
  const lines = [];
70
80
  const counts = new Map();
@@ -104,8 +114,16 @@ export function renderOpsStatus(payload, dim) {
104
114
  }
105
115
  if (payload.fleet)
106
116
  lines.push(...renderFleet(payload.fleet, dim));
107
- if (payload.coverage)
117
+ if (payload.coverage) {
108
118
  lines.push(...renderCoverage(payload.coverage, dim));
119
+ // BLI-3798. The bucket table rides EVERY board, not only `--coverage`:
120
+ // ten QA ticks of `42 sessions seen and never arrived` is what happens
121
+ // when the reasons are only reachable behind a flag nobody knew about.
122
+ // Skipped entirely on a clean fleet, so it costs a healthy board nothing.
123
+ if (options.alwaysShowBuckets || (payload.coverage.counts?.gapSessions ?? 0) > 0) {
124
+ lines.push(...renderCoverageBuckets(payload.coverage.buckets, dim));
125
+ }
126
+ }
109
127
  const slack = payload.skips?.slack;
110
128
  const external = payload.skips?.external;
111
129
  if (slack || external) {
@@ -16,6 +16,15 @@
16
16
  * the command, and it does not pass silently either: it prints as `n/a` with
17
17
  * the reason there is nothing to read.
18
18
  *
19
+ * `ops status --coverage` (BLI-3798) is the same door narrowed to the
20
+ * collection-coverage row, with the BUCKET table under it: which bucket the
21
+ * missing sessions are in, whose machines they are on, which of their own days,
22
+ * and one fix sentence per bucket. It exists because the board said
23
+ * `42 sessions seen and never arrived` for ten consecutive QA ticks while the
24
+ * reasons sat one field deeper in the same response, and reading them meant
25
+ * writing a script against production. The table also rides the ordinary board
26
+ * whenever anything is missing, so nobody has to know this flag exists.
27
+ *
19
28
  * `ops recompile` spends a real provider call, so it says what it is about to
20
29
  * do and reports the ceiling by name. A run that outlives Tower's own 800-second
21
30
  * budget produces no response at all, which is indistinguishable from a network
@@ -83,7 +92,9 @@ async function runOpsStatus(command, io, tower) {
83
92
  }
84
93
  else {
85
94
  const styled = colorEnabled(io);
86
- for (const line of renderOpsStatus(payload, (text) => dim(text, styled))) {
95
+ for (const line of renderOpsStatus(payload, (text) => dim(text, styled), {
96
+ alwaysShowBuckets: command.coverage === true,
97
+ })) {
87
98
  writeLine(io.stdout, line);
88
99
  }
89
100
  if (memory?.section) {
@@ -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.74");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.76");
19
19
  return 0;
20
20
  }
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.74",
3
+ "version": "0.2.76",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {