@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.
Files changed (148) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-direct.js +119 -0
  29. package/dist/commands/cal-overlay.js +73 -30
  30. package/dist/commands/cal.js +62 -25
  31. package/dist/commands/clean.js +4 -1
  32. package/dist/commands/docs-read.js +5 -2
  33. package/dist/commands/docs.js +38 -6
  34. package/dist/commands/doctor-backfill.js +88 -71
  35. package/dist/commands/doctor-pipeline.js +59 -46
  36. package/dist/commands/heartbeat-post.js +116 -0
  37. package/dist/commands/heartbeat-token-lock.js +46 -33
  38. package/dist/commands/heartbeat.js +43 -125
  39. package/dist/commands/install-update.js +17 -15
  40. package/dist/commands/issue-show.js +27 -28
  41. package/dist/commands/issue-versions.js +93 -0
  42. package/dist/commands/issue-write.js +70 -23
  43. package/dist/commands/issue.js +73 -37
  44. package/dist/commands/jarvis-live-answer.js +99 -0
  45. package/dist/commands/jarvis-render.js +1 -94
  46. package/dist/commands/jarvis-trace.js +30 -22
  47. package/dist/commands/jarvis-turn-report.js +124 -0
  48. package/dist/commands/jarvis-turn.js +108 -182
  49. package/dist/commands/jarvis.js +26 -17
  50. package/dist/commands/local-args-collector-setup.js +82 -84
  51. package/dist/commands/local-args-collector-work.js +37 -38
  52. package/dist/commands/local-args-tower-admin.js +155 -142
  53. package/dist/commands/local-args-tower-cal.js +69 -54
  54. package/dist/commands/local-args-tower-careers.js +73 -54
  55. package/dist/commands/local-args-tower-docs-msg.js +79 -51
  56. package/dist/commands/local-args-tower-github.js +26 -20
  57. package/dist/commands/local-args-tower-hits.js +32 -20
  58. package/dist/commands/local-args-tower-manager.js +85 -4
  59. package/dist/commands/local-args-tower-memory.js +139 -117
  60. package/dist/commands/local-args-tower-msg.js +24 -6
  61. package/dist/commands/local-args-tower-notes.js +346 -0
  62. package/dist/commands/local-args-tower-pages.js +100 -378
  63. package/dist/commands/local-args-tower-settings.js +28 -29
  64. package/dist/commands/local-args-tower-setup.js +11 -4
  65. package/dist/commands/local-args-tower-work.js +74 -37
  66. package/dist/commands/local-args.js +80 -129
  67. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  68. package/dist/commands/local-help-commands-tower.js +20 -7
  69. package/dist/commands/local-help-commands.js +342 -341
  70. package/dist/commands/local-help.js +87 -83
  71. package/dist/commands/local-run-guards.js +100 -0
  72. package/dist/commands/local.js +137 -206
  73. package/dist/commands/login.js +13 -9
  74. package/dist/commands/logs.js +55 -38
  75. package/dist/commands/mail-attachments.js +44 -26
  76. package/dist/commands/manager-render-home.js +208 -0
  77. package/dist/commands/manager-render.js +7 -3
  78. package/dist/commands/manager-requests.js +33 -0
  79. package/dist/commands/manager.js +39 -5
  80. package/dist/commands/memory-consolidation.js +18 -9
  81. package/dist/commands/memory-daemon-probe.js +37 -28
  82. package/dist/commands/memory-install-config.js +64 -60
  83. package/dist/commands/memory-install.js +21 -41
  84. package/dist/commands/memory-requeue.js +26 -19
  85. package/dist/commands/memory-rules.js +26 -18
  86. package/dist/commands/memory-search-print.js +26 -0
  87. package/dist/commands/memory-store.js +14 -30
  88. package/dist/commands/memory-verb-list.js +49 -48
  89. package/dist/commands/msg-read-line.js +32 -0
  90. package/dist/commands/msg.js +45 -3
  91. package/dist/commands/notes-accounts.js +49 -32
  92. package/dist/commands/notes-sync-loop.js +115 -89
  93. package/dist/commands/notes-wisprflow-login.js +44 -23
  94. package/dist/commands/notes-writes.js +115 -100
  95. package/dist/commands/notes.js +33 -55
  96. package/dist/commands/onboard-flows.js +69 -51
  97. package/dist/commands/onboard.js +11 -9
  98. package/dist/commands/ops-render-memory.js +10 -7
  99. package/dist/commands/ops-render.js +33 -30
  100. package/dist/commands/ops.js +3 -0
  101. package/dist/commands/public-root.js +1 -1
  102. package/dist/commands/session-sync-outside-roots.js +40 -36
  103. package/dist/commands/session-sync-plan.js +14 -10
  104. package/dist/commands/session-sync-upload.js +24 -22
  105. package/dist/commands/session-sync.js +19 -8
  106. package/dist/commands/setup.js +13 -0
  107. package/dist/commands/slack.js +25 -16
  108. package/dist/commands/sources-check.js +13 -7
  109. package/dist/commands/sources-discovery.js +25 -17
  110. package/dist/commands/sources-verdict.js +19 -18
  111. package/dist/commands/speed.js +23 -19
  112. package/dist/commands/start.js +20 -16
  113. package/dist/commands/sync-doctor.js +52 -48
  114. package/dist/commands/sync.js +22 -16
  115. package/dist/commands/usage-telemetry-toml.js +71 -45
  116. package/dist/commands/usage.js +94 -68
  117. package/dist/commands/workbook-render.js +10 -7
  118. package/dist/evidence-reconcile-client.js +75 -64
  119. package/dist/evidence-redelivery-plan.js +37 -34
  120. package/dist/evidence-redelivery.js +32 -29
  121. package/dist/evidence-transcripts-client.js +20 -20
  122. package/dist/evidence-upload-object.js +86 -89
  123. package/dist/evidence-upload-plan.js +94 -83
  124. package/dist/evidence-upload-terminal.js +45 -50
  125. package/dist/image-description-session.js +156 -159
  126. package/dist/local-state-pairing-code.js +39 -31
  127. package/dist/local-state-status.js +9 -2
  128. package/dist/onboarding-roots.js +5 -8
  129. package/dist/process-runner.js +8 -8
  130. package/dist/raw-evidence-gc.js +33 -38
  131. package/dist/raw-evidence-stuck-summary.js +7 -14
  132. package/dist/repo-identity.js +26 -23
  133. package/dist/scheduled-self-update.js +11 -15
  134. package/dist/second-install.js +3 -0
  135. package/dist/slim/codex-rules.js +23 -36
  136. package/dist/spool/local-spool-parse.js +13 -12
  137. package/dist/sync-lock.js +5 -15
  138. package/dist/tower-client.js +36 -53
  139. package/dist/tower-stream.js +7 -24
  140. package/dist/upload-agent-artifacts.js +58 -64
  141. package/dist/upload-envelope-build.js +60 -59
  142. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  143. package/dist/upload-evidence-delivery-summary.js +32 -34
  144. package/dist/upload-ingest-receipt.js +20 -23
  145. package/dist/upload-session-reports-queue.js +4 -1
  146. package/dist/upload-session-reports-wire.js +64 -68
  147. package/dist/upload-sync.js +171 -157
  148. package/package.json +4 -4
