@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
@@ -33,9 +33,20 @@ import { readTicketBoard } from "./tickets-board.js";
33
33
  import { showIssue } from "./issue-show.js";
34
34
  import { READ_DEADLINE_MS, TAG, resolveProjectId, sizeForEstimate, withSizeLabel, } from "./issue-contracts.js";
35
35
  import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
36
+ import { listIssueVersions, restoreIssueVersion } from "./issue-versions.js";
36
37
  export async function runIssue(command, io) {
37
38
  const door = await openAgentDoor("issue", command, io);
38
39
  switch (command.action) {
40
+ case "mutation-status": {
41
+ const answer = await askAgentDoor(door, { path: `/api/work/mutations/${encodeURIComponent(command.issueRef ?? "")}`,
42
+ method: "GET", label: "ticket mutation status", timeoutMs: READ_DEADLINE_MS });
43
+ if (!answer.ok)
44
+ return failAgentDoor(door, TAG, answer.reason, answer.detail);
45
+ if (door.json)
46
+ return emitAgentDoor(door, { ok: true, ...answer.body });
47
+ writeLine(door.io.stdout, JSON.stringify(answer.body));
48
+ return 0;
49
+ }
39
50
  case "board":
40
51
  return readTicketBoard(command, door);
41
52
  case "list":
@@ -52,6 +63,10 @@ export async function runIssue(command, io) {
52
63
  return commentIssue(command, door);
53
64
  case "history":
54
65
  return issueHistory(command, door);
66
+ case "versions":
67
+ return listIssueVersions(command, door);
68
+ case "restore":
69
+ return restoreIssueVersion(command, door);
55
70
  }
56
71
  }
57
72
  function issueLine(issue, directory) {
@@ -92,23 +107,10 @@ export function listGapLines(body, requestedLimit) {
92
107
  return lines;
93
108
  }
94
109
  async function listIssues(command, door) {
95
- const query = new URLSearchParams();
96
- if (command.stateFilter)
97
- query.set("state", command.stateFilter);
98
- if (command.assignee)
99
- query.set("assignee_id", command.assignee);
100
- if (command.limit !== undefined)
101
- query.set("limit", String(command.limit));
102
- if (command.project) {
103
- const resolved = await resolveProjectId(door, command.project);
104
- if (resolved.status === "list_failed")
105
- return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
106
- if (resolved.status === "not_found") {
107
- return failAgentDoor(door, TAG, "project_not_found_or_unreadable", `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`);
108
- }
109
- query.set("project_id", resolved.id);
110
- }
111
- const suffix = query.toString();
110
+ const built = await buildIssueListQuery(command, door);
111
+ if (!built.ok)
112
+ return built.exitCode;
113
+ const suffix = built.query.toString();
112
114
  // BLI-10579: the assignee directory (`/api/team/members`) is the whole org
113
115
  // roster regardless of which issues come back — it never actually depended
114
116
  // on the issue list — so start it now, alongside the issues request,
@@ -116,26 +118,7 @@ async function listIssues(command, door) {
116
118
  // `work list` p50 was exactly this serial second call).
117
119
  const directoryFetch = beginAssigneeDirectoryFetch(door);
118
120
  const listPath = `/api/work/issues${suffix ? `?${suffix}` : ""}`;
119
- const answer = await readThroughCache({
120
- io: door.io,
121
- homeDir: door.homeDir,
122
- deviceToken: door.deviceToken,
123
- surface: "work",
124
- label: "issue list",
125
- path: listPath,
126
- json: door.json,
127
- ask: () => askAgentDoor(door, {
128
- path: listPath,
129
- method: "GET",
130
- label: "issue list",
131
- timeoutMs: READ_DEADLINE_MS,
132
- // BLI-10746: `work list` was the slowest of the six reads measured
133
- // from Vietnam (1,682 ms p50) — route it through the resident memory
134
- // daemon's warm connection so this command skips a fresh TCP+TLS
135
- // handshake.
136
- warmRoute: true,
137
- }),
138
- });
121
+ const answer = await askIssueList(door, listPath);
139
122
  if (!answer.ok) {
140
123
  // Not awaited for its own sake — `finishAssigneeDirectory` always awaits
141
124
  // the in-flight fetch so it never leaks past this command's exit.
@@ -176,6 +159,51 @@ async function listIssues(command, door) {
176
159
  writeLine(door.io.stdout, `${issues.length} issue(s).`);
177
160
  return 0;
178
161
  }
162
+ /** The list query for the filters given, or the exit code of the project lookup that failed. */
163
+ async function buildIssueListQuery(command, door) {
164
+ const query = new URLSearchParams();
165
+ if (command.stateFilter)
166
+ query.set("state", command.stateFilter);
167
+ if (command.assignee)
168
+ query.set("assignee_id", command.assignee);
169
+ if (command.limit !== undefined)
170
+ query.set("limit", String(command.limit));
171
+ if (command.project) {
172
+ const resolved = await resolveProjectId(door, command.project);
173
+ if (resolved.status === "list_failed")
174
+ return { ok: false, exitCode: failAgentDoor(door, TAG, resolved.reason, resolved.detail) };
175
+ if (resolved.status === "not_found") {
176
+ return {
177
+ ok: false,
178
+ exitCode: failAgentDoor(door, TAG, "project_not_found_or_unreadable", `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`),
179
+ };
180
+ }
181
+ query.set("project_id", resolved.id);
182
+ }
183
+ return { ok: true, query };
184
+ }
185
+ function askIssueList(door, listPath) {
186
+ return readThroughCache({
187
+ io: door.io,
188
+ homeDir: door.homeDir,
189
+ deviceToken: door.deviceToken,
190
+ surface: "work",
191
+ label: "issue list",
192
+ path: listPath,
193
+ json: door.json,
194
+ ask: () => askAgentDoor(door, {
195
+ path: listPath,
196
+ method: "GET",
197
+ label: "issue list",
198
+ timeoutMs: READ_DEADLINE_MS,
199
+ // BLI-10746: `work list` was the slowest of the six reads measured
200
+ // from Vietnam (1,682 ms p50) — route it through the resident memory
201
+ // daemon's warm connection so this command skips a fresh TCP+TLS
202
+ // handshake.
203
+ warmRoute: true,
204
+ }),
205
+ });
206
+ }
179
207
  /**
180
208
  * What a history row actually says. A row imported from Linear's issue
181
209
  * TIMESTAMPS (created/started/completed/canceled) carries no state names at
@@ -187,6 +215,14 @@ async function listIssues(command, door) {
187
215
  * reads a real gap as a broken command.
188
216
  */
189
217
  function historyChange(row) {
218
+ if (row.source === "native" && row.detail?.operation) {
219
+ if (row.detail.operation === "create")
220
+ return "created the ticket";
221
+ if (row.detail.operation === "comment")
222
+ return "added a comment";
223
+ if (row.change_type === "other")
224
+ return `updated ${(row.detail.fields ?? []).join(", ")}`;
225
+ }
190
226
  if (row.from_value === null && row.to_value === null) {
191
227
  return `(no state names recorded — imported from ${row.source})`;
192
228
  }
@@ -0,0 +1,99 @@
1
+ import { colorEnabled, dim, writeFragment, writeLine } from "./cli-io.js";
2
+ import { ANSWER_REVISED_LINE, writeReply, writeReplyContinuation } from "./jarvis-render.js";
3
+ /**
4
+ * The answer as it is written, in a terminal (BLI-3517).
5
+ *
6
+ * A terminal cannot unprint. That single fact decides everything here:
7
+ *
8
+ * - The words stream out under the usual `jarvis> ` prefix, printed the moment
9
+ * they arrive rather than after the whole turn (a measured 4-16 s of nothing
10
+ * before this ticket).
11
+ * - A `reset` — the server taking back a draft the model wrote before deciding
12
+ * to call a tool — cannot erase what is on screen, so it SAYS so on its own
13
+ * line and the replacement follows. Never a retracted sentence left standing
14
+ * with nothing marking it.
15
+ * - The settled reply is the authority. The grounding gate runs on the whole
16
+ * text once the stream ends, so the answer can grow (citations, a fallback
17
+ * notice) or change. A pure continuation is printed as the remainder; a
18
+ * genuine change is reprinted whole under a line saying it was revised.
19
+ * - `--json` never streams a fragment: that contract is exactly one object on
20
+ * stdout, and every method here is a no-op for it.
21
+ */
22
+ export function createLiveAnswer(command, io) {
23
+ const styled = colorEnabled(io);
24
+ const quiet = command.json === true;
25
+ const screen = { printed: "", opened: false };
26
+ return {
27
+ token(event) {
28
+ if (quiet)
29
+ return;
30
+ const text = event.text ?? "";
31
+ if (event.reset) {
32
+ if (screen.printed) {
33
+ writeLine(io.stdout, "");
34
+ writeLine(io.stdout, dim(" — that draft was replaced —", styled));
35
+ screen.opened = false;
36
+ }
37
+ screen.printed = "";
38
+ }
39
+ if (!text)
40
+ return;
41
+ openAnswerLine(io, screen, "");
42
+ screen.printed += text;
43
+ writeFragment(io.stdout, text);
44
+ },
45
+ interrupt() {
46
+ if (quiet || !screen.opened)
47
+ return;
48
+ writeLine(io.stdout, "");
49
+ screen.opened = false;
50
+ },
51
+ settle(reply, subject, revised) {
52
+ if (quiet)
53
+ return;
54
+ settleLiveAnswer(io, styled, screen, { reply, subject, revised });
55
+ },
56
+ abandon() {
57
+ // The turn died with words already on screen. End the line so the
58
+ // failure sentence does not run on from a half-written answer.
59
+ if (!quiet && screen.opened)
60
+ writeLine(io.stdout, "");
61
+ },
62
+ };
63
+ }
64
+ function openAnswerLine(io, screen, subject) {
65
+ if (screen.opened)
66
+ return;
67
+ screen.opened = true;
68
+ writeFragment(io.stdout, `jarvis${subject}> `);
69
+ }
70
+ function settleLiveAnswer(io, styled, screen, settled) {
71
+ const { reply, subject, revised } = settled;
72
+ const { printed } = screen;
73
+ if (!printed) {
74
+ // Nothing streamed: a non-streaming dashboard, `--no-stream`, or a
75
+ // turn whose words never arrived. Exactly the pre-ticket line, dim
76
+ // citation link and all (BLI-3570).
77
+ writeReply(io, `jarvis${subject}> `, reply);
78
+ return;
79
+ }
80
+ if (reply === printed) {
81
+ if (screen.opened)
82
+ writeLine(io.stdout, "");
83
+ return;
84
+ }
85
+ if (reply.startsWith(printed)) {
86
+ // The gate only ADDED — citations going back under the answer, a
87
+ // fallback notice. Print the tail and leave the words alone; the
88
+ // citation block is exactly the thing BLI-3570 puts its link under, and
89
+ // it is arriving here rather than in the stream.
90
+ if (!screen.opened)
91
+ writeFragment(io.stdout, `jarvis${subject}> `);
92
+ writeReplyContinuation(io, reply.slice(printed.length));
93
+ return;
94
+ }
95
+ if (screen.opened)
96
+ writeLine(io.stdout, "");
97
+ writeLine(io.stdout, dim(revised ? ` — ${ANSWER_REVISED_LINE} —` : " — corrected —", styled));
98
+ writeReply(io, `jarvis${subject}> `, reply);
99
+ }
@@ -228,7 +228,7 @@ export function writeReplyContinuation(io, tail) {
228
228
  * here in the same register rather than shared through a dependency that does
229
229
  * not exist.
230
230
  */
231
- const ANSWER_REVISED_LINE = "revised: I checked that against its sources and changed what they did not back up";
231
+ export const ANSWER_REVISED_LINE = "revised: I checked that against its sources and changed what they did not back up";
232
232
  /**
233
233
  * The three lines a correction's rewrite prints (BLI-3567).
234
234
  *
@@ -251,99 +251,6 @@ const ANSWER_CORRECTED_NOTE_LINE = "The paragraph printed above is unchanged —
251
251
  "printed. This is what it says now.";
252
252
  const ANSWER_REWRITE_UNVERIFIED_LINE = "Your correction is recorded. I could not verify a rewrite of that paragraph against its " +
253
253
  "sources, so it stays as it was.";
254
- /**
255
- * The answer as it is written, in a terminal (BLI-3517).
256
- *
257
- * A terminal cannot unprint. That single fact decides everything here:
258
- *
259
- * - The words stream out under the usual `jarvis> ` prefix, printed the moment
260
- * they arrive rather than after the whole turn (a measured 4-16 s of nothing
261
- * before this ticket).
262
- * - A `reset` — the server taking back a draft the model wrote before deciding
263
- * to call a tool — cannot erase what is on screen, so it SAYS so on its own
264
- * line and the replacement follows. Never a retracted sentence left standing
265
- * with nothing marking it.
266
- * - The settled reply is the authority. The grounding gate runs on the whole
267
- * text once the stream ends, so the answer can grow (citations, a fallback
268
- * notice) or change. A pure continuation is printed as the remainder; a
269
- * genuine change is reprinted whole under a line saying it was revised.
270
- * - `--json` never streams a fragment: that contract is exactly one object on
271
- * stdout, and every method here is a no-op for it.
272
- */
273
- export function createLiveAnswer(command, io) {
274
- const styled = colorEnabled(io);
275
- const quiet = command.json === true;
276
- let printed = "";
277
- let opened = false;
278
- const open = (subject) => {
279
- if (opened)
280
- return;
281
- opened = true;
282
- writeFragment(io.stdout, `jarvis${subject}> `);
283
- };
284
- return {
285
- token(event) {
286
- if (quiet)
287
- return;
288
- const text = event.text ?? "";
289
- if (event.reset) {
290
- if (printed) {
291
- writeLine(io.stdout, "");
292
- writeLine(io.stdout, dim(" — that draft was replaced —", styled));
293
- opened = false;
294
- }
295
- printed = "";
296
- }
297
- if (!text)
298
- return;
299
- open("");
300
- printed += text;
301
- writeFragment(io.stdout, text);
302
- },
303
- interrupt() {
304
- if (quiet || !opened)
305
- return;
306
- writeLine(io.stdout, "");
307
- opened = false;
308
- },
309
- settle(reply, subject, revised) {
310
- if (quiet)
311
- return;
312
- if (!printed) {
313
- // Nothing streamed: a non-streaming dashboard, `--no-stream`, or a
314
- // turn whose words never arrived. Exactly the pre-ticket line, dim
315
- // citation link and all (BLI-3570).
316
- writeReply(io, `jarvis${subject}> `, reply);
317
- return;
318
- }
319
- if (reply === printed) {
320
- if (opened)
321
- writeLine(io.stdout, "");
322
- return;
323
- }
324
- if (reply.startsWith(printed)) {
325
- // The gate only ADDED — citations going back under the answer, a
326
- // fallback notice. Print the tail and leave the words alone; the
327
- // citation block is exactly the thing BLI-3570 puts its link under, and
328
- // it is arriving here rather than in the stream.
329
- if (!opened)
330
- writeFragment(io.stdout, `jarvis${subject}> `);
331
- writeReplyContinuation(io, reply.slice(printed.length));
332
- return;
333
- }
334
- if (opened)
335
- writeLine(io.stdout, "");
336
- writeLine(io.stdout, dim(revised ? ` — ${ANSWER_REVISED_LINE} —` : " — corrected —", styled));
337
- writeReply(io, `jarvis${subject}> `, reply);
338
- },
339
- abandon() {
340
- // The turn died with words already on screen. End the line so the
341
- // failure sentence does not run on from a half-written answer.
342
- if (!quiet && opened)
343
- writeLine(io.stdout, "");
344
- },
345
- };
346
- }
347
254
  /**
348
255
  * Print what a correction did to the answer above, once the turn has settled.
349
256
  *
@@ -84,28 +84,10 @@ export async function readRememberedTurn(homeDir) {
84
84
  }
85
85
  export async function runJarvisTrace(context, io) {
86
86
  const log = (line) => writeLine(io.stderr, line);
87
- let traceId = context.requested;
88
- let resolvedFrom = "argument";
89
- if (context.requested === "last") {
90
- const remembered = await readRememberedTurn(context.homeDir);
91
- if (!remembered) {
92
- return refuse(context, io, "no_remembered_turn", "This machine has not asked JARVIS anything since traces were turned on, so there is no last turn to open. Ask something, then run this again — or pass the trace id from the turn's receipt.");
93
- }
94
- if (remembered.traceId) {
95
- traceId = remembered.traceId;
96
- resolvedFrom = "bookmark";
97
- }
98
- else if (remembered.threadId) {
99
- // The bookmark knew the conversation but not the turn (an older receipt).
100
- // Ask the server for that conversation's newest turn rather than nothing.
101
- const newest = await newestTraceForThread(context, remembered.threadId, io);
102
- if (!newest) {
103
- return refuse(context, io, "no_traces_in_thread", "That conversation has no recorded turns inside the 30-day retention. Pass a trace id, or ask something new.");
104
- }
105
- traceId = newest;
106
- resolvedFrom = "thread";
107
- }
108
- }
87
+ const resolution = await resolveRequestedTrace(context, io);
88
+ if (!resolution.ok)
89
+ return resolution.exitCode;
90
+ const { traceId, resolvedFrom } = resolution;
109
91
  const result = await towerJsonRequest({
110
92
  dashboardUrl: context.dashboardUrl,
111
93
  path: `/api/ops/trace/${encodeURIComponent(traceId)}`,
@@ -139,6 +121,32 @@ export async function runJarvisTrace(context, io) {
139
121
  })}`);
140
122
  return 0;
141
123
  }
124
+ /** Turns `last` into the remembered turn's trace id; any other argument is already a trace id. */
125
+ async function resolveRequestedTrace(context, io) {
126
+ if (context.requested !== "last")
127
+ return { ok: true, traceId: context.requested, resolvedFrom: "argument" };
128
+ const remembered = await readRememberedTurn(context.homeDir);
129
+ if (!remembered) {
130
+ return {
131
+ ok: false,
132
+ exitCode: refuse(context, io, "no_remembered_turn", "This machine has not asked JARVIS anything since traces were turned on, so there is no last turn to open. Ask something, then run this again — or pass the trace id from the turn's receipt."),
133
+ };
134
+ }
135
+ if (remembered.traceId)
136
+ return { ok: true, traceId: remembered.traceId, resolvedFrom: "bookmark" };
137
+ if (!remembered.threadId)
138
+ return { ok: true, traceId: context.requested, resolvedFrom: "argument" };
139
+ // The bookmark knew the conversation but not the turn (an older receipt).
140
+ // Ask the server for that conversation's newest turn rather than nothing.
141
+ const newest = await newestTraceForThread(context, remembered.threadId, io);
142
+ if (!newest) {
143
+ return {
144
+ ok: false,
145
+ exitCode: refuse(context, io, "no_traces_in_thread", "That conversation has no recorded turns inside the 30-day retention. Pass a trace id, or ask something new."),
146
+ };
147
+ }
148
+ return { ok: true, traceId: newest, resolvedFrom: "thread" };
149
+ }
142
150
  async function newestTraceForThread(context, threadId, io) {
143
151
  const result = await towerJsonRequest({
144
152
  dashboardUrl: context.dashboardUrl,
@@ -0,0 +1,124 @@
1
+ import { writeLine } from "./cli-io.js";
2
+ import { buildJarvisAnswerEnvelope } from "./jarvis-answer-envelope.js";
3
+ import { clientTimingFields, latencyFields, latencyLogFields, writeModelReceipt, writeRevision, writeTraceBlock, } from "./jarvis-render.js";
4
+ export function printJsonTurn(settled) {
5
+ const { context, io, body, trace, sent, turn, startedAt, preRequestMs } = settled;
6
+ writeLine(io.stdout, JSON.stringify({
7
+ // BLI-3732: the shared envelope FIRST — `answer`, `sources`,
8
+ // `turn_id`, `thread_id`, `trace_thread_id`, `degraded`,
9
+ // `degraded_reasons` — so a script here and an agent on the
10
+ // `jarvis_ask` MCP tool read one contract for one turn. The keys
11
+ // below it are the ones this command has always printed and are
12
+ // untouched; `reply` and `answer` are the same string.
13
+ ...buildJarvisAnswerEnvelope({
14
+ reply: body.reply,
15
+ thread: body.thread ?? context.command.thread,
16
+ traceId: body.traceId,
17
+ traceThread: body.traceThread,
18
+ modelFallback: body.model?.fallback,
19
+ revised: body.revised,
20
+ trace,
21
+ proposalId: body.proposalId,
22
+ // BLI-3787: the sentence beside it — what a person must do next when
23
+ // the arm proposed something, and when it refused instead.
24
+ nextAction: body.nextAction,
25
+ // BLI-3770: the door's own structured citations. Absent from a
26
+ // dashboard that predates it, and the envelope falls back to
27
+ // reading the answer's own `Source:` lines when it is.
28
+ sources: body.sources,
29
+ }),
30
+ reply: body.reply,
31
+ thread: body.thread ?? context.command.thread,
32
+ model: body.model ?? null,
33
+ trace,
34
+ subject: body.subject ?? null,
35
+ // BLI-3582: the server's own split of the wait. `null` from a
36
+ // dashboard that does not measure it yet.
37
+ latency: latencyFields(body.latency),
38
+ // BLI-3591: this side's own half of the same wait, so a consumer can
39
+ // put the whole journey together without a stopwatch of its own.
40
+ clientLatency: {
41
+ bootMs: context.boot.bootMs,
42
+ sessionMs: context.boot.sessionMs,
43
+ promptMs: context.boot.promptMs,
44
+ preRequestMs,
45
+ respondedMs: sent.respondedMs,
46
+ firstByteMs: turn.timing.firstByteMs,
47
+ firstFrameMs: turn.timing.firstFrameMs,
48
+ firstTokenMs: turn.timing.firstTokenMs,
49
+ elapsedMs: Date.now() - startedAt,
50
+ },
51
+ ...(body.traceId ? { traceId: body.traceId } : {}),
52
+ }));
53
+ }
54
+ export function printTerminalTurn(settled, live) {
55
+ const { context, io, body, trace, liveTraceLines } = settled;
56
+ const subject = body.subject?.displayName ? ` (${body.subject.displayName})` : "";
57
+ // BLI-3517: when the answer streamed, `settle` prints only what the live
58
+ // words did not already say — and says so out loud when the grounding gate
59
+ // took some of them back. When nothing streamed it prints the whole reply
60
+ // through `writeReply`, which is byte-for-byte what this command printed
61
+ // before, BLI-3570's dim link line included.
62
+ live.settle(body.reply, subject, body.revised === true);
63
+ // BLI-3567: what the correction did to the answer above, printed under
64
+ // this turn's reply because that is the only place a terminal has.
65
+ if (body.revision)
66
+ writeRevision(context.command, io, body.revision);
67
+ // Only when nothing was drawn live — otherwise every tool would print twice.
68
+ if (liveTraceLines === 0)
69
+ writeTraceBlock(io, trace);
70
+ writeModelReceipt(io, body.model);
71
+ }
72
+ export function logAnsweredTurn(settled) {
73
+ const { context, io, prompt, body, trace, attachment, sent, turn, startedAt, preRequestMs, liveTraceLines } = settled;
74
+ writeLine(io.stderr, `[jarvis cli] answered ${JSON.stringify({
75
+ prompt_length: prompt.length,
76
+ reply_length: body.reply.length,
77
+ trace_steps: trace?.length ?? 0,
78
+ trace_failed: trace?.filter((step) => step.status === "failed").length ?? 0,
79
+ model_requested: context.command.model ?? null,
80
+ elapsed_ms: Date.now() - startedAt,
81
+ // BLI-3786: `minted` is a conversation the DOOR opened for this call
82
+ // because nothing named one — read off the answer rather than guessed
83
+ // from the name, so this side keeps no copy of the server's rule. Never
84
+ // the thread name itself.
85
+ thread: body.thread && body.thread !== context.command.thread
86
+ ? "minted"
87
+ : context.command.thread === "main"
88
+ ? "default"
89
+ : "named",
90
+ subject: context.command.subject ? "selected" : "caller",
91
+ image_attached: attachment !== null,
92
+ image_byte_size: attachment?.bytes.byteLength ?? null,
93
+ streamed: turn.streamed,
94
+ live_trace_lines: liveTraceLines,
95
+ // BLI-3517: whether the words arrived live, and whether the settled
96
+ // answer superseded them. `streamed_chars: 0` against a streaming
97
+ // dashboard means the answer landed all at once.
98
+ streamed_chars: turn.draft.length,
99
+ revised: body.revised === true,
100
+ // BLI-3567: whether this turn was told there was an answer above it, and
101
+ // what the correction did to it. `sent_previous_answer: false` is a
102
+ // one-shot turn; a status with no revision at all is a dashboard that
103
+ // predates the step.
104
+ sent_previous_answer: Boolean(context.previous),
105
+ revision: body.revision?.status ?? null,
106
+ revision_reason: body.revision?.reason ?? null,
107
+ // BLI-3582: `elapsed_ms` above is the whole wait as this side felt it;
108
+ // these three say which part of it was the dashboard's prep, which was
109
+ // the model's first token, and how long the socket stayed silent before
110
+ // the first byte. All null against a dashboard that does not send them.
111
+ ...latencyLogFields(body.latency),
112
+ // BLI-3591: the terminal's own half, on the same line, so the gap
113
+ // between the dashboard's first token and the first character a person
114
+ // sees stops being a mystery with nobody's name on it. `boot_ms` and
115
+ // `session_ms` are process facts (identical on every turn of an
116
+ // interactive session); the rest are this turn's, measured from the
117
+ // moment the turn started.
118
+ ...clientTimingFields(context.boot, {
119
+ preRequestMs,
120
+ respondedMs: sent.respondedMs,
121
+ timing: turn.timing,
122
+ }),
123
+ })}`);
124
+ }