@bli-cockpit/cli 0.2.137 → 0.2.141

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.
@@ -157,6 +157,7 @@ export const SLACK_WORKSPACE_KEYS = ["bli", "blue_pearl"];
157
157
  * `cockpit slack read [--person|--channel|--query|--since|--until|--limit]`
158
158
  * (BLI-3462).
159
159
  */
160
+ const SLACK_READ_FORMATS = ["compact", "full"];
160
161
  export function parseSlackArgs(args) {
161
162
  const values = parseNamedArgs(args, {
162
163
  allowedFlags: [
@@ -164,12 +165,15 @@ export function parseSlackArgs(args) {
164
165
  "--dashboard-url",
165
166
  "--workspace",
166
167
  "--stale-only",
168
+ "--list",
167
169
  "--person",
168
170
  "--channel",
169
171
  "--query",
172
+ "--word",
170
173
  "--since",
171
174
  "--until",
172
175
  "--limit",
176
+ "--format",
173
177
  "--json",
174
178
  ],
175
179
  valueFlags: [
@@ -182,6 +186,7 @@ export function parseSlackArgs(args) {
182
186
  "--since",
183
187
  "--until",
184
188
  "--limit",
189
+ "--format",
185
190
  ],
186
191
  });
187
192
  if (values.positionals.length > 1) {
@@ -196,6 +201,12 @@ export function parseSlackArgs(args) {
196
201
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
197
202
  json: values.booleans.has("--json"),
198
203
  };
204
+ if (values.flags.has("--format") && rawAction !== "read") {
205
+ throw new Error("--format belongs to `cockpit slack read`.");
206
+ }
207
+ if (values.booleans.has("--list") && rawAction !== "coverage") {
208
+ throw new Error("--list belongs to `cockpit slack coverage`.");
209
+ }
199
210
  if (rawAction === "coverage") {
200
211
  // `--workspace` here is a Slack workspace key, NOT the collection-root
201
212
  // `--workspace <path>` every setup command takes. Named at the point of use
@@ -209,6 +220,8 @@ export function parseSlackArgs(args) {
209
220
  action: "coverage",
210
221
  workspace,
211
222
  staleOnly: values.booleans.has("--stale-only"),
223
+ // BLI-10450: counts alone could not answer "is there a Kaiben channel?".
224
+ list: values.booleans.has("--list"),
212
225
  ...base,
213
226
  };
214
227
  }
@@ -219,15 +232,27 @@ export function parseSlackArgs(args) {
219
232
  throw new Error("slack read needs at least one of --person, --channel or --query. An unfiltered dump of every " +
220
233
  "message is not something this command offers.");
221
234
  }
235
+ const word = values.booleans.has("--word");
236
+ if (word && !query) {
237
+ throw new Error("--word needs --query: it says how the query text is matched.");
238
+ }
239
+ const format = optionalNonEmpty(values.flags.get("--format"));
240
+ if (format && !SLACK_READ_FORMATS.includes(format)) {
241
+ throw new Error(`slack read --format must be one of ${SLACK_READ_FORMATS.join(", ")}; got ${format}.`);
242
+ }
222
243
  return {
223
244
  kind: "slack",
224
245
  action: "read",
225
246
  person,
226
247
  channel,
227
248
  query,
249
+ ...(word ? { word } : {}),
228
250
  since: optionalNonEmpty(values.flags.get("--since")),
229
251
  until: optionalNonEmpty(values.flags.get("--until")),
230
252
  limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
253
+ // BLI-10450: every result printed twice (a summary, then blocks) — one
254
+ // rendering now, never both. `full` (default) keeps the detailed blocks.
255
+ format: format ?? "full",
231
256
  ...base,
232
257
  };
233
258
  }
@@ -8,7 +8,10 @@
8
8
  */
9
9
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
10
10
  const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "link", "sync-status"]);
11
- const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["read", "update", "link"]);
11
+ // BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
12
+ // it takes SEVERAL references now, so it no longer shares the "exactly one"
13
+ // rule `update`/`link` still enforce.
14
+ const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link"]);
12
15
  export function parseDocsArgs(args) {
13
16
  const values = parseNamedArgs(args, {
14
17
  allowedFlags: [
@@ -22,17 +25,56 @@ export function parseDocsArgs(args) {
22
25
  "--file",
23
26
  "--body-stdin",
24
27
  "--query",
28
+ "--word",
25
29
  "--limit",
26
30
  "--clear",
31
+ "--bodies",
32
+ "--section",
33
+ "--offset",
27
34
  "--json",
28
35
  ],
29
- valueFlags: ["--home", "--dashboard-url", "--title", "--parent", "--visibility", "--file", "--query", "--limit"],
36
+ valueFlags: [
37
+ "--home",
38
+ "--dashboard-url",
39
+ "--title",
40
+ "--parent",
41
+ "--visibility",
42
+ "--file",
43
+ "--query",
44
+ "--limit",
45
+ "--section",
46
+ "--offset",
47
+ ],
30
48
  });
31
49
  const { action, rest } = parseDocsAction(values.positionals);
32
50
  const link = parseDocsLink(action, rest, values.booleans.has("--clear"));
33
- const docRef = parseDocsDocumentReference(action, link.rest);
34
- const { visibility, clearParent, parentId, allowEmpty, title, query, limit } = parseDocsFlags(action, values);
35
- return buildDocsCommand(values, action, docRef, visibility, clearParent, parentId, allowEmpty, title, query, limit, link);
51
+ const { docRef, docRefs } = parseDocsDocumentReference(action, link.rest);
52
+ const { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset } = parseDocsFlags(action, values);
53
+ if (action === "read")
54
+ validateDocsReadTargets(docRefs, parentId, bodies);
55
+ return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link);
56
+ }
57
+ /**
58
+ * BLI-10450: `read` takes either several document references OR
59
+ * `--parent <id> --bodies` (every document filed directly under that
60
+ * parent, with bodies) — never both, and never neither. Checked once flags
61
+ * are known, not in `parseDocsDocumentReference`, because "was --bodies
62
+ * given" decides which of the two empty-input refusals is the right one.
63
+ */
64
+ function validateDocsReadTargets(docRefs, parentId, bodies) {
65
+ const hasRefs = Boolean(docRefs && docRefs.length > 0);
66
+ if (hasRefs && parentId) {
67
+ throw new Error("docs read takes either document references or --parent --bodies, not both.");
68
+ }
69
+ if (bodies && !parentId) {
70
+ throw new Error("--bodies needs --parent, e.g. `cockpit docs read --parent <id> --bodies`.");
71
+ }
72
+ if (parentId && !bodies) {
73
+ throw new Error("docs read --parent needs --bodies too (or use `cockpit docs list --parent <id>` for metadata only).");
74
+ }
75
+ if (!hasRefs && !(parentId && bodies)) {
76
+ throw new Error("docs read needs one or more document ids/slugs, or --parent <id> --bodies.");
77
+ }
36
78
  }
