@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
@@ -9,11 +9,56 @@ const NEEDS_ROLE = ['takehome-show', 'takehome-set'];
9
9
  export function parseCareersArgs(args) {
10
10
  const valueFlags = ['--role', '--min-score', '--since', '--home', '--dashboard-url', '--decision', '--stage', '--subject', '--body-file', '--link', '--pass-score', '--account', '--auto-invite-min-score', '--attention-min-score', '--enabled', '--booking-link', '--grader-model', '--out', '--record-sent-at', '--at', '--followup-enabled', '--followup-after-days', '--takehome-window-hours'];
11
11
  const values = parseNamedArgs(args, { allowedFlags: [...valueFlags, '--json', '--now', '--force', '--clear'], valueFlags });
12
- const positionals = [...values.positionals];
13
- // `takehome show` and `settings set` are two words a person types and one
14
- // action everything downstream reads.
15
- // A bare `careers takehome` or `careers settings` reads as its `show`, the way
16
- // `team device` reads as `team device list`: the noun alone is a question.
12
+ const { verb, target } = readCareersVerb([...values.positionals]);
13
+ const rawScore = values.flags.get('--min-score');
14
+ const minScore = rawScore === undefined ? undefined : Number(rawScore);
15
+ if (minScore !== undefined && (!Number.isFinite(minScore) || minScore < 0 || minScore > 100))
16
+ throw new Error('--min-score must be 0 to 100.');
17
+ const since = optionalNonEmpty(values.flags.get('--since'));
18
+ if (since && !Number.isFinite(Date.parse(since)))
19
+ throw new Error('--since must be a date.');
20
+ const decision = optionalNonEmpty(values.flags.get('--decision'));
21
+ if (verb === 'decide' && (!decision || !DECISIONS.includes(decision)))
22
+ throw new Error(`careers decide needs --decision ${DECISIONS.join('|')}.`);
23
+ const passScore = numberFlag(values.flags.get('--pass-score'), '--pass-score', 0, 10);
24
+ const autoInviteMinScore = numberFlag(values.flags.get('--auto-invite-min-score'), '--auto-invite-min-score', 0, 100);
25
+ const attentionMinScore = numberFlag(values.flags.get('--attention-min-score'), '--attention-min-score', 0, 100);
26
+ const enabledRaw = trueOrFalseFlag(values.flags.get('--enabled'), '--enabled');
27
+ const recordSentAt = readRecordSentAt(values, verb);
28
+ const { interviewAt, clear } = readInterviewFlags(values, verb);
29
+ const followupEnabledRaw = trueOrFalseFlag(values.flags.get('--followup-enabled'), '--followup-enabled');
30
+ const followupAfterDays = wholeFlag(values.flags.get('--followup-after-days'), '--followup-after-days', 1, 60);
31
+ const takehomeWindowHours = wholeFlag(values.flags.get('--takehome-window-hours'), '--takehome-window-hours', 1, 720);
32
+ const { outPath, force } = readResumeFileFlags(values, verb);
33
+ return {
34
+ kind: 'careers', action: verb,
35
+ id: NEEDS_ID.includes(verb) ? target : undefined,
36
+ takehomeRole: NEEDS_ROLE.includes(verb) ? target : undefined,
37
+ role: optionalNonEmpty(values.flags.get('--role')), minScore, since: since ? new Date(since).toISOString() : undefined,
38
+ stage: optionalNonEmpty(values.flags.get('--stage')), decision,
39
+ subject: optionalNonEmpty(values.flags.get('--subject')), bodyFile: optionalNonEmpty(values.flags.get('--body-file')),
40
+ link: optionalNonEmpty(values.flags.get('--link')), passScore,
41
+ inviteAccountId: optionalNonEmpty(values.flags.get('--account')),
42
+ bookingLink: httpsFlag(values.flags.get('--booking-link')),
43
+ graderModel: optionalNonEmpty(values.flags.get('--grader-model')),
44
+ now: values.booleans.has('--now'),
45
+ outPath, force,
46
+ interviewAt: interviewAt ? new Date(interviewAt).toISOString() : undefined, clear: clear || undefined,
47
+ recordSentAt: recordSentAt ? new Date(recordSentAt).toISOString() : undefined,
48
+ followupEnabled: followupEnabledRaw === undefined ? undefined : followupEnabledRaw === 'true',
49
+ followupAfterDays, takehomeWindowHours,
50
+ autoInviteMinScore, attentionMinScore,
51
+ autoInviteEnabled: enabledRaw === undefined ? undefined : enabledRaw === 'true',
52
+ homeDir: optionalNonEmpty(values.flags.get('--home')), dashboardUrl: optionalUrl(values.flags.get('--dashboard-url')), json: values.booleans.has('--json'),
53
+ };
54
+ }
55
+ /**
56
+ * `takehome show` and `settings set` are two words a person types and one
57
+ * action everything downstream reads.
58
+ * A bare `careers takehome` or `careers settings` reads as its `show`, the way
59
+ * `team device` reads as `team device list`: the noun alone is a question.
60
+ */
61
+ function readCareersVerb(positionals) {
17
62
  const first = positionals[0] ?? 'list';
18
63
  const paired = first === 'takehome' || first === 'settings';
19
64
  const typed = paired
@@ -32,32 +77,28 @@ export function parseCareersArgs(args) {
32
77
  throw new Error(`careers ${verb.replace('-', ' ')} needs a role slug.`);
33
78
  if (positionals.length > 0)
34
79
  throw new Error('Unexpected careers argument.');
35
- const rawScore = values.flags.get('--min-score');
36
- const minScore = rawScore === undefined ? undefined : Number(rawScore);
37
- if (minScore !== undefined && (!Number.isFinite(minScore) || minScore < 0 || minScore > 100))
38
- throw new Error('--min-score must be 0 to 100.');
39
- const since = optionalNonEmpty(values.flags.get('--since'));
40
- if (since && !Number.isFinite(Date.parse(since)))
41
- throw new Error('--since must be a date.');
42
- const decision = optionalNonEmpty(values.flags.get('--decision'));
43
- if (verb === 'decide' && (!decision || !DECISIONS.includes(decision)))
44
- throw new Error(`careers decide needs --decision ${DECISIONS.join('|')}.`);
45
- const passScore = numberFlag(values.flags.get('--pass-score'), '--pass-score', 0, 10);
46
- const autoInviteMinScore = numberFlag(values.flags.get('--auto-invite-min-score'), '--auto-invite-min-score', 0, 100);
47
- const attentionMinScore = numberFlag(values.flags.get('--attention-min-score'), '--attention-min-score', 0, 100);
48
- const enabledRaw = optionalNonEmpty(values.flags.get('--enabled'));
49
- if (enabledRaw !== undefined && enabledRaw !== 'true' && enabledRaw !== 'false')
50
- throw new Error('--enabled must be true or false.');
51
- // BLI-12183: the second-chance follow-up. `--record-sent-at` records one sent
52
- // outside Tower and sends nothing; the three settings flags are whole numbers
53
- // and a boolean, refused here as well as on the door.
80
+ return { typed, verb, target };
81
+ }
82
+ function trueOrFalseFlag(raw, flag) {
83
+ const value = optionalNonEmpty(raw);
84
+ if (value !== undefined && value !== 'true' && value !== 'false')
85
+ throw new Error(`${flag} must be true or false.`);
86
+ return value;
87
+ }
88
+ // BLI-12183: the second-chance follow-up. `--record-sent-at` records one sent
89
+ // outside Tower and sends nothing; the three settings flags are whole numbers
90
+ // and a boolean, refused here as well as on the door.
91
+ function readRecordSentAt(values, verb) {
54
92
  const recordSentAt = optionalNonEmpty(values.flags.get('--record-sent-at'));
55
93
  if (recordSentAt !== undefined && verb !== 'followup')
56
94
  throw new Error('--record-sent-at belongs to `cockpit careers followup <id>`.');
57
95
  if (recordSentAt !== undefined && !Number.isFinite(Date.parse(recordSentAt)))
58
96
  throw new Error('--record-sent-at must be an ISO date-time, e.g. 2026-10-03T08:00:00Z.');
59
- // BLI-12198: an interview Edward booked himself. Exactly one of `--at <iso>`
60
- // (book or reschedule) or `--clear`, refused here as well as on the door.
97
+ return recordSentAt;
98
+ }
99
+ // BLI-12198: an interview Edward booked himself. Exactly one of `--at <iso>`
100
+ // (book or reschedule) or `--clear`, refused here as well as on the door.
101
+ function readInterviewFlags(values, verb) {
61
102
  const interviewAt = optionalNonEmpty(values.flags.get('--at'));
62
103
  const clear = values.booleans.has('--clear');
63
104
  if (interviewAt !== undefined && verb !== 'interview')
@@ -71,13 +112,11 @@ export function parseCareersArgs(args) {
71
112
  throw new Error('--at must be an ISO date-time with an offset, e.g. 2026-10-05T10:00:00-07:00.');
72
113
  }
73
114
  }
74
- const followupEnabledRaw = optionalNonEmpty(values.flags.get('--followup-enabled'));
75
- if (followupEnabledRaw !== undefined && followupEnabledRaw !== 'true' && followupEnabledRaw !== 'false')
76
- throw new Error('--followup-enabled must be true or false.');
77
- const followupAfterDays = wholeFlag(values.flags.get('--followup-after-days'), '--followup-after-days', 1, 60);
78
- const takehomeWindowHours = wholeFlag(values.flags.get('--takehome-window-hours'), '--takehome-window-hours', 1, 720);
79
- // The resume is the only thing this noun writes to disk, so both file flags
80
- // belong to it alone and say so rather than being accepted and ignored.
115
+ return { interviewAt, clear };
116
+ }
117
+ // The resume is the only thing this noun writes to disk, so both file flags
118
+ // belong to it alone and say so rather than being accepted and ignored.
119
+ function readResumeFileFlags(values, verb) {
81
120
  const outPath = optionalNonEmpty(values.flags.get('--out'));
82
121
  const force = values.booleans.has('--force');
83
122
  if (outPath !== undefined && verb !== 'resume')
@@ -86,27 +125,7 @@ export function parseCareersArgs(args) {
86
125
  throw new Error('--force belongs to `cockpit careers resume <id> --out <path>`.');
87
126
  if (force && outPath === undefined)
88
127
  throw new Error('--force only means something beside --out <path>: with no file to write there is nothing to overwrite.');
89
- return {
90
- kind: 'careers', action: verb,
91
- id: NEEDS_ID.includes(verb) ? target : undefined,
92
- takehomeRole: NEEDS_ROLE.includes(verb) ? target : undefined,
93
- role: optionalNonEmpty(values.flags.get('--role')), minScore, since: since ? new Date(since).toISOString() : undefined,
94
- stage: optionalNonEmpty(values.flags.get('--stage')), decision,
95
- subject: optionalNonEmpty(values.flags.get('--subject')), bodyFile: optionalNonEmpty(values.flags.get('--body-file')),
96
- link: optionalNonEmpty(values.flags.get('--link')), passScore,
97
- inviteAccountId: optionalNonEmpty(values.flags.get('--account')),
98
- bookingLink: httpsFlag(values.flags.get('--booking-link')),
99
- graderModel: optionalNonEmpty(values.flags.get('--grader-model')),
100
- now: values.booleans.has('--now'),
101
- outPath, force,
102
- interviewAt: interviewAt ? new Date(interviewAt).toISOString() : undefined, clear: clear || undefined,
103
- recordSentAt: recordSentAt ? new Date(recordSentAt).toISOString() : undefined,
104
- followupEnabled: followupEnabledRaw === undefined ? undefined : followupEnabledRaw === 'true',
105
- followupAfterDays, takehomeWindowHours,
106
- autoInviteMinScore, attentionMinScore,
107
- autoInviteEnabled: enabledRaw === undefined ? undefined : enabledRaw === 'true',
108
- homeDir: optionalNonEmpty(values.flags.get('--home')), dashboardUrl: optionalUrl(values.flags.get('--dashboard-url')), json: values.booleans.has('--json'),
109
- };
128
+ return { outPath, force };
110
129
  }
111
130
  /**
112
131
  * BLI-4461: a booking link goes into an email a stranger clicks, so the
@@ -8,63 +8,70 @@
8
8
  */
9
9
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
10
10
  import { parseDocsExportFlags } from "./local-args-docs-export.js";
11
- const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export"]);
11
+ const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status"]);
12
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
13
  // BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
13
14
  // it takes SEVERAL references now, so it no longer shares the "exactly one"
14
15
  // rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
15
16
  // (the same door, called once per ref), so it joined `read`'s branch rather
16
17
  // than this set.
17
- const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export"]);
18
+ const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status"]);
19
+ const DOCS_ALLOWED_FLAGS = [
20
+ "--home",
21
+ "--dashboard-url",
22
+ "--title",
23
+ "--parent",
24
+ "--clear-parent",
25
+ "--allow-empty",
26
+ "--expected-revision",
27
+ "--request-id",
28
+ "--visibility",
29
+ "--file",
30
+ "--body-stdin",
31
+ "--query",
32
+ "--word",
33
+ "--limit",
34
+ "--clear",
35
+ "--bodies",
36
+ "--section",
37
+ "--offset",
38
+ "--format",
39
+ "--out",
40
+ "--json",
41
+ ];
42
+ const DOCS_VALUE_FLAGS = [
43
+ "--home",
44
+ "--dashboard-url",
45
+ "--title",
46
+ "--parent",
47
+ "--expected-revision",
48
+ "--request-id",
49
+ "--visibility",
50
+ "--file",
51
+ "--query",
52
+ "--limit",
53
+ "--section",
54
+ "--offset",
55
+ "--format",
56
+ "--out",
57
+ ];
18
58
  export function parseDocsArgs(args) {
19
- const values = parseNamedArgs(args, {
20
- allowedFlags: [
21
- "--home",
22
- "--dashboard-url",
23
- "--title",
24
- "--parent",
25
- "--clear-parent",
26
- "--allow-empty",
27
- "--visibility",
28
- "--file",
29
- "--body-stdin",
30
- "--query",
31
- "--word",
32
- "--limit",
33
- "--clear",
34
- "--bodies",
35
- "--section",
36
- "--offset",
37
- "--format",
38
- "--out",
39
- "--json",
40
- ],
41
- valueFlags: [
42
- "--home",
43
- "--dashboard-url",
44
- "--title",
45
- "--parent",
46
- "--visibility",
47
- "--file",
48
- "--query",
49
- "--limit",
50
- "--section",
51
- "--offset",
52
- "--format",
53
- "--out",
54
- ],
55
- });
59
+ const values = parseNamedArgs(args, { allowedFlags: DOCS_ALLOWED_FLAGS, valueFlags: DOCS_VALUE_FLAGS });
56
60
  const { action, rest } = parseDocsAction(values.positionals);
57
61
  const link = parseDocsLink(action, rest, values.booleans.has("--clear"));
58
62
  const { docRef, docRefs } = parseDocsDocumentReference(action, link.rest);
59
- const { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset } = parseDocsFlags(action, values);
63
+ const { visibility, clearParent, parentId, allowEmpty, title, requestId, expectedRevision, query, wholeWord, limit, bodies, section, offset } = parseDocsFlags(action, values);
60
64
  if (action === "read")
61
65
  validateDocsReadTargets(docRefs, parentId, bodies);
66
+ if (action === "mutation-status" && !UUID_PATTERN.test(docRef ?? "")) {
67
+ throw new Error("docs mutation-status needs a request UUID (the --request-id you sent, or the one `docs update` printed).");
68
+ }
62
69
  // BLI‑10555: `delete` needs no `--parent`/`--bodies` shape, only at least
63
70
  // one reference — `read`'s emptier validation does not apply.
64
71
  if (action === "delete" && (!docRefs || docRefs.length === 0)) {
65
72
  throw new Error("docs delete needs one or more document ids/slugs.");
66
73
  }
67
- return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link);
74
+ return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, { requestId, expectedRevision });
68
75
  }
