@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
@@ -7,33 +7,20 @@
7
7
  * `jarvis-render.ts` — imported one direction only.
8
8
  */
9
9
  import { colorEnabled, dim, writeLine } from "./cli-io.js";
10
- import { buildJarvisAnswerEnvelope } from "./jarvis-answer-envelope.js";
11
10
  import { readAttachedImage } from "./jarvis-attachment.js";
11
+ import { logAnsweredTurn, printJsonTurn, printTerminalTurn, } from "./jarvis-turn-report.js";
12
+ import { createLiveAnswer } from "./jarvis-live-answer.js";
12
13
  import { rememberTurnTrace } from "./jarvis-trace.js";
13
14
  import { towerFailureDetail, towerJsonRequest, towerRequest } from "../tower-client.js";
14
15
  import { readTowerTurn, streamFailureDetail } from "../tower-stream.js";
15
- import { activityToTrace, agentSurface, ANSWER_UPDATING_LINE, buildAttachmentForm, clientTimingFields, createLiveAnswer, latencyFields, latencyLogFields, TURN_DEADLINE_MS, writeActivityLine, writeAttachmentRefusal, writeFailure, writeModelReceipt, writeRevision, writeTraceBlock, } from "./jarvis-render.js";
16
+ import { activityToTrace, agentSurface, ANSWER_UPDATING_LINE, buildAttachmentForm, TURN_DEADLINE_MS, writeActivityLine, writeAttachmentRefusal, writeFailure, } from "./jarvis-render.js";
17
+ const FAILED_TURN = { exitCode: 1, reply: null };
16
18
  export async function sendOneTurn(context, prompt, io) {
17
19
  const startedAt = Date.now();
18
- // BLI-3414: an attached file is read and locally screened (exists,
19
- // readable, a supported extension, under the byte ceiling) BEFORE any
20
- // network call — a refusal here never reaches the dashboard and is
21
- // terminal, same discipline as the panel's own attached-image gate.
22
- let attachment = null;
23
- if (context.command.imagePath) {
24
- const read = await readAttachedImage(context.command.imagePath);
25
- if (!read.ok) {
26
- writeAttachmentRefusal(context.command, io, read.refusal, context.command.imagePath);
27
- return { exitCode: 1, reply: null };
28
- }
29
- attachment = read;
30
- }
31
- // BLI-3457: streaming is the default. A dashboard that has not shipped the
32
- // NDJSON half yet answers `application/json`, which the reader takes as a
33
- // single final event and names `stream_not_available` — so the terminal
34
- // works against both server versions with no flag.
35
- const wantsStream = context.command.stream !== false;
36
- const log = (line) => writeLine(io.stderr, line);
20
+ const attached = await readTurnAttachment(context.command, io);
21
+ if (!attached.ok)
22
+ return FAILED_TURN;
23
+ const { attachment } = attached;
37
24
  // BLI-3591: everything this turn did before the question left the machine.
38
25
  // There is no self-update probe, no floor check and no settings fetch on
39
26
  // this path — the ONLY awaits between the command starting and the POST are
@@ -41,12 +28,57 @@ export async function sendOneTurn(context, prompt, io) {
41
28
  // all three are named. If this number is ever large, the step that made it
42
29
  // large is on the same line.
43
30
  const preRequestMs = Date.now() - startedAt;
31
+ const log = (line) => writeLine(io.stderr, line);
32
+ const sent = await postTurn(context, io, prompt, attachment, log);
33
+ if (!sent)
34
+ return FAILED_TURN;
35
+ const read = await readStreamedTurn(context, io, sent, startedAt, log);
36
+ if (!read.ok)
37
+ return FAILED_TURN;
38
+ const { turn, liveTraceLines } = read;
39
+ const body = turn.final;
40
+ const httpStatus = typeof turn.final.httpStatus === "number" ? turn.final.httpStatus : sent.response.status;
41
+ if (httpStatus >= 400 || !body.ok || !body.reply) {
42
+ read.live.abandon();
43
+ const reason = body.error ?? body.reply ?? `http_${httpStatus}`;
44
+ writeFailure(context.command, io, "turn_failed", reason);
45
+ return FAILED_TURN;
46
+ }
47
+ // A streaming server may leave the settled trace out of the final event
48
+ // because it already sent every step live; the activity we collected is that
49
+ // same trace, so `--json` still gets one.
50
+ const trace = body.trace ?? activityToTrace(turn.activity);
51
+ // BLI-3560: bookmark this turn so `cockpit jarvis --trace last` can open its
52
+ // step tree. Written before the reply is printed for no reason other than
53
+ // keeping the failure — which is only ever a stderr line — above the answer
54
+ // rather than after it.
55
+ await rememberTurnTrace({ traceId: body.traceId ?? null, threadId: body.traceThread ?? null }, io, context.command.homeDir);
56
+ const settled = {
57
+ context, io, prompt, body: body, trace, attachment, sent, turn,
58
+ startedAt, preRequestMs, liveTraceLines: liveTraceLines(),
59
+ };
60
+ if (context.command.json) {
61
+ printJsonTurn(settled);
62
+ }
63
+ else {
64
+ printTerminalTurn(settled, read.live);
65
+ }
66
+ logAnsweredTurn(settled);
67
+ return { exitCode: 0, reply: body.reply };
68
+ }
69
+ /** Posts the question to the dashboard; null (after saying why) when the request failed. */
70
+ async function postTurn(context, io, prompt, attachment, log) {
71
+ // BLI-3457: streaming is the default. A dashboard that has not shipped the
72
+ // NDJSON half yet answers `application/json`, which the reader takes as a
73
+ // single final event and names `stream_not_available` — so the terminal
74
+ // works against both server versions with no flag.
75
+ const wantsStream = context.command.stream !== false;
44
76
  // BLI-3755: never silently. A script that asked for the code is a script
45
77
  // that can spend a human's approval, and the one line saying so is here.
46
78
  if (context.command.json && context.command.showApprovalCode) {
47
79
  log("[jarvis] approval code requested by a --json caller (--show-approval-code)");
48
80
  }
49
- const requested = await towerRequest({
81
+ const sent = await towerRequest({
50
82
  dashboardUrl: context.dashboardUrl,
51
83
  path: "/api/jarvis/cli",
52
84
  deviceToken: context.deviceToken,
@@ -56,40 +88,67 @@ export async function sendOneTurn(context, prompt, io) {
56
88
  headers: wantsStream ? { accept: "application/x-ndjson" } : {},
57
89
  body: attachment
58
90
  ? buildAttachmentForm(context.command, prompt, attachment)
59
- : {
60
- question: prompt,
61
- thread: context.command.thread,
62
- subject: context.command.subject,
63
- model: context.command.model,
64
- // BLI-3484: which day's page this turn is about. Sent verbatim — the
65
- // dashboard decides what counts as a date and whose day it is.
66
- date: context.command.date,
67
- // BLI-3567: the answer this session last printed, so a correction can
68
- // rewrite the paragraph it contradicts. Absent on a one-shot turn and
69
- // on the first turn of a session, which is how the dashboard knows
70
- // there is nothing above to revise.
71
- previousAnswer: context.previous?.answer,
72
- previousQuestion: context.previous?.question,
73
- // BLI-3755: who is reading this answer. A `--json` turn is a
74
- // PROGRAM's, so the coding arm withholds its approval code and
75
- // answers with a proposal id instead; an interactive turn is a
76
- // person's and is unchanged. `--show-approval-code` opts a script
77
- // back in, and says so on stderr below.
78
- surface: agentSurface(context.command) ? "agent" : undefined,
79
- },
91
+ : buildQuestionBody(context, prompt),
80
92
  log,
81
93
  });
82
- if (!requested.ok) {
83
- writeFailure(context.command, io, requested.reason, towerFailureDetail(requested.reason, requested.detail));
84
- return { exitCode: 1, reply: null };
94
+ if (!sent.ok) {
95
+ writeFailure(context.command, io, sent.reason, towerFailureDetail(sent.reason, sent.detail));
96
+ return null;
97
+ }
98
+ return sent;
99
+ }
100
+ /**
101
+ * BLI-3414: an attached file is read and locally screened (exists,
102
+ * readable, a supported extension, under the byte ceiling) BEFORE any
103
+ * network call — a refusal here never reaches the dashboard and is
104
+ * terminal, same discipline as the panel's own attached-image gate.
105
+ */
106
+ async function readTurnAttachment(command, io) {
107
+ if (!command.imagePath)
108
+ return { ok: true, attachment: null };
109
+ const read = await readAttachedImage(command.imagePath);
110
+ if (!read.ok) {
111
+ writeAttachmentRefusal(command, io, read.refusal, command.imagePath);
112
+ return { ok: false };
85
113
  }
114
+ return { ok: true, attachment: read };
115
+ }
116
+ function buildQuestionBody(context, prompt) {
117
+ return {
118
+ question: prompt,
119
+ thread: context.command.thread,
120
+ subject: context.command.subject,
121
+ model: context.command.model,
122
+ // BLI-3484: which day's page this turn is about. Sent verbatim — the
123
+ // dashboard decides what counts as a date and whose day it is.
124
+ date: context.command.date,
125
+ // BLI-3567: the answer this session last printed, so a correction can
126
+ // rewrite the paragraph it contradicts. Absent on a one-shot turn and
127
+ // on the first turn of a session, which is how the dashboard knows
128
+ // there is nothing above to revise.
129
+ previousAnswer: context.previous?.answer,
130
+ previousQuestion: context.previous?.question,
131
+ // BLI-3755: who is reading this answer. A `--json` turn is a
132
+ // PROGRAM's, so the coding arm withholds its approval code and
133
+ // answers with a proposal id instead; an interactive turn is a
134
+ // person's and is unchanged. `--show-approval-code` opts a script
135
+ // back in, and says so on stderr below.
136
+ surface: agentSurface(context.command) ? "agent" : undefined,
137
+ };
138
+ }
139
+ /**
140
+ * Reads the turn off the wire, drawing live words and trace rows as they land.
141
+ * `liveTraceLines` is a function because the count keeps growing until the
142
+ * stream ends; it is read once, after.
143
+ */
144
+ async function readStreamedTurn(context, io, sent, startedAt, log) {
86
145
  // Live trace lines go to a person as they land, never to a `--json`
87
146
  // consumer: that contract is exactly one object on stdout, so the events are
88
147
  // buffered and folded into the final payload instead. The same rule governs
89
148
  // the answer's own words (BLI-3517): `--json` stays exactly one object.
90
149
  let liveTraceLines = 0;
91
150
  const live = createLiveAnswer(context.command, io);
92
- const turn = await readTowerTurn(requested.response, {
151
+ const turn = await readTowerTurn(sent.response, {
93
152
  startedAt,
94
153
  log,
95
154
  onActivity: (event) => {
@@ -122,142 +181,9 @@ export async function sendOneTurn(context, prompt, io) {
122
181
  if (!turn.ok) {
123
182
  live.abandon();
124
183
  writeFailure(context.command, io, turn.reason, streamFailureDetail(turn.reason, turn.detail));
125
- return { exitCode: 1, reply: null };
126
- }
127
- const body = turn.final;
128
- const httpStatus = typeof turn.final.httpStatus === "number" ? turn.final.httpStatus : requested.response.status;
129
- if (httpStatus >= 400 || !body.ok || !body.reply) {
130
- live.abandon();
131
- const reason = body.error ?? body.reply ?? `http_${httpStatus}`;
132
- writeFailure(context.command, io, "turn_failed", reason);
133
- return { exitCode: 1, reply: null };
184
+ return { ok: false };
134
185
  }
135
- // A streaming server may leave the settled trace out of the final event
136
- // because it already sent every step live; the activity we collected is that
137
- // same trace, so `--json` still gets one.
138
- const trace = body.trace ?? activityToTrace(turn.activity);
139
- // BLI-3560: bookmark this turn so `cockpit jarvis --trace last` can open its
140
- // step tree. Written before the reply is printed for no reason other than
141
- // keeping the failure — which is only ever a stderr line — above the answer
142
- // rather than after it.
143
- await rememberTurnTrace({ traceId: body.traceId ?? null, threadId: body.traceThread ?? null }, io, context.command.homeDir);
144
- if (context.command.json) {
145
- writeLine(io.stdout, JSON.stringify({
146
- // BLI-3732: the shared envelope FIRST — `answer`, `sources`,
147
- // `turn_id`, `thread_id`, `trace_thread_id`, `degraded`,
148
- // `degraded_reasons` — so a script here and an agent on the
149
- // `jarvis_ask` MCP tool read one contract for one turn. The keys
150
- // below it are the ones this command has always printed and are
151
- // untouched; `reply` and `answer` are the same string.
152
- ...buildJarvisAnswerEnvelope({
153
- reply: body.reply,
154
- thread: body.thread ?? context.command.thread,
155
- traceId: body.traceId,
156
- traceThread: body.traceThread,
157
- modelFallback: body.model?.fallback,
158
- revised: body.revised,
159
- trace,
160
- proposalId: body.proposalId,
161
- // BLI-3787: the sentence beside it — what a person must do next when
162
- // the arm proposed something, and when it refused instead.
163
- nextAction: body.nextAction,
164
- // BLI-3770: the door's own structured citations. Absent from a
165
- // dashboard that predates it, and the envelope falls back to
166
- // reading the answer's own `Source:` lines when it is.
167
- sources: body.sources,
168
- }),
169
- reply: body.reply,
170
- thread: body.thread ?? context.command.thread,
171
- model: body.model ?? null,
172
- trace,
173
- subject: body.subject ?? null,
174
- // BLI-3582: the server's own split of the wait. `null` from a
175
- // dashboard that does not measure it yet.
176
- latency: latencyFields(body.latency),
177
- // BLI-3591: this side's own half of the same wait, so a consumer can
178
- // put the whole journey together without a stopwatch of its own.
179
- clientLatency: {
180
- bootMs: context.boot.bootMs,
181
- sessionMs: context.boot.sessionMs,
182
- promptMs: context.boot.promptMs,
183
- preRequestMs,
184
- respondedMs: requested.respondedMs,
185
- firstByteMs: turn.timing.firstByteMs,
186
- firstFrameMs: turn.timing.firstFrameMs,
187
- firstTokenMs: turn.timing.firstTokenMs,
188
- elapsedMs: Date.now() - startedAt,
189
- },
190
- ...(body.traceId ? { traceId: body.traceId } : {}),
191
- }));
192
- }
193
- else {
194
- const subject = body.subject?.displayName ? ` (${body.subject.displayName})` : "";
195
- // BLI-3517: when the answer streamed, `settle` prints only what the live
196
- // words did not already say — and says so out loud when the grounding gate
197
- // took some of them back. When nothing streamed it prints the whole reply
198
- // through `writeReply`, which is byte-for-byte what this command printed
199
- // before, BLI-3570's dim link line included.
200
- live.settle(body.reply, subject, body.revised === true);
201
- // BLI-3567: what the correction did to the answer above, printed under
202
- // this turn's reply because that is the only place a terminal has.
203
- if (body.revision)
204
- writeRevision(context.command, io, body.revision);
205
- // Only when nothing was drawn live — otherwise every tool would print twice.
206
- if (liveTraceLines === 0)
207
- writeTraceBlock(io, trace);
208
- writeModelReceipt(io, body.model);
209
- }
210
- writeLine(io.stderr, `[jarvis cli] answered ${JSON.stringify({
211
- prompt_length: prompt.length,
212
- reply_length: body.reply.length,
213
- trace_steps: trace?.length ?? 0,
214
- trace_failed: trace?.filter((step) => step.status === "failed").length ?? 0,
215
- model_requested: context.command.model ?? null,
216
- elapsed_ms: Date.now() - startedAt,
217
- // BLI-3786: `minted` is a conversation the DOOR opened for this call
218
- // because nothing named one — read off the answer rather than guessed
219
- // from the name, so this side keeps no copy of the server's rule. Never
220
- // the thread name itself.
221
- thread: body.thread && body.thread !== context.command.thread
222
- ? "minted"
223
- : context.command.thread === "main"
224
- ? "default"
225
- : "named",
226
- subject: context.command.subject ? "selected" : "caller",
227
- image_attached: attachment !== null,
228
- image_byte_size: attachment?.bytes.byteLength ?? null,
229
- streamed: turn.streamed,
230
- live_trace_lines: liveTraceLines,
231
- // BLI-3517: whether the words arrived live, and whether the settled
232
- // answer superseded them. `streamed_chars: 0` against a streaming
233
- // dashboard means the answer landed all at once.
234
- streamed_chars: turn.draft.length,
235
- revised: body.revised === true,
236
- // BLI-3567: whether this turn was told there was an answer above it, and
237
- // what the correction did to it. `sent_previous_answer: false` is a
238
- // one-shot turn; a status with no revision at all is a dashboard that
239
- // predates the step.
240
- sent_previous_answer: Boolean(context.previous),
241
- revision: body.revision?.status ?? null,
242
- revision_reason: body.revision?.reason ?? null,
243
- // BLI-3582: `elapsed_ms` above is the whole wait as this side felt it;
244
- // these three say which part of it was the dashboard's prep, which was
245
- // the model's first token, and how long the socket stayed silent before
246
- // the first byte. All null against a dashboard that does not send them.
247
- ...latencyLogFields(body.latency),
248
- // BLI-3591: the terminal's own half, on the same line, so the gap
249
- // between the dashboard's first token and the first character a person
250
- // sees stops being a mystery with nobody's name on it. `boot_ms` and
251
- // `session_ms` are process facts (identical on every turn of an
252
- // interactive session); the rest are this turn's, measured from the
253
- // moment the turn started.
254
- ...clientTimingFields(context.boot, {
255
- preRequestMs,
256
- respondedMs: requested.respondedMs,
257
- timing: turn.timing,
258
- }),
259
- })}`);
260
- return { exitCode: 0, reply: body.reply };
186
+ return { ok: true, turn, live, liveTraceLines: () => liveTraceLines };
261
187
  }
262
188
  /**
263
189
  * `--threads` and `--thread <name> --history`.
@@ -57,29 +57,38 @@ export async function runJarvis(command, io) {
57
57
  homeDir: command.homeDir,
58
58
  }, io);
59
59
  }
60
+ const connection = { dashboardUrl, deviceToken: session.device_token, bootMs, sessionMs };
60
61
  const promptStartedAt = Date.now();
61
62
  const oneShotPrompt = await resolveOneShotPrompt(command, io);
62
63
  if (oneShotPrompt !== null) {
63
- // A one-shot turn has nothing above it — this process printed no earlier
64
- // answer — so it sends no previous answer and nothing is ever revised. The
65
- // `sent_previous_answer: false` field on the answered line says so.
66
- const turn = await sendOneTurn({
67
- command,
68
- dashboardUrl,
69
- deviceToken: session.device_token,
70
- boot: {
71
- bootMs,
72
- sessionMs,
73
- // Only meaningful when the question was piped; an argument costs
74
- // nothing to read and reporting a stdin wait for it would be a lie.
75
- promptMs: command.prompt ? null : Date.now() - promptStartedAt,
76
- },
77
- }, oneShotPrompt, io);
78
- return turn.exitCode;
64
+ return sendOneShotTurn(command, io, connection, oneShotPrompt, promptStartedAt);
79
65
  }
80
66
  if (command.json) {
81
67
  throw new Error("cockpit jarvis --json needs --prompt, positional text, or piped stdin.");
82
68
  }
69
+ return chatInTerminal(command, io, connection);
70
+ }
71
+ async function sendOneShotTurn(command, io, connection, prompt, promptStartedAt) {
72
+ // A one-shot turn has nothing above it — this process printed no earlier
73
+ // answer — so it sends no previous answer and nothing is ever revised. The
74
+ // `sent_previous_answer: false` field on the answered line says so.
75
+ const turn = await sendOneTurn({
76
+ command,
77
+ dashboardUrl: connection.dashboardUrl,
78
+ deviceToken: connection.deviceToken,
79
+ boot: {
80
+ bootMs: connection.bootMs,
81
+ sessionMs: connection.sessionMs,
82
+ // Only meaningful when the question was piped; an argument costs
83
+ // nothing to read and reporting a stdin wait for it would be a lie.
84
+ promptMs: command.prompt ? null : Date.now() - promptStartedAt,
85
+ },
86
+ }, prompt, io);
87
+ return turn.exitCode;
88
+ }
89
+ /** The interactive loop: read a line, send it as a turn, repeat until `/exit`. */
90
+ async function chatInTerminal(command, io, connection) {
91
+ const { dashboardUrl, deviceToken, bootMs, sessionMs } = connection;
83
92
  writeLine(io.stdout, "JARVIS terminal chat. Type /exit to leave.");
84
93
  // BLI-3567: the answer this session last printed, and the question it
85
94
  // answered. It is what a correction typed on the next line can rewrite —
@@ -97,7 +106,7 @@ export async function runJarvis(command, io) {
97
106
  const turn = await sendOneTurn({
98
107
  command,
99
108
  dashboardUrl,
100
- deviceToken: session.device_token,
109
+ deviceToken,
101
110
  // The typing wait belongs to the person, not to the turn, so no prompt
102
111
  // span is reported for an interactive turn.
103
112
  boot: { bootMs, sessionMs, promptMs: null },
@@ -13,46 +13,45 @@
13
13
  */
14
14
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
15
15
  import { assertNoPositionals, normalizeUrl, optionalEmail, optionalNonEmpty, optionalNonEmptyList, optionalPositiveInteger, optionalUrl, parseNamedArgs, workRootFlagValue, workRootFlagValues, } from "./local-arg-values.js";
16
+ const ONBOARD_ALLOWED_FLAGS = [
17
+ "--home",
18
+ "--repo",
19
+ "--workspace",
20
+ "--dashboard-url",
21
+ "--email",
22
+ "--device-name",
23
+ "--ticket",
24
+ "--branch",
25
+ "--json",
26
+ "--no-auth",
27
+ "--poll-interval-ms",
28
+ "--timeout-ms",
29
+ "--max-depth",
30
+ "--max-repos",
31
+ "--allow-home-root",
32
+ // BLI-3731, the one-sign-in ceremony. Same three flags as `login`, so a
33
+ // person who learned them on one door does not relearn them on another.
34
+ "--pair",
35
+ "--no-browser",
36
+ "--legacy-pair",
37
+ ];
38
+ const ONBOARD_VALUE_FLAGS = [
39
+ "--home",
40
+ "--repo",
41
+ "--workspace",
42
+ "--dashboard-url",
43
+ "--email",
44
+ "--device-name",
45
+ "--ticket",
46
+ "--branch",
47
+ "--poll-interval-ms",
48
+ "--timeout-ms",
49
+ "--max-depth",
50
+ "--max-repos",
51
+ "--pair",
52
+ ];
16
53
  function parseOnboardLikeArgs(args, command) {
17
- const values = parseNamedArgs(args, {
18
- allowedFlags: [
19
- "--home",
20
- "--repo",
21
- "--workspace",
22
- "--dashboard-url",
23
- "--email",
24
- "--device-name",
25
- "--ticket",
26
- "--branch",
27
- "--json",
28
- "--no-auth",
29
- "--poll-interval-ms",
30
- "--timeout-ms",
31
- "--max-depth",
32
- "--max-repos",
33
- "--allow-home-root",
34
- // BLI-3731, the one-sign-in ceremony. Same three flags as `login`, so a
35
- // person who learned them on one door does not relearn them on another.
36
- "--pair",
37
- "--no-browser",
38
- "--legacy-pair",
39
- ],
40
- valueFlags: [
41
- "--home",
42
- "--repo",
43
- "--workspace",
44
- "--dashboard-url",
45
- "--email",
46
- "--device-name",
47
- "--ticket",
48
- "--branch",
49
- "--poll-interval-ms",
50
- "--timeout-ms",
51
- "--max-depth",
52
- "--max-repos",
53
- "--pair",
54
- ],
55
- });
54
+ const values = parseNamedArgs(args, { allowedFlags: ONBOARD_ALLOWED_FLAGS, valueFlags: ONBOARD_VALUE_FLAGS });
56
55
  assertNoPositionals(values.positionals, command);
57
56
  return {
58
57
  homeDir: optionalNonEmpty(values.flags.get("--home")),
@@ -86,55 +85,54 @@ export function parseUpdateArgs(alias, args) {
86
85
  ...parseOnboardLikeArgs(args, alias),
87
86
  };
88
87
  }
88
+ const DOCTOR_ALLOWED_FLAGS = [
89
+ "--home",
90
+ "--repo",
91
+ "--workspace",
92
+ "--dashboard-url",
93
+ "--update-tag",
94
+ "--dry-run",
95
+ "--check",
96
+ "--no-repair",
97
+ "--no-usage-telemetry",
98
+ "--lock-wait",
99
+ "--backfill-budget",
100
+ "--json",
101
+ "--allow-home-root",
102
+ "--max-depth",
103
+ "--max-repos",
104
+ // Accepted and ignored: the convergence run works these out itself.
105
+ // Rejecting them would break existing DMs and runbooks for no gain.
106
+ "--email",
107
+ "--device-name",
108
+ "--ticket",
109
+ "--branch",
110
+ "--no-auth",
111
+ "--poll-interval-ms",
112
+ "--timeout-ms",
113
+ ];
114
+ const DOCTOR_VALUE_FLAGS = [
115
+ "--lock-wait",
116
+ "--backfill-budget",
117
+ "--home",
118
+ "--repo",
119
+ "--workspace",
120
+ "--dashboard-url",
121
+ "--update-tag",
122
+ "--max-depth",
123
+ "--max-repos",
124
+ "--email",
125
+ "--device-name",
126
+ "--ticket",
127
+ "--branch",
128
+ "--poll-interval-ms",
129
+ "--timeout-ms",
130
+ ];
89
131
  export function parseDoctorArgs(alias, args) {
90
132
  // The allowed set is the UNION of what the six setup doors used to accept.
91
133
  // An alias that silently rejected a flag its own docs told people to pass
92
134
  // would be a worse dead end than the one we are removing.
93
- const values = parseNamedArgs(args, {
94
- allowedFlags: [
95
- "--home",
96
- "--repo",
97
- "--workspace",
98
- "--dashboard-url",
99
- "--update-tag",
100
- "--dry-run",
101
- "--check",
102
- "--no-repair",
103
- "--no-usage-telemetry",
104
- "--lock-wait",
105
- "--backfill-budget",
106
- "--json",
107
- "--allow-home-root",
108
- "--max-depth",
109
- "--max-repos",
110
- // Accepted and ignored: the convergence run works these out itself.
111
- // Rejecting them would break existing DMs and runbooks for no gain.
112
- "--email",
113
- "--device-name",
114
- "--ticket",
115
- "--branch",
116
- "--no-auth",
117
- "--poll-interval-ms",
118
- "--timeout-ms",
119
- ],
120
- valueFlags: [
121
- "--lock-wait",
122
- "--backfill-budget",
123
- "--home",
124
- "--repo",
125
- "--workspace",
126
- "--dashboard-url",
127
- "--update-tag",
128
- "--max-depth",
129
- "--max-repos",
130
- "--email",
131
- "--device-name",
132
- "--ticket",
133
- "--branch",
134
- "--poll-interval-ms",
135
- "--timeout-ms",
136
- ],
137
- });
135
+ const values = parseNamedArgs(args, { allowedFlags: DOCTOR_ALLOWED_FLAGS, valueFlags: DOCTOR_VALUE_FLAGS });
138
136
  assertNoPositionals(values.positionals, alias);
139
137
  const updateTag = optionalNonEmpty(values.flags.get("--update-tag"));
140
138
  if (updateTag && !/^[a-z0-9][a-z0-9._-]*$/u.test(updateTag)) {
@@ -9,45 +9,44 @@
9
9
  */
10
10
  import { IntentSourceSchema, WorkIntentSchema, WorkPhaseSchema, } from "@bli-cockpit/telemetry-core";
11
11
  import { assertNoPositionals, optionalConfidence, optionalNonEmpty, optionalNonNegativeInteger, optionalPositiveInteger, optionalSchemaValue, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
12
+ const START_ALLOWED_FLAGS = [
13
+ "--home",
14
+ "--repo",
15
+ "--workspace",
16
+ "--branch",
17
+ "--ticket",
18
+ "--clear-ticket",
19
+ "--topic",
20
+ "--topic-summary",
21
+ "--intent",
22
+ "--phase",
23
+ "--intent-source",
24
+ "--intent-confidence",
25
+ "--operator-id",
26
+ "--session-id",
27
+ "--json",
28
+ "--max-depth",
29
+ "--max-repos",
30
+ ];
31
+ const START_VALUE_FLAGS = [
32
+ "--home",
33
+ "--repo",
34
+ "--workspace",
35
+ "--branch",
36
+ "--ticket",
37
+ "--topic",
38
+ "--topic-summary",
39
+ "--intent",
40
+ "--phase",
41
+ "--intent-source",
42
+ "--intent-confidence",
43
+ "--operator-id",
44
+ "--session-id",
45
+ "--max-depth",
46
+ "--max-repos",
47
+ ];
12
48
  export function parseStartArgs(args) {
13
- const values = parseNamedArgs(args, {
14
- allowedFlags: [
15
- "--home",
16
- "--repo",
17
- "--workspace",
18
- "--branch",
19
- "--ticket",
20
- "--clear-ticket",
21
- "--topic",
22
- "--topic-summary",
23
- "--intent",
24
- "--phase",
25
- "--intent-source",
26
- "--intent-confidence",
27
- "--operator-id",
28
- "--session-id",
29
- "--json",
30
- "--max-depth",
31
- "--max-repos",
32
- ],
33
- valueFlags: [
34
- "--home",
35
- "--repo",
36
- "--workspace",
37
- "--branch",
38
- "--ticket",
39
- "--topic",
40
- "--topic-summary",
41
- "--intent",
42
- "--phase",
43
- "--intent-source",
44
- "--intent-confidence",
45
- "--operator-id",
46
- "--session-id",
47
- "--max-depth",
48
- "--max-repos",
49
- ],
50
- });
49
+ const values = parseNamedArgs(args, { allowedFlags: START_ALLOWED_FLAGS, valueFlags: START_VALUE_FLAGS });
51
50
  assertNoPositionals(values.positionals, "start");
52
51
  const activeTicketId = optionalNonEmpty(values.flags.get("--ticket"));
53
52
  const clearTicket = values.booleans.has("--clear-ticket");