@@ -18,6 +18,7 @@
18
18
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
19
19
  import { estimateForSize } from "./issue-contracts.js";
20
20
  const ISSUE_ACTIONS = new Set([
21
+ "mutation-status",
21
22
  "board",
22
23
  "list",
23
24
  "show",
@@ -26,14 +27,19 @@ const ISSUE_ACTIONS = new Set([
26
27
  "move",
27
28
  "comment",
28
29
  "history",
30
+ "versions",
31
+ "restore",
29
32
  ]);
30
33
  /** Every verb except `list` and `create` names ONE issue first. */
31
34
  const ISSUE_ACTIONS_NEEDING_AN_ISSUE = new Set([
35
+ "mutation-status",
32
36
  "show",
33
37
  "update",
34
38
  "move",
35
39
  "comment",
36
40
  "history",
41
+ "versions",
42
+ "restore",
37
43
  ]);
38
44
  /**
39
45
  * The closed state vocabulary, copied verbatim from
@@ -45,43 +51,60 @@ const ISSUE_ACTIONS_NEEDING_AN_ISSUE = new Set([
45
51
  */
46
52
  export const ISSUE_STATES = ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
47
53
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
54
+ const ISSUE_ALLOWED_FLAGS = [
55
+ "--request-id",
56
+ "--expected-revision",
57
+ "--home",
58
+ "--dashboard-url",
59
+ "--state",
60
+ "--assignee",
61
+ "--project",
62
+ "--label",
63
+ "--rows",
64
+ "--limit",
65
+ "--title",
66
+ "--priority",
67
+ "--size",
68
+ "--parent",
69
+ "--file",
70
+ "--version",
71
+ "--body-stdin",
72
+ "--json",
73
+ ];
74
+ const ISSUE_VALUE_FLAGS = [
75
+ "--request-id",
76
+ "--expected-revision",
77
+ "--home",
78
+ "--dashboard-url",
79
+ "--state",
80
+ "--assignee",
81
+ "--project",
82
+ "--label",
83
+ "--rows",
84
+ "--limit",
85
+ "--title",
86
+ "--priority",
87
+ "--size",
88
+ "--parent",
89
+ "--file",
90
+ "--version",
91
+ ];
48
92
  export function parseIssueArgs(args) {
49
93
  const values = parseNamedArgs(args, {
50
- allowedFlags: [
51
- "--home",
52
- "--dashboard-url",
53
- "--state",
54
- "--assignee",
55
- "--project",
56
- "--label",
57
- "--rows",
58
- "--limit",
59
- "--title",
60
- "--priority",
61
- "--size",
62
- "--parent",
63
- "--file",
64
- "--body-stdin",
65
- "--json",
66
- ],
67
- valueFlags: [
68
- "--home",
69
- "--dashboard-url",
70
- "--state",
71
- "--assignee",
72
- "--project",
73
- "--label",
74
- "--rows",
75
- "--limit",
76
- "--title",
77
- "--priority",
78
- "--size",
79
- "--parent",
80
- "--file",
81
- ],
94
+ allowedFlags: ISSUE_ALLOWED_FLAGS,
95
+ valueFlags: ISSUE_VALUE_FLAGS,
82
96
  });
83
97
  const { action, rest } = readIssueAction(values);
84
98
  const { issueRef, issueRefs, moveState } = readIssueReference(action, rest);
99
+ const requestId = optionalNonEmpty(values.flags.get("--request-id"));
100
+ const expectedRevision = optionalPositiveInteger(values.flags.get("--expected-revision"), "--expected-revision");
101
+ if (requestId && !UUID_PATTERN.test(requestId))
102
+ throw new Error("--request-id must be a UUID.");
103
+ if (action === "mutation-status" && !UUID_PATTERN.test(issueRef ?? ""))
104
+ throw new Error("mutation-status needs a request UUID.");
105
+ if ((requestId || expectedRevision !== undefined) && !["create", "update", "move", "comment", "restore"].includes(action)) {
106
+ throw new Error("Write-safety flags belong to create, update, move, comment or restore.");
107
+ }
85
108
  validateIssueState(moveState, "issue move state must be one of: ");
86
109
  const stateFilter = readIssueStateFilter(action, values);
87
110
  const title = readIssueTitle(action, values);
@@ -95,13 +118,14 @@ export function parseIssueArgs(args) {
95
118
  const estimate = readIssueSize(values);
96
119
  const assignee = readIssueAssignee(action, values);
97
120
  const limit = readIssueLimit(action, values);
98
- return buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit);
121
+ const versionNumber = readIssueVersion(action, values);
122
+ return buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber);
99
123
  }
100
124
  function readIssueAction(values) {
101
125
  const first = values.positionals[0];
102
126
  const action = (first === undefined ? "list" : first);
103
127
  if (!ISSUE_ACTIONS.has(action)) {
104
- throw new Error(`Unknown issue command: ${first}. Try board, list, show, create, update, move, comment, or history.`);
128
+ throw new Error(`Unknown issue command: ${first}. Try board, list, show, create, update, move, comment, history, versions, restore, or mutation-status.`);
105
129
  }
106
130
  return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
107
131
  }
@@ -189,16 +213,28 @@ function readIssueAssignee(action, values) {
189
213
  }
190
214
  return assignee;
191
215
  }