69
76
  /**
70
77
  * BLI-10450: `read` takes either several document references OR
@@ -116,7 +123,7 @@ function parseDocsAction(positionals) {
116
123
  const first = positionals[0];
117
124
  const action = (first === undefined ? "list" : first);
118
125
  if (!DOCS_ACTIONS.has(action)) {
119
- throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, or export.`);
126
+ throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export, or mutation-status.`);
120
127
  }
121
128
  return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
122
129
  }
@@ -150,6 +157,10 @@ function parseDocsDocumentReference(action, rest) {
150
157
  return { docRef: undefined, docRefs: undefined };
151
158
  }
152
159
  function parseDocsFlags(action, values) {
160
+ return { ...parseDocsWriteFlags(action, values), ...parseDocsReadFlags(action, values) };
161
+ }
162
+ /** The flags that decide where a document lives and what it is called. */
163
+ function parseDocsWriteFlags(action, values) {
153
164
  const visibility = optionalNonEmpty(values.flags.get("--visibility"));
154
165
  if (visibility && visibility !== "org" && visibility !== "private") {
155
166
  throw new Error('docs --visibility must be "org" or "private".');
@@ -170,10 +181,22 @@ function parseDocsFlags(action, values) {
170
181
  if (allowEmpty && action !== "update") {
171
182
  throw new Error("--allow-empty belongs to `cockpit docs update`.");
172
183
  }
184
+ // BLI-21298: the write-safety pair belongs to `update`, like the other save-shaping flags.
185
+ if ((values.flags.has("--request-id") || values.flags.has("--expected-revision")) && action !== "update") {
186
+ throw new Error("--request-id and --expected-revision belong to `cockpit docs update`.");
187
+ }
188
+ const requestId = optionalNonEmpty(values.flags.get("--request-id"));
189
+ if (requestId && !UUID_PATTERN.test(requestId))
190
+ throw new Error("--request-id must be a UUID.");
191
+ const expectedRevision = optionalPositiveInteger(values.flags.get("--expected-revision"), "--expected-revision");
173
192
  const title = optionalNonEmpty(values.flags.get("--title"));
174
193
  if (action === "create" && !title) {
175
194
  throw new Error("docs create needs --title.");
176
195
  }
196
+ return { visibility, clearParent, parentId, allowEmpty, title, requestId, expectedRevision };
197
+ }
198
+ /** The flags that narrow or slice what a list or read returns. */
199
+ function parseDocsReadFlags(action, values) {
177
200
  // BLI-3737: narrowing belongs to `list` only. `--parent` keeps its create/
178
201
  // update meaning elsewhere (which parent to file the document under); on
179
202
  // `list` the same word names which parent to list UNDER, plus the literal
@@ -205,18 +228,21 @@ function parseDocsFlags(action, values) {
205
228
  const section = optionalNonEmpty(values.flags.get("--section"));
206
229
  if (section && action !== "read")
207
230
  throw new Error("--section belongs to `cockpit docs read`.");
208
- const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
209
- let offset;
210
- if (offsetRaw !== undefined) {
211
- offset = Number(offsetRaw);
212
- if (!Number.isInteger(offset) || offset < 0)
213
- throw new Error("--offset must be a whole number 0 or greater.");
214
- }
231
+ const offset = parseDocsOffset(values.flags.get("--offset"));
215
232
  if (offset !== undefined && action !== "read")
216
233
  throw new Error("--offset belongs to `cockpit docs read`.");
217
- return { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset };
234
+ return { query, wholeWord, limit, bodies, section, offset };
235
+ }
236
+ function parseDocsOffset(raw) {
237
+ const offsetRaw = optionalNonEmpty(raw);
238
+ if (offsetRaw === undefined)
239
+ return undefined;
240
+ const offset = Number(offsetRaw);
241
+ if (!Number.isInteger(offset) || offset < 0)
242
+ throw new Error("--offset must be a whole number 0 or greater.");
243
+ return offset;
218
244
  }
219
- function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link) {
245
+ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, safety) {
220
246
  return {
221
247
  kind: "docs",
222
248
  action,
@@ -228,6 +254,8 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
228
254
  ...(parentId ? { parentId } : {}),
229
255
  clearParent,
230
256
  ...(allowEmpty ? { allowEmpty } : {}),
257
+ ...(safety.requestId ? { requestId: safety.requestId } : {}),
258
+ ...(safety.expectedRevision === undefined ? {} : { expectedRevision: safety.expectedRevision }),
231
259
  visibility: visibility,
232
260
  filePath: optionalNonEmpty(values.flags.get("--file")),
233
261
  bodyStdin: values.booleans.has("--body-stdin"),
@@ -33,26 +33,8 @@ export function parseGithubArgs(args) {
33
33
  throw new Error(`github ${action} takes a repo and a pull request number only.`);
34
34
  const pullNumber = numberText === undefined ? undefined : optionalPositiveInteger(numberText, "pull request number");
35
35
  const runId = optionalPositiveInteger(values.flags.get("--run"), "--run");
36
- if (action === "rerun") {
37
- if ((pullNumber === undefined) === (runId === undefined)) {
38
- throw new Error("github rerun needs exactly one of a pull request number or --run <id>.");
39
- }
40
- }
41
- else if (pullNumber === undefined) {
42
- throw new Error(`github ${action} needs a pull request number.`);
43
- }
44
- const only = (flag, allowed) => {
45
- if (values.flags.has(flag) && !allowed.includes(action)) {
46
- throw new Error(`${flag} belongs to \`cockpit github ${allowed.join("|")}\`.`);
47
- }
48
- };
49
- only("--claim", ["pr"]);
50
- only("--method", ["merge"]);
51
- only("--title", ["retitle"]);
52
- only("--run", ["rerun"]);
53
- if (values.flags.has("--confirm") && action === "pr") {
54
- throw new Error("--confirm belongs to merge, retitle and rerun; pr only reads.");
55
- }
36
+ requireTargetFor(action, pullNumber, runId);
37
+ refuseFlagsFromOtherActions(values, action);
56
38
  const title = optionalNonEmpty(values.flags.get("--title"));
57
39
  if (action === "retitle" && !title)
58
40
  throw new Error("github retitle needs --title \"<new title>\".");
@@ -78,4 +60,28 @@ export function parseGithubArgs(args) {
78
60
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
79
61
  json: values.booleans.has("--json"),
80
62
  };
63
+ }
64
+ function requireTargetFor(action, pullNumber, runId) {
65
+ if (action === "rerun") {
66
+ if ((pullNumber === undefined) === (runId === undefined)) {
67
+ throw new Error("github rerun needs exactly one of a pull request number or --run <id>.");
68
+ }
69
+ }
70
+ else if (pullNumber === undefined) {
71
+ throw new Error(`github ${action} needs a pull request number.`);
72
+ }
73
+ }
74
+ function refuseFlagsFromOtherActions(values, action) {
75
+ const only = (flag, allowed) => {
76
+ if (values.flags.has(flag) && !allowed.includes(action)) {
77
+ throw new Error(`${flag} belongs to \`cockpit github ${allowed.join("|")}\`.`);
78
+ }
79
+ };
80
+ only("--claim", ["pr"]);
81
+ only("--method", ["merge"]);
82
+ only("--title", ["retitle"]);
83
+ only("--run", ["rerun"]);
84
+ if (values.flags.has("--confirm") && action === "pr") {
85
+ throw new Error("--confirm belongs to merge, retitle and rerun; pr only reads.");
86
+ }
81
87
  }
@@ -37,27 +37,39 @@ export function parseHitsArgs(args) {
37
37
  }
38
38
  if (given("--weeks") && action !== "timeline")
39
39
  throw new Error("--weeks belongs to hits timeline.");
40
- if (action === "timeline") {
41
- if (given("--scope") || given("--version"))
42
- throw new Error("--scope and --version belong to hits approve.");
43
- const weeks = optionalPositiveInteger(values.flags.get("--weeks"), "--weeks");
44
- if (weeks !== undefined && weeks > 26)
45
- throw new Error("--weeks must be 26 or fewer.");
46
- return { ...common, action, ...(weeks !== undefined ? { weeks } : {}) };
47
- }
48
- if (action === "coach") {
49
- if (given("--scope") || given("--version"))
50
- throw new Error("--scope and --version belong to hits approve.");
51
- return { ...common, action };
52
- }
53
- if (action === "approve") {
54
- const scope = optionalNonEmpty(values.flags.get("--scope"));
55
- if (!scope || !SCOPE.test(scope))
56
- throw new Error("hits approve wants --scope team, agents or project:<id> (for example project:pearl-portal).");
57
- return { ...common, action, scope, ...versionOf(values.flags.get("--version")) };
58
- }
40
+ const parsed = { values, given, common };
41
+ if (action === "timeline")
42
+ return parseHitsTimeline(parsed);
43
+ if (action === "coach")
44
+ return parseHitsCoach(parsed);
45
+ if (action === "approve")
46
+ return parseHitsApprove(parsed);
47
+ return parseHitsRead(parsed);
48
+ }
49
+ function refuseApproveFlags({ given }) {
59
50
  if (given("--scope") || given("--version"))
60
51
  throw new Error("--scope and --version belong to hits approve.");
52
+ }
53
+ function parseHitsTimeline(parsed) {
54
+ refuseApproveFlags(parsed);
55
+ const weeks = optionalPositiveInteger(parsed.values.flags.get("--weeks"), "--weeks");
56
+ if (weeks !== undefined && weeks > 26)
57
+ throw new Error("--weeks must be 26 or fewer.");
58
+ return { ...parsed.common, action: "timeline", ...(weeks !== undefined ? { weeks } : {}) };
59
+ }
60
+ function parseHitsCoach(parsed) {
61
+ refuseApproveFlags(parsed);
62
+ return { ...parsed.common, action: "coach" };
63
+ }
64
+ function parseHitsApprove({ values, common }) {
65
+ const scope = optionalNonEmpty(values.flags.get("--scope"));
66
+ if (!scope || !SCOPE.test(scope))
67
+ throw new Error("hits approve wants --scope team, agents or project:<id> (for example project:pearl-portal).");
68
+ return { ...common, action: "approve", scope, ...versionOf(values.flags.get("--version")) };
69
+ }
70
+ function parseHitsRead(parsed) {
71
+ const { values, given, common } = parsed;
72
+ refuseApproveFlags(parsed);
61
73
  const whoFlags = ["--mine", "--person", "--agent", "--project"].filter(given);
62
74
  if (whoFlags.length > 1)
63
75
  throw new Error(`Pick one of --mine, --person, --agent or --project (got ${whoFlags.join(" and ")}).`);
@@ -77,7 +89,7 @@ export function parseHitsArgs(args) {
77
89
  throw new Error("--learning is read for one person: use --mine or --person <name>.");
78
90
  return {
79
91
  ...common,
80
- action,
92
+ action: "read",
81
93
  ...(who ? { who } : {}),
82
94
  ...(person ? { person } : {}),
83
95
  ...(project ? { project } : {}),
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * cockpit manager clients
5
5
  * cockpit manager client <client>
6
+ * cockpit manager home <client>
7
+ * cockpit manager summary <client> --version <v>|none [--health on_track|at_risk|blocked] [--headline <t>] [--bullet "YYYY-MM-DD: t"|"t"]... [--next <t>|none] [--blocker <t>|none]
6
8
  * cockpit manager run <run-id>
7
9
  * 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]
8
10
  * cockpit manager queue <client>
@@ -22,27 +24,36 @@
22
24
  */
23
25
  import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
24
26
  export const MANAGER_VERBS = [
25
- "clients", "client", "run", "list", "queue", "show", "add", "edit", "done", "reopen", "link",
27
+ "clients", "client", "home", "summary", "run", "list", "queue", "show", "add", "edit", "done", "reopen", "link",
26
28
  "approve", "dismiss", "proposal", "archive", "unarchive",
27
29
  ];
28
30
  export const MANAGER_ACTION_STATUSES = ["open", "done", "dropped", "closed", "all"];
29
31
  export const MANAGER_TIMELINE_KINDS = ["meeting", "document", "milestone", "internal"];
32
+ export const MANAGER_SUMMARY_HEALTHS = ["on_track", "at_risk", "blocked"];
33
+ /** The verbs whose one positional word is a client (its id or its exact name). */
34
+ const CLIENT_VERBS = new Set(["client", "home", "summary", "list", "queue", "add"]);
30
35
  const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
31
36
  const VERSION = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
32
37
  const ONE_VERB = `manager takes one verb: ${MANAGER_VERBS.join(", ")}. Example: \`cockpit manager clients\` or \`cockpit manager list <client>\`.`;
33
38
  const OWN_FLAGS = [
34
39
  "--timeline", "--needs-review", "--status", "--owner", "--owners", "--no-owner", "--kind", "--search", "--archived", "--review",
35
40
  "--title", "--detail", "--due", "--date", "--meeting", "--none", "--version", "--request-id", "--restore", "--overdue", "--source-url", "--source-note",
41
+ "--health", "--headline", "--bullet", "--next", "--blocker",
36
42
  ];
37
43
  const COMMON_FLAGS = ["--home", "--dashboard-url", "--json", "--help"];
38
44
  const VALUE_FLAGS = [
39
45
  "--status", "--owner", "--owners", "--kind", "--search", "--review", "--title", "--detail", "--due", "--date", "--meeting",
40
46
  "--version", "--request-id", "--source-url", "--source-note", "--home", "--dashboard-url",
47
+ "--health", "--headline", "--bullet", "--next", "--blocker",
41
48
  ];
49
+ /** `summary --bullet` is given once per bullet: the one value flag that may repeat. */
50
+ const REPEATABLE_FLAGS = new Set(["--bullet"]);
42
51
  /** Which flags each verb owns, so a flag on the wrong verb is refused by name rather than ignored. */
43
52
  export const FLAGS_BY_VERB = {
44
53
  clients: [],
45
54
  client: [],
55
+ home: [],
56
+ summary: ["--version", "--health", "--headline", "--bullet", "--next", "--blocker"],
46
57
  run: [],
47
58
  list: ["--timeline", "--needs-review", "--status", "--owner", "--no-owner", "--overdue", "--kind", "--search", "--archived", "--review"],
48
59
  queue: [],
@@ -62,6 +73,8 @@ export const FLAGS_BY_VERB = {
62
73
  const POSITIONALS = {
63
74
  clients: [],
64
75
  client: ["client"],
76
+ home: ["client"],
77
+ summary: ["client"],
65
78
  run: ["run id"],
66
79
  list: ["client"],
67
80
  queue: ["client"],
@@ -80,6 +93,8 @@ const POSITIONALS = {
80
93
  /** A value flag given twice would silently keep only the last; say so instead. */
81
94
  function refuseRepeatedFlags(args) {
82
95
  for (const flag of VALUE_FLAGS) {
96
+ if (REPEATABLE_FLAGS.has(flag))
97
+ continue;
83
98
  const times = args.filter((arg) => arg === flag || arg.startsWith(`${flag}=`)).length;
84
99
  if (times > 1) {
85
100
  throw new Error(flag === "--owners"
@@ -113,12 +128,12 @@ export function parseManagerArgs(args) {
113
128
  if (rest.length !== wanted.length) {
114
129
  throw new Error(wanted.length === 0
115
130
  ? `\`cockpit manager ${verb}\` takes no positional arguments.`
116
- : `\`cockpit manager ${verb}\` takes exactly: ${wanted.map((word) => `<${word}>`).join(" ")}${verb === "list" || verb === "queue" || verb === "add" || verb === "client" ? " (quote a client's name that has spaces)" : ""}.`);
131
+ : `\`cockpit manager ${verb}\` takes exactly: ${wanted.map((word) => `<${word}>`).join(" ")}${CLIENT_VERBS.has(verb) ? " (quote a client's name that has spaces)" : ""}.`);
117
132
  }
118
133
  const timeline = values.booleans.has("--timeline");
119
134
  const shape = { ...base, action: verb, timeline };
120
135
  const flag = (name) => values.flags.get(name);
121
- if (verb === "list" || verb === "queue" || verb === "add" || verb === "client")
136
+ if (CLIENT_VERBS.has(verb))
122
137
  shape.client = rest[0];
123
138
  else if (verb === "run")
124
139
  shape.runId = readId(rest[0], "A run");
@@ -128,9 +143,11 @@ export function parseManagerArgs(args) {
128
143
  shape.proposalId = rest[1];
129
144
  shape.decision = readDecision(rest[2]);
130
145
  }
131
- const needsVersion = !["clients", "client", "run", "list", "queue", "show", "add"].includes(verb);
146
+ const needsVersion = !["clients", "client", "home", "summary", "run", "list", "queue", "show", "add"].includes(verb);
132
147
  if (needsVersion)
133
148
  shape.version = readVersion(flag("--version"), verb);
149
+ if (verb === "summary")
150
+ readSummaryFields(values, shape);
134
151
  if (verb === "list")
135
152
  readListFilters(values, shape);
136
153
  if (verb === "add")
@@ -368,4 +385,68 @@ function readLink(values, shape) {
368
385
  if (!meeting && !none)
369
386
  throw new Error("`cockpit manager link` needs --meeting <timeline-id> to link, or --none to unlink.");
370
387
  shape.timelineItemId = meeting ? readId(meeting, "A timeline item") : null;
388
+ }
389
+ /** `summary`'s version: the card's `summary.updatedAt` as `home --json` gives it, or `none` when the client has no card yet. */
390
+ function readSummaryVersion(raw) {
391
+ if (raw === undefined) {
392
+ throw new Error("`cockpit manager summary` needs --version: the card's summary.updatedAt from `cockpit manager home <client> --json`, or none when the client has no card yet. It keeps you from writing over a newer card.");
393
+ }
394
+ if (isNone(raw))
395
+ return null;
396
+ if (!VERSION.test(raw))
397
+ throw new Error("--version is the card's summary.updatedAt exactly as `cockpit manager home <client> --json` gives it, like 2026-10-09T17:03:11.123456+00:00, or none.");
398
+ return raw;
399
+ }
400
+ /** `--bullet "2026-10-14: Signed the SOW"` is a dated bullet; without a leading `YYYY-MM-DD: ` it is undated. */
401
+ export function readBullet(raw) {
402
+ // A bullet pasted from a list keeps no "- " or "* " of its own.
403
+ const value = raw.trim().replace(/^[-*•]\s+/u, "");
404
+ const dated = /^(\d{4}-\d{2}-\d{2}):\s*(.*)$/s.exec(value);
405
+ const dateLike = dated ? null : /^(\d{1,4}[-/.]\d{1,2}[-/.]\d{1,4})\s*:/.exec(value);
406
+ if (dateLike)
407
+ throw new Error(`--bullet "${raw}" starts with ${dateLike[1]}, which is not a YYYY-MM-DD day. Write it like 2026-10-14, or leave the date out for an undated bullet.`);
408
+ const date = dated ? dated[1] : null;
409
+ const text = (dated ? dated[2] : value).trim();
410
+ if (date !== null && !isCalendarDate(date))
411
+ throw new Error(`--bullet "${raw}" starts with ${date}, which is not a real day. Write the date YYYY-MM-DD, like 2026-10-14, or leave it out for an undated bullet.`);
412
+ if (text === "")
413
+ throw new Error("--bullet needs words after its date. Example: --bullet \"2026-10-14: Signed the SOW\".");
414
+ return { date, text };
415
+ }
416
+ /** The card's fields. Counts, lengths and "blocked needs a blocker" are the server's rules, refused there by name. */
417
+ function readSummaryFields(values, shape) {
418
+ shape.version = readSummaryVersion(values.flags.get("--version"));
419
+ let changes = 0;
420
+ const health = values.flags.get("--health");
421
+ if (health !== undefined) {
422
+ if (!MANAGER_SUMMARY_HEALTHS.includes(health))
423
+ throw new Error(`--health wants one of ${MANAGER_SUMMARY_HEALTHS.join(", ")}.`);
424
+ shape.health = health;
425
+ changes += 1;
426
+ }
427
+ const headline = values.flags.get("--headline");
428
+ if (headline !== undefined) {
429
+ if (headline.trim() === "")
430
+ throw new Error("--headline cannot be blank.");
431
+ shape.headline = headline.trim();
432
+ changes += 1;
433
+ }
434
+ const bullets = values.flagValues.get("--bullet");
435
+ if (bullets !== undefined && bullets.length > 0) {
436
+ shape.bullets = bullets.map(readBullet);
437
+ changes += 1;
438
+ }
439
+ const next = values.flags.get("--next");
440
+ if (next !== undefined) {
441
+ shape.next = isNone(next) ? null : next.trim();
442
+ changes += 1;
443
+ }
444
+ const blocker = values.flags.get("--blocker");
445
+ if (blocker !== undefined) {
446
+ shape.blocker = isNone(blocker) ? null : blocker.trim();
447
+ changes += 1;
448
+ }
449
+ if (changes === 0) {
450
+ throw new Error("`cockpit manager summary` has nothing to change. Give at least one of --health, --headline, --bullet (1 to 5 of them, replacing the list), --next, --blocker. A flag you leave out keeps its value; none clears --next or --blocker.");
451
+ }
371
452
  }