37
79
  /**
38
80
  * BLI-10430: `docs link <id|slug> <notion-url|page-id>` ties a document to a
@@ -67,23 +109,31 @@ function parseDocsAction(positionals) {
67
109
  return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
68
110
  }
69
111
  function parseDocsDocumentReference(action, rest) {
112
+ // BLI-10450: `read` alone takes several references in one call — an agent
113
+ // reading a folder's worth of documents used to pay a resolve-then-read
114
+ // pair PER document. Emptiness is validated later, once `--parent`/
115
+ // `--bodies` are known (`validateDocsReadTargets`).
116
+ if (action === "read") {
117
+ const refs = rest.map((token) => optionalNonEmpty(token)).filter((token) => Boolean(token));
118
+ return { docRef: refs[0], docRefs: refs.length > 0 ? refs : undefined };
119
+ }
70
120
  if (DOCS_ACTIONS_NEEDING_A_DOC.has(action)) {
71
121
  const docRef = optionalNonEmpty(rest[0]);
72
122
  if (!docRef)
73
123
  throw new Error(`docs ${action} needs a document id or slug.`);
74
124
  if (rest.length > 1)
75
125
  throw new Error(`docs ${action} takes one document reference, not ${rest.length}.`);
76
- return docRef;
126
+ return { docRef, docRefs: undefined };
77
127
  }
78
128
  // BLI-10430: `sync-status` reports the whole linked library, or one document.
79
129
  if (action === "sync-status") {
80
130
  if (rest.length > 1)
81
131
  throw new Error(`docs sync-status takes at most one document reference, not ${rest.length}.`);
82
- return optionalNonEmpty(rest[0]);
132
+ return { docRef: optionalNonEmpty(rest[0]), docRefs: undefined };
83
133
  }
84
134
  if (rest.length > 0)
85
135
  throw new Error(`docs ${action} does not take "${rest[0]}".`);
86
- return undefined;
136
+ return { docRef: undefined, docRefs: undefined };
87
137
  }
88
138
  function parseDocsFlags(action, values) {
89
139
  const visibility = optionalNonEmpty(values.flags.get("--visibility"));
@@ -118,19 +168,48 @@ function parseDocsFlags(action, values) {
118
168
  if (query && action !== "list") {
119
169
  throw new Error("--query belongs to `cockpit docs list`.");
120
170
  }
171
+ const wholeWord = values.booleans.has("--word");
172
+ if (wholeWord && !query) {
173
+ throw new Error("--word needs --query: it says how the query text is matched.");
174
+ }
175
+ // BLI-10450: `--limit` now also belongs to `read`, where it means two
176
+ // different things depending on the mode it is used in — a row cap in
177
+ // `--parent --bodies` mode (matching `list`'s own meaning), or a character
178
+ // cap when slicing an explicit reference's body. The two modes are mutually
179
+ // exclusive (`validateDocsReadTargets`), so the flag is never ambiguous in
180
+ // any one call, only overloaded across the command's two shapes.
121
181
  const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
122
- if (limit !== undefined && action !== "list") {
123
- throw new Error("--limit belongs to `cockpit docs list`.");
182
+ if (limit !== undefined && action !== "list" && action !== "read") {
183
+ throw new Error("--limit belongs to `cockpit docs list` or `cockpit docs read`.");
184
+ }
185
+ // BLI-10450: `--bodies` pairs with `--parent` to bulk-read a folder;
186
+ // `--section`/`--offset` slice an explicit reference's body, mirroring
187
+ // `cockpit workbook --section`.
188
+ const bodies = values.booleans.has("--bodies");
189
+ if (bodies && action !== "read")
190
+ throw new Error("--bodies belongs to `cockpit docs read`.");
191
+ const section = optionalNonEmpty(values.flags.get("--section"));
192
+ if (section && action !== "read")
193
+ throw new Error("--section belongs to `cockpit docs read`.");
194
+ const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
195
+ let offset;
196
+ if (offsetRaw !== undefined) {
197
+ offset = Number(offsetRaw);
198
+ if (!Number.isInteger(offset) || offset < 0)
199
+ throw new Error("--offset must be a whole number 0 or greater.");
124
200
  }
125
- return { visibility, clearParent, parentId, allowEmpty, title, query, limit };
201
+ if (offset !== undefined && action !== "read")
202
+ throw new Error("--offset belongs to `cockpit docs read`.");
203
+ return { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset };
126
204
  }
127
- function buildDocsCommand(values, action, docRef, visibility, clearParent, parentId, allowEmpty, title, query, limit, link) {
205
+ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link) {
128
206
  return {
129
207
  kind: "docs",
130
208
  action,
131
209
  homeDir: optionalNonEmpty(values.flags.get("--home")),
132
210
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
133
211
  ...(docRef ? { docRef } : {}),
212
+ ...(docRefs ? { docRefs } : {}),
134
213
  title,
135
214
  ...(parentId ? { parentId } : {}),
136
215
  clearParent,
@@ -139,7 +218,11 @@ function buildDocsCommand(values, action, docRef, visibility, clearParent, paren
139
218
  filePath: optionalNonEmpty(values.flags.get("--file")),
140
219
  bodyStdin: values.booleans.has("--body-stdin"),
141
220
  ...(query ? { query } : {}),
221
+ ...(wholeWord ? { wholeWord } : {}),
142
222
  ...(limit === undefined ? {} : { limit }),
223
+ ...(bodies ? { bodies } : {}),
224
+ ...(section ? { section } : {}),
225
+ ...(offset === undefined ? {} : { offset }),
143
226
  ...(link.notionPage ? { notionPage: link.notionPage } : {}),
144
227
  ...(link.clearLink ? { clearLink: true } : {}),
145
228
  json: values.booleans.has("--json"),
@@ -73,14 +73,14 @@ export function parseIssueArgs(args) {
73
73
  ],
74
74
  });
75
75
  const { action, rest } = readIssueAction(values);
76
- const { issueRef, moveState } = readIssueReference(action, rest);
76
+ const { issueRef, issueRefs, moveState } = readIssueReference(action, rest);
77
77
  validateIssueState(moveState, "issue move state must be one of: ");
78
78
  const stateFilter = readIssueStateFilter(action, values);
79
79
  const title = readIssueTitle(action, values);
80
80
  const priority = readIssuePriority(values);
81
81
  const assignee = readIssueAssignee(action, values);
82
82
  const limit = readIssueLimit(action, values);
83
- return buildIssueCommand(values, action, issueRef, moveState, stateFilter, title, priority, assignee, limit);
83
+ return buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, assignee, limit);
84
84
  }
85
85
  function readIssueAction(values) {
86
86
  const first = values.positionals[0];
@@ -92,8 +92,19 @@ function readIssueAction(values) {
92
92
  }
93
93
  function readIssueReference(action, rest) {
94
94
  let issueRef;
95
+ let issueRefs;
95
96
  let moveState;
96
- if (ISSUE_ACTIONS_NEEDING_AN_ISSUE.has(action)) {
97
+ // BLI-10450: `show` alone takes several references in one call — an agent
98
+ // reading a sprint's worth of tickets used to pay one process per issue.
99
+ // Every other verb here still names exactly one issue.
100
+ if (action === "show") {
101
+ const refs = rest.map((token) => optionalNonEmpty(token)).filter((token) => Boolean(token));
102
+ if (refs.length === 0)
103
+ throw new Error("issue show needs one or more issue ids or identifiers, e.g. BLI-3654.");
104
+ issueRefs = refs;
105
+ issueRef = refs[0];
106
+ }
107
+ else if (ISSUE_ACTIONS_NEEDING_AN_ISSUE.has(action)) {
97
108
  issueRef = optionalNonEmpty(rest[0]);
98
109
  if (!issueRef)
99
110
  throw new Error(`issue ${action} needs an issue id or identifier, e.g. BLI-3654.`);
@@ -112,7 +123,7 @@ function readIssueReference(action, rest) {
112
123
  else if (rest.length > 0) {
113
124
  throw new Error(`issue ${action} does not take "${rest[0]}".`);
114
125
  }
115
- return { issueRef, moveState };
126
+ return { issueRef, issueRefs, moveState };
116
127
  }
117
128
  function validateIssueState(state, errorPrefix) {
118
129
  if (state && !ISSUE_STATES.includes(state)) {
@@ -166,13 +177,14 @@ function readIssueLimit(action, values) {
166
177
  }
167
178
  return limit;
168
179
  }
169
- function buildIssueCommand(values, action, issueRef, moveState, stateFilter, title, priority, assignee, limit) {
180
+ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, assignee, limit) {
170
181
  return {
171
182
  kind: "issue",
172
183
  action,
173
184
  homeDir: optionalNonEmpty(values.flags.get("--home")),
174
185
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
175
186
  ...(issueRef ? { issueRef } : {}),
187
+ ...(issueRefs ? { issueRefs } : {}),
176
188
  ...(moveState ? { moveState } : {}),
177
189
  ...(stateFilter ? { stateFilter } : {}),
178
190
  ...(assignee ? { assignee } : {}),
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The long-form `cockpit <command> --help` text for JARVIS, the model cards,
3
+ * Scout and the ops doors.
4
+ *
5
+ * Split out of `local-help-commands.ts` (BLI-10445) when that file crossed the
6
+ * 500-line readability ratchet, along the same seam BLI-3709 drew: these are
7
+ * Tower surfaces, not the collector's own commands.
8
+ *
9
+ * Every string moved verbatim. Help output is what an intern pastes back when
10
+ * something breaks, so it is a user-visible contract like any other.
11
+ */
12
+ export const TOWER_OPS_COMMAND_HELP = [
13
+ [
14
+ "jarvis",
15
+ [
16
+ "Usage: cockpit jarvis [question] [--prompt <question>] [--as <person>] [--date <YYYY-MM-DD>] [--thread <name>] [--model <key>] [--image <path>|--file <path>] [--no-stream] [--threads|--history [--limit <n>]] [--trace <id|last>] [--dashboard-url <url>] [--json [--show-approval-code]]",
17
+ "",
18
+ "Chats with the same JARVIS used by Tower web chat and the BLI Slack DM.",
19
+ "--as selects the existing website person space; it changes who the chat is about, never who is authenticated.",
20
+ "--date <YYYY-MM-DD> binds the page that was live on one of that person's days (`today` and `yesterday` work too), so the turn is about that day's page instead of the newest one. A day with nothing compiled binds no page and JARVIS says so rather than answering from a different day.",
21
+ "--model <key> requests one provider:model pair (e.g. openai:gpt-5.6-terra); an unrecognised key is refused by the dashboard, not this command.",
22
+ "--image <path> (alias --file) attaches one local PNG, JPEG, or WEBP under 10 MB with the question, the same one-image gate the JARVIS panel uses. A bad path, an unsupported type, or an oversized file is refused with its own plain sentence — never a stack trace.",
23
+ "Run with no question for an interactive terminal conversation.",
24
+ "Agents can pass --prompt, positional text, or pipe one question on stdin.",
25
+ "Each answer prints a per-tool trace line and, only on a fallback or model mismatch, one model receipt line.",
26
+ "Trace lines arrive live while JARVIS works; pass --no-stream to wait for the whole answer in one piece instead.",
27
+ "--json writes one machine-readable response (including trace and model) to stdout; operational metadata stays on stderr. A --json run never streams fragments, whichever mode it is in.",
28
+ "A --json turn is treated as an AGENT surface: if it proposes a coding task, JARVIS answers with proposal_id and NOT the eight-character approval code, because a code a program can read is a code it could approve with. The person fetches the code themself — a Tower browser tab, their Slack DM, or an interactive `cockpit jarvis` — and hands it back. Pass --show-approval-code when a script really is driving a dispatch for a person; the run says on stderr that it did.",
29
+ "That object carries answer, sources (the Source: lines inside it), turn_id (pass it to --trace), thread_id (pass it to --thread), proposal_id, next_action, degraded and degraded_reasons — the SAME shape the bli-tower MCP server's jarvis_ask tool returns, so a script and an agent read one contract. reply/thread/traceId/model/trace/latency are still there beside it and still mean what they always meant.",
30
+ "proposal_id carries the coding-arm proposal a turn made, and is null when it made none — including when the arm refused. next_action is the sentence that says which of those it was and what a PERSON has to do about it: fetch the approval code for that proposal (a Tower tab, their Slack DM, or their own interactive `cockpit jarvis`), or approve a refused one where a person reads. A null proposal_id is never on its own. Do not send a proposal_id back on a --json turn: this is the surface the code was withheld from, and reading it back is refused here.",
31
+ "A turn that outlives the dashboard's own ceiling is reported as turn_timed_out — an incomplete answer is never printed as a finished one.",
32
+ "The command uses the existing paired device identity. It cannot override the caller, team, role, or person scope.",
33
+ "A thread is SINGLE-WRITER: two turns in the same conversation at the same time can read each other, and one can answer the other's question. Your own terminal is one conversation, named main, and stays that way. A --json turn is a program's, so it gets a FRESH conversation per call unless --thread names one, and the answer's thread_id is the name it used — pass that back with --thread to continue it deliberately. On a --json turn, --thread main asks for the shared default nobody chose, so it is treated as naming nothing and a fresh conversation is minted anyway.",
34
+ "--threads lists the terminal conversations on this account; --thread <name> --history replays one (the newest --limit <n> messages). The per-call conversations a --json turn minted are counted rather than listed, and any one of them still replays by name. Neither takes a question, and neither can reach anybody else's conversations.",
35
+ "--trace <id|last> prints the step tree of one turn — every model step, tool call and server-side memory step, with how long each took, which model ran it, the tokens it spent and what failed. `last` opens the turn this machine asked most recently. Like --threads it takes no question and reaches no model.",
36
+ "Run `cockpit login` first if this machine is not paired.",
37
+ ],
38
+ ],
39
+ [
40
+ "models",
41
+ [
42
+ "Usage: cockpit models [list|show <provider:model>|compare <id> <id> [...]] [--highlight] [--dashboard-url <url>] [--json]",
43
+ "",
44
+ "The model-card shelf: one card per model, with what the PROVIDER documents (context",
45
+ "window, max output, price per million, reasoning modes, tool calling, structured output,",
46
+ "quirks) beside what OUR machine measured (latency p50/p95, observed tokens/s, the last",
47
+ "belt and conformance probe verdicts).",
48
+ "Every value carries the page it was read from; a field the provider does not state reads",
49
+ "unknown with the reason, never a guess.",
50
+ "STALE:<field> means a measurement contradicted the documentation — reasoning-off refused",
51
+ "where the card says it can be turned off, the belt refused where the card says tool",
52
+ "calling. The line under it is the command that re-reads the provider.",
53
+ "compare puts two to four cards side by side: the same rows the page at /settings/models/",
54
+ "compare renders, including `$ per 100 JARVIS turns` (our own measured tokens a turn times",
55
+ "the provider's price, with the window it was measured over beside it — `not measured`",
56
+ "where no profile exists, never a per-million price relabelled) and the bench row, which",
57
+ "appears only when every model was judged in the SAME run against the SAME baseline;",
58
+ "otherwise it says `benched separately — not a head-to-head` rather than subtracting two",
59
+ "win rates. --highlight marks the winner in each scored row (lowest wins on price and",
60
+ "latency, highest on tokens/s, most wins on the bench); a row where any column is `—` or",
61
+ "`not measured` is not scored at all, and says why.",
62
+ "`cockpit models` is the SHELF; `cockpit model` is the org setting saying which model a",
63
+ "slot runs on. Different nouns, one letter apart, on purpose.",
64
+ "--json writes one object to stdout; operational lines stay on stderr.",
65
+ "Run `cockpit login` first if this machine is not paired.",
66
+ ],
67
+ ],
68
+ [
69
+ "scout",
70
+ [
71
+ "Usage: cockpit scout [board|start|dismiss|undo <experiment-id>] [--days <n>] [--dashboard-url <url>] [--json]",
72
+ "",
73
+ "Prints the same Scout board the Tower page shows, in the same words: the standing",
74
+ "watch line, the cards waiting on a decision, and the raw signal watch underneath.",
75
+ "This is the board itself. To ask a QUESTION about it — what it means, whether it is",
76
+ "worth doing here — use `cockpit jarvis`, whose readScout tool reads the same rows.",
77
+ "`cockpit scout board` and a bare `cockpit scout` are the same command; the MCP twin",
78
+ "is `scout_board` on the bli-tower server, over the same GET /api/cockpit/scout door.",
79
+ "--days <n> widens or narrows the window (1 to 90; the board defaults to 14).",
80
+ "Reading follows the Scout page audience setting, so whoever can open /scout can run this.",
81
+ "start, dismiss, and undo move one card and are super_admin actions on every surface;",
82
+ "a refusal is printed as Tower worded it and the board stays readable underneath.",
83
+ "The id can be the first 6 or more characters printed beside each card; an ambiguous",
84
+ "prefix is refused with the candidates rather than acting on the first match.",
85
+ "--json writes one object to stdout; operational lines stay on stderr.",
86
+ "Run `cockpit login` first if this machine is not paired.",
87
+ ],
88
+ ],
89
+ [
90
+ "ops",
91
+ [
92
+ "Usage: cockpit ops [status [--job <id>] [--coverage] [--skips] [--memory [--memory-days N]] [--models] [--turns] [--tool-router] | recompile --person <email|name|id> [--dry-run]] [--json]",
93
+ "",
94
+ " cockpit ops status",
95
+ " One line per scheduled job: when it last produced something, and whether that",
96
+ " is late FOR THAT JOB. A once-daily job quiet for 18 hours reads ok; a",
97
+ " quarter-hourly one does not. Every line names the schedule it is judged against.",
98
+ " --turns adds 30-day JARVIS turn aggregates; --tool-router adds the shadow-router agreement board.",
99
+ " A job that is not ok also prints what its artifact does and does not prove —",
100
+ " several only write a row when there is something new, so quiet can mean a quiet",
101
+ " week rather than a broken cron.",
102
+ " --job <id> asks about one; --skips adds the open Slack and external-sync skips,",
103
+ " grouped by reason. Exits 1 if anything is stale, empty or unreadable.",
104
+ " --coverage is the collection-coverage row on its own, with the BUCKET table:",
105
+ " which bucket the missing sessions are in (staged-not-committed with the upload",
106
+ " ledger's own reason, still pending upload, deliberately withheld, or no row at",
107
+ " all), whose machines they are on, which of their own days, and what to run for",
108
+ " each. That table also prints on the ordinary board whenever anything is missing.",
109
+ " Every machine's line also carries its BLI Memory column — whether the MCP",
110
+ " servers and the recall/save hooks are registered for Claude Code and Codex,",
111
+ " and whether Codex has been told to TRUST its hooks. That column never colours",
112
+ " a machine: a laptop with no memory hooks is still collecting sessions.",
113
+ " --memory adds the adoption gauge: memories saved per person per day for the",
114
+ " last 7 days, split by source (an agent calling save_memory, the Stop hook, or",
115
+ " the vendor import, which is a backfill and never counted as use).",
116
+ " --memory-days N changes the window (1-30) and implies --memory. Searches are",
117
+ " NOT counted — nothing in Tower records one, and the gauge says so rather than",
118
+ " printing a zero you would read as nobody searching.",
119
+ " It also shows ordinary prompt-hook observations: outcome counts, 100 ms",
120
+ " duration bounds, actual vector-cache reuse and named coverage gaps. These",
121
+ " are hook-body timings, not full host wait or retrieval-quality scores.",
122
+ " cockpit ops recompile --person <email|name|id> [--dry-run]",
123
+ " Writes that person's page again, now. Your own page is always yours to",
124
+ " recompile; somebody else's is for the people who read across the team.",
125
+ " --dry-run resolves the person and says which page is being served, compiling",
126
+ " nothing. A compile only ever ADDS a version — nothing is overwritten.",
127
+ " It can take minutes, and if Tower's 800-second budget runs out first you are",
128
+ " told exactly that: the compile may still have finished. Check with",
129
+ " `cockpit brief --for <person>`.",
130
+ " There is no --cadence: the compile path behind this writes a daily, and a flag",
131
+ " that was accepted and dropped would be worse than no flag.",
132
+ ],
133
+ ],
134
+ ];