216
+ /** `restore` names the version it wants; there is no "restore the previous one" (BLI-21300). */
217
+ function readIssueVersion(action, values) {
218
+ const version = optionalPositiveInteger(values.flags.get("--version"), "--version");
219
+ if (action === "restore" && version === undefined) {
220
+ throw new Error("issue restore needs --version <n>; list them with `cockpit ticket versions <id>`.");
221
+ }
222
+ if (version !== undefined && action !== "restore")
223
+ throw new Error("--version belongs to `cockpit ticket restore`.");
224
+ return version;
225
+ }
192
226
  function readIssueLimit(action, values) {
193
227
  const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
194
- if (limit !== undefined && action !== "list" && action !== "history") {
195
- throw new Error("--limit belongs to `cockpit issue list` or `cockpit issue history`.");
228
+ if (limit !== undefined && action !== "list" && action !== "history" && action !== "versions") {
229
+ throw new Error("--limit belongs to `cockpit issue list`, `history` or `versions`.");
196
230
  }
197
231
  return limit;
198
232
  }
199
- function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit) {
233
+ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, stateFilter, title, priority, estimate, assignee, limit, versionNumber) {
200
234
  return {
201
235
  kind: "issue",
236
+ requestId: optionalNonEmpty(values.flags.get("--request-id")),
237
+ expectedRevision: optionalPositiveInteger(values.flags.get("--expected-revision"), "--expected-revision"),
202
238
  action,
203
239
  homeDir: optionalNonEmpty(values.flags.get("--home")),
204
240
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
@@ -211,6 +247,7 @@ function buildIssueCommand(values, action, issueRef, issueRefs, moveState, state
211
247
  rows: optionalNonEmpty(values.flags.get("--rows")),
212
248
  project: optionalNonEmpty(values.flags.get("--project")),
213
249
  ...(limit === undefined ? {} : { limit }),
250
+ ...(versionNumber === undefined ? {} : { versionNumber }),
214
251
  title,
215
252
  ...(priority === undefined ? {} : { priority }),
216
253
  ...(estimate === undefined ? {} : { estimate }),
@@ -31,6 +31,82 @@ export { normalizeUrl } from "./local-arg-values.js";
31
31
  // parser import them from this path.
32
32
  export { DOCTOR_SETUP_ALIASES, } from "./local-command-shapes.js";
33
33
  export { SCOUT_MIN_PREFIX_LENGTH, SLACK_WORKSPACE_KEYS, WORKBOOK_MIN_WIDTH, ISSUE_STATES, } from "./local-args-tower.js";
34
+ /**
35
+ * Which decision table reads the rest of `argv`, by the first word. A word
36
+ * absent from this table is "Unknown local command". Every entry is a thunk
37
+ * so an import cycle cannot hand the table an undefined parser at load time.
38
+ */
39
+ const COMMAND_PARSERS = new Map([
40
+ ["roots", (_command, rest) => parseRootsArgs(rest)],
41
+ ["onboard", (_command, rest) => parseOnboardArgs(rest)],
42
+ ["update", (command, rest) => parseUpdateArgs(command, rest)],
43
+ ["upgrade", (command, rest) => parseUpdateArgs(command, rest)],
44
+ // NOT yet aliased to the convergence command. `onboard --ticket <id>` binds
45
+ // work to a ticket, and routing it here would accept the flag and drop the
46
+ // binding with no error — the silent breakage BLI-2490 explicitly forbids.
47
+ // Aliasing lands once the convergence run honours --ticket. See BLI-2494.
48
+ ["do-everything", (command, rest) => parseDoctorArgs(command, rest)],
49
+ ["fix", (command, rest) => parseDoctorArgs(command, rest)],
50
+ // BLI-3768: `doctor` is what the ops board's RED rows, the setup receipt
51
+ // and the heartbeat all tell a person to run. It routes here exactly like
52
+ // `fix` — the check table diagnoses, then converges.
53
+ ["doctor", (command, rest) => parseDoctorArgs(command, rest)],
54
+ ["install", (_command, rest) => parseInstallArgs(rest)],
55
+ ["login", (_command, rest) => parseLoginArgs(rest)],
56
+ ["pair", (_command, rest) => parseLoginArgs(rest)],
57
+ ["logout", (_command, rest) => parseLogoutArgs(rest)],
58
+ ["start", (_command, rest) => parseStartArgs(rest)],
59
+ ["sync", (_command, rest) => parseSyncArgs(rest)],
60
+ ["analyze", (_command, rest) => parseAnalyzeArgs(rest)],
61
+ ["jarvis", (_command, rest) => parseJarvisArgs(rest)],
62
+ ["model", (_command, rest) => parseModelArgs(rest)],
63
+ ["models", (_command, rest) => parseModelsArgs(rest)],
64
+ ["scout", (_command, rest) => parseScoutArgs(rest)],
65
+ ["ci", (_command, rest) => parseCiArgs(rest)],
66
+ ["ops", (_command, rest) => parseOpsArgs(rest)],
67
+ ["slack", (_command, rest) => parseSlackArgs(rest)],
68
+ ["settings", (_command, rest) => parseSettingsArgs(rest)],
69
+ ["team", (_command, rest) => parseTeamArgs(rest)],
70
+ ["workbook", (_command, rest) => parseWorkbookArgs(rest)],
71
+ ["brief", (_command, rest) => parseBriefArgs(rest)],
72
+ ["correct", (_command, rest) => parseCorrectArgs(rest)],
73
+ ["notes", (_command, rest) => parseNotesArgs(rest)],
74
+ ["backfill", (_command, rest) => parseBackfillArgs(rest)],
75
+ ["status", (_command, rest) => parseStatusArgs(rest)],
76
+ ["sessions", (_command, rest) => parseSessionsArgs(rest)],
77
+ ["serve", (_command, rest) => parseServeArgs(rest)],
78
+ ["autostart", (_command, rest) => parseAutostartArgs(rest)],
79
+ ["agent-rules", (_command, rest) => parseAgentRulesArgs(rest)],
80
+ ["memory", (_command, rest) => parseMemoryArgs(rest)],
81
+ ["clean", (_command, rest) => parseCleanArgs(rest)],
82
+ ["sources", (_command, rest) => parseSourcesArgs(rest)],
83
+ ["glossary", (_command, rest) => parseGlossaryArgs(rest)],
84
+ ["spend", (_command, rest) => parseSpendArgs(rest)],
85
+ ["docs", (_command, rest) => parseDocsArgs(rest)],
86
+ ["msg", (_command, rest) => parseMsgArgs(rest)],
87
+ ["issue", (_command, rest) => parseIssueArgs(rest)],
88
+ ["mail", (_command, rest) => parseMailArgs(rest)],
89
+ ["cal", (_command, rest) => parseCalArgs(rest)],
90
+ ["project", (_command, rest) => parseProjectArgs(rest)],
91
+ ["search", (_command, rest) => parseSearchArgs(rest)],
92
+ ["github", (_command, rest) => parseGithubArgs(rest)],
93
+ ["release", (_command, rest) => parseReleaseArgs(rest)],
94
+ ["careers", (_command, rest) => parseCareersArgs(rest)],
95
+ ["rename", (_command, rest) => parseRenameArgs(rest)],
96
+ ["setup", (_command, rest) => parseSetupArgs(rest)],
97
+ ["usage", (_command, rest) => parseUsageArgs(rest)],
98
+ ["speed", (_command, rest) => parseSpeedArgs(rest)],
99
+ ["logs", (_command, rest) => parseLogsArgs(rest)],
100
+ ["brain", (_command, rest) => parseBrainArgs(rest)],
101
+ ["hits", (_command, rest) => parseHitsArgs(rest)],
102
+ ["intentions", (_command, rest) => parseIntentionsArgs(rest)],
103
+ ["who", (_command, rest) => parseWhoArgs(rest)],
104
+ ["people", (_command, rest) => parsePeopleArgs(rest)],
105
+ ["approvals", (_command, rest) => parseApprovalsArgs(rest)],
106
+ ["drills", (_command, rest) => parseDrillsArgs(rest)],
107
+ ["manager", (_command, rest) => parseManagerArgs(rest)],
108
+ ["image", (_command, rest) => parseImageArgs(rest)],
109
+ ]);
34
110
  export function parseLocalArgs(argv) {
35
111
  // BLI-10431: a Tower page name (`work`, `calendar`, ...) is the same parser
36
112
  // as the noun that serves the page — a second spelling, never a second door.
@@ -38,133 +114,8 @@ export function parseLocalArgs(argv) {
38
114
  if (aliasTarget)
39
115
  return parseLocalArgs([aliasTarget, ...argv.slice(1)]);
40
116
  const command = argv[0];
41
- switch (command) {
42
- case "roots": return parseRootsArgs(argv.slice(1));
43
- case "onboard":
44
- return parseOnboardArgs(argv.slice(1));
45
- case "update":
46
- case "upgrade":
47
- return parseUpdateArgs(command, argv.slice(1));
48
- // NOT yet aliased to the convergence command. `onboard --ticket <id>` binds
49
- // work to a ticket, and routing it here would accept the flag and drop the
50
- // binding with no error — the silent breakage BLI-2490 explicitly forbids.
51
- // Aliasing lands once the convergence run honours --ticket. See BLI-2494.
52
- case "do-everything":
53
- case "fix":
54
- // BLI-3768: `doctor` is what the ops board's RED rows, the setup receipt
55
- // and the heartbeat all tell a person to run. It routes here exactly like
56
- // `fix` — the check table diagnoses, then converges.
57
- case "doctor":
58
- return parseDoctorArgs(command, argv.slice(1));
59
- case "install":
60
- return parseInstallArgs(argv.slice(1));
61
- case "login":
62
- case "pair":
63
- return parseLoginArgs(argv.slice(1));
64
- case "logout":
65
- return parseLogoutArgs(argv.slice(1));
66
- case "start":
67
- return parseStartArgs(argv.slice(1));
68
- case "sync":
69
- return parseSyncArgs(argv.slice(1));
70
- case "analyze":
71
- return parseAnalyzeArgs(argv.slice(1));
72
- case "jarvis":
73
- return parseJarvisArgs(argv.slice(1));
74
- case "model":
75
- return parseModelArgs(argv.slice(1));
76
- case "models":
77
- return parseModelsArgs(argv.slice(1));
78
- case "scout":
79
- return parseScoutArgs(argv.slice(1));
80
- case "ci": return parseCiArgs(argv.slice(1));
81
- case "ops":
82
- return parseOpsArgs(argv.slice(1));
83
- case "slack":
84
- return parseSlackArgs(argv.slice(1));
85
- case "settings":
86
- return parseSettingsArgs(argv.slice(1));
87
- case "team":
88
- return parseTeamArgs(argv.slice(1));
89
- case "workbook":
90
- return parseWorkbookArgs(argv.slice(1));
91
- case "brief":
92
- return parseBriefArgs(argv.slice(1));
93
- case "correct":
94
- return parseCorrectArgs(argv.slice(1));
95
- case "notes":
96
- return parseNotesArgs(argv.slice(1));
97
- case "backfill":
98
- return parseBackfillArgs(argv.slice(1));
99
- case "status":
100
- return parseStatusArgs(argv.slice(1));
101
- case "sessions":
102
- return parseSessionsArgs(argv.slice(1));
103
- case "serve":
104
- return parseServeArgs(argv.slice(1));
105
- case "autostart":
106
- return parseAutostartArgs(argv.slice(1));
107
- case "agent-rules":
108
- return parseAgentRulesArgs(argv.slice(1));
109
- case "memory":
110
- return parseMemoryArgs(argv.slice(1));
111
- case "clean":
112
- return parseCleanArgs(argv.slice(1));
113
- case "sources":
114
- return parseSourcesArgs(argv.slice(1));
115
- case "glossary":
116
- return parseGlossaryArgs(argv.slice(1));
117
- case "spend":
118
- return parseSpendArgs(argv.slice(1));
119
- case "docs":
120
- return parseDocsArgs(argv.slice(1));
121
- case "msg":
122
- return parseMsgArgs(argv.slice(1));
123
- case "issue":
124
- return parseIssueArgs(argv.slice(1));
125
- case "mail":
126
- return parseMailArgs(argv.slice(1));
127
- case "cal":
128
- return parseCalArgs(argv.slice(1));
129
- case "project":
130
- return parseProjectArgs(argv.slice(1));
131
- case "search":
132
- return parseSearchArgs(argv.slice(1));
133
- case "github":
134
- return parseGithubArgs(argv.slice(1));
135
- case "release":
136
- return parseReleaseArgs(argv.slice(1));
137
- case "careers":
138
- return parseCareersArgs(argv.slice(1));
139
- case "rename":
140
- return parseRenameArgs(argv.slice(1));
141
- case "setup":
142
- return parseSetupArgs(argv.slice(1));
143
- case "usage":
144
- return parseUsageArgs(argv.slice(1));
145
- case "speed":
146
- return parseSpeedArgs(argv.slice(1));
147
- case "logs":
148
- return parseLogsArgs(argv.slice(1));
149
- case "brain":
150
- return parseBrainArgs(argv.slice(1));
151
- case "hits":
152
- return parseHitsArgs(argv.slice(1));
153
- case "intentions":
154
- return parseIntentionsArgs(argv.slice(1));
155
- case "who":
156
- return parseWhoArgs(argv.slice(1));
157
- case "people":
158
- return parsePeopleArgs(argv.slice(1));
159
- case "approvals":
160
- return parseApprovalsArgs(argv.slice(1));
161
- case "drills":
162
- return parseDrillsArgs(argv.slice(1));
163
- case "manager":
164
- return parseManagerArgs(argv.slice(1));
165
- case "image":
166
- return parseImageArgs(argv.slice(1));
167
- default:
168
- throw new Error(`Unknown local command: ${command ?? ""}`);
169
- }
117
+ const parse = command === undefined ? undefined : COMMAND_PARSERS.get(command);
118
+ if (!parse)
119
+ throw new Error(`Unknown local command: ${command ?? ""}`);
120
+ return parse(command ?? "", argv.slice(1));
170
121
  }
@@ -210,13 +210,24 @@ export const TOWER_OPS_COMMAND_HELP = [
210
210
  [
211
211
  "manager",
212
212
  [
213
- "Usage: cockpit manager <clients|client|run|list|queue|show|add|edit|done|reopen|link|approve|dismiss|proposal|archive|unarchive> ... [--dashboard-url <url>] [--json]",
213
+ "Usage: cockpit manager <clients|client|home|summary|run|list|queue|show|add|edit|done|reopen|link|approve|dismiss|proposal|archive|unarchive> ... [--dashboard-url <url>] [--json]",
214
214
  "",
215
- "Tower Manager's client actions and timeline items, from the terminal: the same things the manager page does.",
215
+ "Tower Manager's clients, from the terminal: each client's Home and \"Where we are\", its actions and its timeline items, the same things the manager page does.",
216
216
  " cockpit manager clients every client, with its id",
217
217
  " cockpit manager client <client> one client: state, last read, pages waiting; run <run-id> one run's receipt",
218
+ " cockpit manager home <client> the client's Home: how fresh its data is (last read, when Notion last changed), \"Where we are\" (health, headline, up to 5 dated bullets,",
219
+ " next step, blocker, who wrote it and when), live counts, what needs attention by owner (overdue and due within 7 days), what is coming up,",
220
+ " what the newest read that changed anything did, and how many sync changes wait for review (`queue <client>` lists them).",
221
+ " cockpit manager summary <client> --version <v>|none [--health on_track|at_risk|blocked] [--headline <text>] [--bullet \"YYYY-MM-DD: text\"]... [--next <text>|none] [--blocker <text>|none]",
222
+ " edit \"Where we are\": any field alone, or several at once. --bullet is given 1 to 5 times and REPLACES the whole list; a leading",
223
+ " \"YYYY-MM-DD: \" dates a bullet, without it the bullet is undated. none clears --next or --blocker. blocked needs a blocker (given, or already on the card),",
224
+ " and a blocker is kept only while the card is Blocked: with any other health it is dropped.",
225
+ " --version is summary.updatedAt from `home <client> --json`, or none when the client has no card yet (then --health, --headline and at least one --bullet are all needed).",
226
+ " Limits: headline 140 characters, each bullet 160, next and blocker 200. The edit is marked as yours and stands until the client's",
227
+ " data changes or an action goes overdue; the next check after that writes the card again (newest wins).",
218
228
  " cockpit manager list <client> [--timeline] [--needs-review] [--status open|done|dropped|closed|all] [--owner <name>|--no-owner] [--overdue] [--kind <kind>] [--search <text>] [--archived] [--review dismissed|all]",
219
229
  " actions (default), or with --timeline the timeline; --status, --owner, --no-owner and --overdue (open actions due before today) are for actions, --kind for the timeline.",
230
+ " The timeline reads top down: undated items (\"Needs a date\"), then upcoming, a Today line, then the past, newest first (today is Vancouver's day).",
220
231
  " Dismissed and archived items are hidden: --review dismissed (or all) and --archived find them. A list holds at most 500 rows and says when it is cut.",
221
232
  " cockpit manager queue <client> changes from syncs after onboarding that nobody has looked at yet (the onboarding's own items are not in it)",
222
233
  " an empty queue does not mean everything is reviewed: list <client> --needs-review --status all, and list <client> --timeline --needs-review",
@@ -239,12 +250,12 @@ export const TOWER_OPS_COMMAND_HELP = [
239
250
  "since, the write is refused with a sentence saying so and nothing is written; run show again for the new version and retry.",
240
251
  "--owners on an action takes, as the web form says, \"One or more people, separated by commas\" (\"Rowan Ash, Juniper Twig\"), and it REPLACES the whole list. Leave it out and the owners stay;",
241
252
  "--owners none clears them. A timeline item has ONE owner: --owner <name> or --owner none. The same pattern clears a date: --due none, --date none.",
242
- "Owner names are kept as written, not matched to accounts: use the spelling `list` shows, so one person stays one name. A flag you leave out is never sent, and a flag given twice is refused. A date is YYYY-MM-DD. `cockpit manager`, `manager help` and `manager --help` print this and change nothing.",
253
+ "Owner names are kept as written, not matched to accounts: use the spelling `list` shows, so one person stays one name. A flag you leave out is never sent, and a flag given twice is refused (all but summary's --bullet). A date is YYYY-MM-DD. `cockpit manager`, `manager help` and `manager --help` print this and change nothing.",
243
254
  "add always sends a request id (--request-id <uuid>, else a fresh one) and prints it. If Tower does not answer, nothing was confirmed: rerun the same command with",
244
255
  "--request-id <id> and it retries without making a duplicate.",
245
256
  "--json writes the door's body to stdout, verbatim, and nothing else (an add's body also carries requestId); a refusal is { ok: false, error, detail, refusal? } and exits 1.",
246
257
  "Progress lines on stderr start with [tag]; stdout is the answer (2>/dev/null hides them). Creating a client, syncing, Rebuild brief and merging owners are on the manager page only. Teaching: docs/architecture/tower-manager.md.",
247
- "The bli-tower MCP server has the twins: manager_clients, manager_client, manager_run, manager_list, manager_queue, manager_show, manager_add, manager_edit,",
258
+ "The bli-tower MCP server has the twins: manager_clients, manager_client, manager_home, manager_set_summary (summary), manager_run, manager_list, manager_queue, manager_show, manager_add, manager_edit,",
248
259
  "manager_done, manager_reopen, manager_link, manager_approve, manager_dismiss, manager_proposal, manager_archive, manager_unarchive.",
249
260
  ],
250
261
  ],
@@ -47,7 +47,7 @@ export const TOWER_COMMAND_HELP = [
47
47
  [
48
48
  "docs",
49
49
  [
50
- "Usage: cockpit docs [list|tree|read <id|slug> [<id|slug>...]|read --parent <id> --bodies|create|update <id>|delete <id|slug> [<id|slug>...]|link <id|slug> <notion-page>|sync-status [<id|slug>]|export <id|slug> [--format md|pdf|docx|txt] [--out <path>]] [flags]",
50
+ "Usage: cockpit docs [list|tree|read <id|slug> [<id|slug>...]|read --parent <id> --bodies|create|update <id>|delete <id|slug> [<id|slug>...]|link <id|slug> <notion-page>|sync-status [<id|slug>]|export <id|slug> [--format md|pdf|docx|txt] [--out <path>]|mutation-status <request-uuid>] [flags]",
51
51
  "",
52
52
  "The Tower document library, typed. Bare `cockpit docs` lists every document you may read.",
53
53
  "list [--parent <id>|root] [--query <text> [--word]] [--limit <n>] — every document you may see: id, visibility, body size, slug, title. Metadata only — a list never carries a body, so it stays cheap enough for an agent to call (--json was 604 KB for 155 documents before BLI-3737, and is now a few tens of KB). --json shape: { documents: [{ id, slug, title, visibility, parent_id, source, updated_at, body_chars }] }.",
@@ -62,7 +62,10 @@ export const TOWER_COMMAND_HELP = [
62
62
  " --json shape: one ref answers { document }, unchanged from before BLI-10450. Several refs answer { documents: [{ ref, ok, document } | { ref, ok: false, error, detail }] }.",
63
63
  "read --parent <id>|root --bodies [--limit <n>] — every document filed directly under that parent, bodies included, in ONE call (GET /api/docs/documents?parent_id=...&bodies=true). --limit here caps the DOCUMENT COUNT (1-50), not characters. --json shape: { documents: [{ id, ..., body_markdown }] }.",
64
64
  "create --title \"<title>\" [--parent <id>] [--visibility org|private] [--file <path>|--body-stdin] — the body comes from --file, or stdin (`cat body.md | cockpit docs create --title \"...\"`); neither means an empty body, matching the browser's own default. --json shape: { document }.",
65
- "update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] [--allow-empty] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb. --json shape: { document }.",
65
+ "update <id|slug> [--title \"<t>\"] [--visibility org|private] [--parent <id>|--clear-parent] [--file <path>|--body-stdin] [--allow-empty] [--expected-revision <n>] [--request-id <uuid>] — only the fields you pass change; a `--parent` change IS a move, there is no separate move verb. --json shape: { document, mutation }.",
66
+ " --expected-revision <n> is the page revision you read (`docs read --json` shows document.revision). The save is one atomic step: if someone saved since, nothing is written and you get revision_conflict with the current page (--json: mutation.current). Omit it for an unconditional save. Every save advances the revision.",
67
+ " --request-id <uuid> makes a retry apply once (minted and printed when absent). Reusing it with different input is request_id_reused. If the response is lost, run `cockpit docs mutation-status <uuid>`, then retry the SAME input with the SAME id.",
68
+ "mutation-status <request-uuid> — your own save receipt: committed (with the stored revision), not_committed (with the reason) or unknown_check_again (no receipt yet; it never proves nothing changed). --json shape: { mutation }.",
66
69
  " --allow-empty clears the page on purpose. A body that would empty a document that holds text is refused (refused_empty_body) unless you say so, because far more often it is a surface that lost the content than a person who meant it.",
67
70
  "delete <id|slug> [<id|slug>...] — soft-deletes one or more documents (BLI-10555): the browser's own delete, sets deleted_at, never a real SQL DELETE. Recoverable — only a super_admin clearing deleted_at brings it back, there is no self-service undo. Deleting a parent does NOT delete its children; they keep their own parent_id and stay exactly where they were. Several refs resolve against ONE shared list call, then one DELETE each — one bad ref never loses the rest. --json shape: one ref answers { ok, document_id }; several answer { documents: [{ ref, ok, document_id } | { ref, ok: false, error, detail }] }.",
68
71
  "link <id|slug> <notion-url|page-id> — ties the document to a Notion page so the two-way sync moves it both ways. The page is the truth from then on: the next sync pulls it over this copy and keeps the copy as a version. `link <id|slug> --clear` unties it. Refusals: invalid_notion_page, notion_page_already_linked, document_not_writable.",
@@ -77,13 +80,14 @@ export const TOWER_COMMAND_HELP = [
77
80
  [
78
81
  "msg",
79
82
  [
80
- "Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|send <channel>|thread <id> --channel <channel>|show <message-id>] [flags]",
83
+ "Usage: cockpit msg [channels|create <name>|dm <email>|read <channel>|unread <channel> --from <message-id>|send <channel>|thread <id> --channel <channel>|show <message-id>] [flags]",
81
84
  "",
82
85
  "Channels and messages, typed. <channel> is a channel id, or its name with or without a leading #.",
83
86
  "channels — every channel you are a member of (or, as a super_admin, every channel).",
84
87
  "create <name> [--private] [--members a@x.test,b@y.test] [--description \"<text>\"] — makes a channel and prints its id. A leading # is fine; --private means membership decides who may read it, and --members names who joins at birth BY EMAIL. An address Tower does not carry refuses the whole create — no half-built channel.",
85
88
  "dm <email> — opens (or re-opens) the direct message with one person. Idempotent: the same address always resolves to the same channel, and you never have to name yourself.",
86
- "read <channel> [--limit <n>] [--thread <id>] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead.",
89
+ "read <channel> [--limit <n>] [--thread <id>] [--mark-read] — the channel's most recent top-level messages, oldest first; --thread <id> reads one thread's replies instead. The first page also says where the messages come from (for an imported Slack channel: how long ago the archive was checked, or that it is unknown) and where YOUR read line stands. Reading never moves the read line; --mark-read moves it to the newest message shown, and says plainly if that failed.",
90
+ "unread <channel> --from <message-id> — marks that message and everything after it unread, so the web sidebar badge and the NEW MESSAGES line start there. Opening or reading a channel never clears unseen history.",
87
91
  "send <channel> [--thread <id>] — posts a message. The content is never accepted on the command line: pipe it in, e.g. `echo \"hello\" | cockpit msg send general`.",
88
92
  "thread <id> --channel <channel> [--before <cursor>] [--limit <n>] — one thread's replies by the parent message's id.",
89
93
  "show <message-id> — exact message, thread root and bounded neighbors. read/thread --before takes the preceding nextCursor; repeat until hasMore is false.",
@@ -95,7 +99,7 @@ export const TOWER_COMMAND_HELP = [
95
99
  [
96
100
  "issue",
97
101
  [
98
- "Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|history <id>] [flags]",
102
+ "Usage: cockpit issue [board|list|show <id> [<id>...]|create|update <id>|move <id> <state>|comment <id>|history <id>|versions <id>|restore <id> --version <n>|mutation-status <request-uuid>] [flags]",
99
103
  "",
100
104
  "Tower's issue tracker. <id> is a BLI-#### identifier (BLI-3654) or an issue's uuid — both work everywhere an issue is named.",
101
105
  "list [--state <s>] [--assignee me|unassigned|<uuid>] [--project <name|id>] [--limit <n>] — the issues you may see, most recently updated first. Each row carries assignee_name/assignee_email alongside the bare assignee_id (BLI-10450, one extra `cockpit team members` call for the whole page, never per row), and a `size` label (S/M/L) alongside the raw `estimate` when it matches Tower's own scale (BLI-10575). --json shape: { issues: [{ id, identifier, title, state, priority, estimate, size, assignee_id, assignee_name, assignee_email, project_id, parent_id, updated_at }] }.",
@@ -106,7 +110,11 @@ export const TOWER_COMMAND_HELP = [
106
110
  "update <id> [--title \"<t>\"] [--priority 0-4] [--size S|M|L] [--assignee me|<uuid>] [--project <name|id>] [--parent <id>] [--file <path>|--body-stdin] — only the fields you pass change. State does NOT move here; use move. --json shape: { issue }.",
107
111
  "move <id> <state> — moves an issue and records the move. States: backlog, todo, in_progress, in_review, done, canceled.",
108
112
  "comment <id> — posts a comment. The body is never taken on the command line: `echo \"shipped\" | cockpit issue comment BLI-3654`.",
113
+ "Writes accept --request-id <uuid> and --expected-revision <n>. Keep the UUID and identical fields across retries; JSON includes mutation receipt and mirror delivery separately.",
114
+ "mutation-status <request-uuid> — check your own local commit and mirror delivery after a lost response; no receipt means unknown_check_again.",
109
115
  "history <id> [--limit <n>] — every recorded state move and reassignment, oldest first.",
116
+ "versions <id> [--limit <n>] — every saved description, newest first, each with its number, id, author, source and time (BLI-21300). --json shape: { versions: [{ id, version, markdown, author_id, source, restored_from_version, created_at }] }.",
117
+ "restore <id> --version <n> [--request-id <uuid>] [--expected-revision <n>] — writes version <n>'s text back as a NEW version; nothing is rewritten, so the restore can itself be undone. A stale revision conflicts and changes nothing. --json shape: { issue, mutation }.",
110
118
  "A project may be named instead of id'd; the name is matched exactly (case-insensitively) against `cockpit project list`.",
111
119
  "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",
112
120
  "A refusal keeps Tower's own reason label — needs_rls_client, issue_not_found_or_unreadable, issue_not_writable, invalid_state, comment_too_long, and so on.",
@@ -143,7 +151,7 @@ export const TOWER_COMMAND_HELP = [
143
151
  [
144
152
  "cal",
145
153
  [
146
- 'Usage: cockpit cal [today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find "<words>"|calendars|add-ical|create|share <id>|sync <id>|sync-status [<id>]|detach <id>|delete <event id> --yes] [flags]',
154
+ 'Usage: cockpit cal [event <id>|export <id>|today|week|conflicts|calendar-change <id> --color <hex-or-provider>|next|find "<words>"|calendars|add-ical|create|duplicate <event id>|batch|undo [<change id>]|share <id>|sync <id>|sync-status [<id>]|detach <id>|delete <event id> --yes] [flags]',
147
155
  "",
148
156
  "Every calendar you attached, in one place — plus any calendar a colleague marked shared.",
149
157
  "today [--offset 1] [--tz <zone>] — what is on today, in YOUR zone (this machine's, unless --tz says otherwise). --offset 1 is tomorrow, -1 yesterday.",
@@ -151,6 +159,8 @@ export const TOWER_COMMAND_HELP = [
151
159
  "next [--hours 12] — what is coming, from RIGHT NOW rather than from midnight. Defaults to the next 72 hours, so a Friday evening still answers.",
152
160
  'find "<words>" — full text over titles, locations and descriptions. A repeating event is answered with the NEXT time it happens, not its first one in 2024.',
153
161
  "conflicts [--from <iso> --to <iso>] [--tz <IANA>] — own-account double-bookings with duplicate invitation merging, checked calendars and age. Visible calendars only by default; --all includes hidden calendars. All-day and free events never conflict.",
162
+ "event <id> [--occurrence-start <iso>] [--tz <zone>] [--json] — exact occurrence details, human recurrence, named guests/RSVP, organizer, source zone, HTTPS Join and edit capability. The selector must match a real occurrence; cancelled/missing instances are refused.",
163
+ "export <id> [--occurrence-start <iso>] [--json] — iCalendar text to stdout; redirect to event.ics. Recurring exports include the complete series and exceptions, keeping source zone/recurrence. No calendar credentials.",
154
164
  "calendar-change <id> --color <hex|provider> — persistent owner colour override, or reset to provider colour. Also accepts stdin JSON {color_override, selected}. History is retained.",
155
165
  "calendars — every calendar, its provider (google_oauth or ical_url), whether it is shared, and when it last synced. The ids other verbs take are on the last line.",
156
166
  "add-ical [--name \"<what to call it>\"] — attaches a personal calendar by its SECRET iCal ADDRESS, read from stdin. There is no flag for it:",
@@ -164,7 +174,10 @@ export const TOWER_COMMAND_HELP = [
164
174
  "sync <id> [--full] — reads that calendar now instead of waiting for the cron. --full ignores the sync token / ETag and re-reads everything.",
165
175
  "sync-status [<id>] — every calendar's health, or one's: last successful sync, the last pass's own reason, how many events it carried, failures in the last 24h, and the lag between an event's own provider clock and when Tower stored it (ticket 10730).",
166
176
  "detach <id> — removes the calendar and its stored events from Tower. The calendar itself is untouched at Google.",
167
- "change <event id> [--account <name>] [--title <title>] [--at <iso> --until <iso>] [--location <place>] — changes an event on its own account.",
177
+ "change <event id> [--account <name>] [--title <title>] [--at <iso> --until <iso>] [--location <place>] [--all-day|--timed] [--tz <zone>] [--expected-version <v>] [--idempotency-key <k>] [--reason <why>] — changes an event on its own account and prints the change id (undo it with `undo`). --all-day converts to an all-day event (--at/--until are dates, --until the day AFTER the last day); --timed converts back. --expected-version refuses (version_conflict) instead of overwriting an event that changed since you read it. Recurring events are refused until scoped edits ship.",
178
+ "duplicate <event id> [--at <iso> --until <iso>] [--calendar <id>|--account <name>] [--keep-invitees] [--idempotency-key <k>] — copies ONE non-recurring event. The copy has NO guests unless you pass --keep-invitees (Google then emails each one) and no Meet room. Same times unless --at/--until are given; --calendar/--account puts the copy on another of your calendars on purpose. The same --idempotency-key twice makes one copy.",
179
+ 'batch [--idempotency-key <k>] [--reason <why>] — up to 25 changes from JSON on stdin: {"changes":[{"event_id":"…","starts_at":"…","ends_at":"…","expected_version":"…"}]} (or a bare array; fields as `change`: title, location, all_day, time_zone). Each is its own write: the output lists every item and the ones left unchanged, and the exit code is 1 unless all were written.',
180
+ "undo [<change id>] — a NEW write that puts the recorded earlier values back (or removes the event a create or duplicate made), never a deletion of history. No id means your latest change. Refuses when the event changed again since (undo_stale), when that change was already undone, and for guest changes, cancellations and calendar moves.",
168
181
  "cancel <event id> --yes [--account <name>] — same door as delete; a named account must own the event.",
169
182
  "delete <event id> --yes — removes ONE event, AT GOOGLE first, then here. Refuses without --yes: a delete can email everybody on it, exactly like create's invitations do. Google already having it gone (404/410) is answered as success, not a refusal.",
170
183
  "--json writes one machine-readable object to stdout; every reason and receipt line stays on stderr.",