@bli-cockpit/cli 0.2.64 → 0.2.66
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.
|
@@ -27,6 +27,25 @@
|
|
|
27
27
|
* older `reply` / `thread` / `traceId` / `model` / `trace` / `latency` /
|
|
28
28
|
* `clientLatency` keys are untouched, because scripts already read them and a
|
|
29
29
|
* one-contract ticket that broke the contract would be a joke.
|
|
30
|
+
*
|
|
31
|
+
* ## Where `sources` comes from (BLI-3770)
|
|
32
|
+
*
|
|
33
|
+
* From the DOOR, as objects, when the door sends them: `POST /api/jarvis/cli`
|
|
34
|
+
* carries `sources` built from the citations the grounding layer minted, and
|
|
35
|
+
* the `Source:` lines inside `answer` are rendered from that same array. The
|
|
36
|
+
* older derivation — a line-anchored regex over the finished prose — could
|
|
37
|
+
* only find a citation the renderer happened to put on its own line, so QA
|
|
38
|
+
* tick 16's c9, c9b and n9 (all off `searchEverything`) ended *"…Source:
|
|
39
|
+
* issue, at /work/BLI-3706, by …"* INSIDE the final sentence and returned
|
|
40
|
+
* `"sources": []`: the person saw a citation, the machine did not.
|
|
41
|
+
*
|
|
42
|
+
* The prose derivation survives as the FALLBACK, for a dashboard deployed
|
|
43
|
+
* before that door field existed, and it is wider than it was: a whole
|
|
44
|
+
* `Sources:` line (plural, any case, bulleted) counts — that is what the
|
|
45
|
+
* server itself treats as provenance — and so does a citation that begins a
|
|
46
|
+
* sentence inside a paragraph. A `Source:` in the MIDDLE of a sentence ("the
|
|
47
|
+
* Source: field on that row was blank") is prose about a source, not a
|
|
48
|
+
* source, and is deliberately not matched.
|
|
30
49
|
*/
|
|
31
50
|
/**
|
|
32
51
|
* Every key of the envelope, in the order it is written. The literal list IS
|
|
@@ -43,8 +62,20 @@ export const JARVIS_ANSWER_ENVELOPE_KEYS = [
|
|
|
43
62
|
"degraded",
|
|
44
63
|
"degraded_reasons",
|
|
45
64
|
];
|
|
46
|
-
/**
|
|
47
|
-
|
|
65
|
+
/**
|
|
66
|
+
* A citation line, in every shape the server itself treats as one.
|
|
67
|
+
*
|
|
68
|
+
* BLI-3770 widened this from `/^\s*Source:\s*\S/` to mirror
|
|
69
|
+
* `apps/dashboard/src/lib/jarvis/chat-v2/provenance-lines.ts`, which matches
|
|
70
|
+
* `sources?` case-insensitively with an optional bullet. The narrow version
|
|
71
|
+
* meant a `Sources: …` line the server had already lifted out of the body as
|
|
72
|
+
* provenance, and put straight back under the answer, was not a source to
|
|
73
|
+
* this side: one thing, two definitions, in two packages.
|
|
74
|
+
*/
|
|
75
|
+
const SOURCE_LINE = /^\s*(?:[-*\u2022]\s+)?sources?\s*:\s*\S/i;
|
|
76
|
+
/** The receipt's link, as `appendCitations` lays it out: `<url>` after the words. */
|
|
77
|
+
const BRACKETED_LINK = /\s*<(https?:\/\/[^>\s]+)>\s*$/;
|
|
78
|
+
const TRAILING_LINK = /\s+(https?:\/\/\S+)\s*$/;
|
|
48
79
|
/**
|
|
49
80
|
* The `Source:` lines inside an answer. Pure string work on what the server
|
|
50
81
|
* already sent — this side never decides what a source IS, it only finds the
|
|
@@ -56,6 +87,90 @@ export function extractSourceLines(answer) {
|
|
|
56
87
|
.map((line) => line.trim())
|
|
57
88
|
.filter((line) => SOURCE_LINE.test(line));
|
|
58
89
|
}
|
|
90
|
+
/** The words and the link, kept apart, the way every surface renders them. */
|
|
91
|
+
function splitLink(line) {
|
|
92
|
+
const trimmed = line.trim().replace(/^[-*\u2022]\s+/, "");
|
|
93
|
+
const bracketed = trimmed.match(BRACKETED_LINK);
|
|
94
|
+
if (bracketed)
|
|
95
|
+
return { label: trimmed.slice(0, bracketed.index).trim(), href: bracketed[1] ?? null };
|
|
96
|
+
const trailing = trimmed.match(TRAILING_LINK);
|
|
97
|
+
if (trailing)
|
|
98
|
+
return { label: trimmed.slice(0, trailing.index).trim(), href: trailing[1] ?? null };
|
|
99
|
+
return { label: trimmed, href: null };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A citation the model wrote INSIDE a paragraph (BLI-3770).
|
|
103
|
+
*
|
|
104
|
+
* It counts when it begins a sentence, which is the shape QA tick 16 caught
|
|
105
|
+
* three times. It deliberately does not count in the middle of one — "the
|
|
106
|
+
* Source: field on that row was blank" is a sentence about a field, and
|
|
107
|
+
* treating it as a receipt would put prose in a contract that is supposed to
|
|
108
|
+
* carry evidence.
|
|
109
|
+
*/
|
|
110
|
+
function inlineSourceFragments(line) {
|
|
111
|
+
return line
|
|
112
|
+
.split(/(?<=[.!?])\s+/)
|
|
113
|
+
.map((part) => part.trim())
|
|
114
|
+
.filter((part) => SOURCE_LINE.test(part));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The fallback derivation: what this side can tell from the answer alone.
|
|
118
|
+
*
|
|
119
|
+
* Only reached when the door sent no structured `sources` — a dashboard
|
|
120
|
+
* deployed before BLI-3770, or a turn whose citation nothing minted. Every
|
|
121
|
+
* row it produces is labelled `kind: "prose"` so a consumer can tell a
|
|
122
|
+
* recovered line from a minted receipt.
|
|
123
|
+
*/
|
|
124
|
+
export function sourcesFromAnswer(answer) {
|
|
125
|
+
const found = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const add = (text) => {
|
|
128
|
+
const { label, href } = splitLink(text);
|
|
129
|
+
if (!label || seen.has(label))
|
|
130
|
+
return;
|
|
131
|
+
seen.add(label);
|
|
132
|
+
found.push({ kind: "prose", label, href, id: null, tool: null });
|
|
133
|
+
};
|
|
134
|
+
for (const raw of answer.split("\n")) {
|
|
135
|
+
const line = raw.trim();
|
|
136
|
+
if (!line)
|
|
137
|
+
continue;
|
|
138
|
+
if (SOURCE_LINE.test(line)) {
|
|
139
|
+
add(line);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const fragment of inlineSourceFragments(line))
|
|
143
|
+
add(fragment);
|
|
144
|
+
}
|
|
145
|
+
return found;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The door's own `sources`, checked rather than trusted. A field that arrives
|
|
149
|
+
* as something other than a list of labelled objects is treated as absent, so
|
|
150
|
+
* a malformed body degrades to the prose fallback instead of putting
|
|
151
|
+
* `[object Object]` in front of an agent.
|
|
152
|
+
*/
|
|
153
|
+
function doorSources(sent) {
|
|
154
|
+
if (!Array.isArray(sent))
|
|
155
|
+
return [];
|
|
156
|
+
const rows = [];
|
|
157
|
+
for (const entry of sent) {
|
|
158
|
+
if (!entry || typeof entry !== "object")
|
|
159
|
+
continue;
|
|
160
|
+
const row = entry;
|
|
161
|
+
const label = typeof row.label === "string" ? row.label.trim() : "";
|
|
162
|
+
if (!label)
|
|
163
|
+
continue;
|
|
164
|
+
rows.push({
|
|
165
|
+
kind: typeof row.kind === "string" && row.kind ? row.kind : "unknown",
|
|
166
|
+
label,
|
|
167
|
+
href: typeof row.href === "string" ? row.href : null,
|
|
168
|
+
id: typeof row.id === "string" ? row.id : null,
|
|
169
|
+
tool: typeof row.tool === "string" ? row.tool : null,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return rows;
|
|
173
|
+
}
|
|
59
174
|
export function buildJarvisAnswerEnvelope(input) {
|
|
60
175
|
const reasons = [];
|
|
61
176
|
if (input.modelFallback === true)
|
|
@@ -68,10 +183,14 @@ export function buildJarvisAnswerEnvelope(input) {
|
|
|
68
183
|
const turnId = input.traceId ?? null;
|
|
69
184
|
if (!turnId)
|
|
70
185
|
reasons.push("no_turn_id");
|
|
186
|
+
const sent = doorSources(input.sources);
|
|
71
187
|
return {
|
|
72
188
|
ok: true,
|
|
73
189
|
answer: input.reply,
|
|
74
|
-
|
|
190
|
+
// BLI-3770: the door's own citations when it sent them, and only then the
|
|
191
|
+
// prose fallback. Never both — a turn's receipts have one origin, and
|
|
192
|
+
// merging the two would double-count the lines the door already described.
|
|
193
|
+
sources: sent.length > 0 ? sent : sourcesFromAnswer(input.reply),
|
|
75
194
|
turn_id: turnId,
|
|
76
195
|
thread_id: input.thread ?? null,
|
|
77
196
|
trace_thread_id: input.traceThread ?? null,
|
|
@@ -158,6 +158,10 @@ export async function sendOneTurn(context, prompt, io) {
|
|
|
158
158
|
revised: body.revised,
|
|
159
159
|
trace,
|
|
160
160
|
proposalId: body.proposalId,
|
|
161
|
+
// BLI-3770: the door's own structured citations. Absent from a
|
|
162
|
+
// dashboard that predates it, and the envelope falls back to
|
|
163
|
+
// reading the answer's own `Source:` lines when it is.
|
|
164
|
+
sources: body.sources,
|
|
161
165
|
}),
|
|
162
166
|
reply: body.reply,
|
|
163
167
|
thread: body.thread ?? context.command.thread,
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.66");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/dist/commands/status.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* pastes into Slack when something looks wrong. The `--json` payload is the
|
|
9
9
|
* machine contract and is byte-for-byte unchanged — `status-json-shape.test.ts`
|
|
10
10
|
* holds it to that.
|
|
11
|
+
*
|
|
12
|
+
* Two branches, one machine: a folder holding several repos prints a row each,
|
|
13
|
+
* a single repo prints the full block, and BOTH carry the setup receipt
|
|
14
|
+
* (BLI-3789) — it says whether this HOST is connected, which does not change
|
|
15
|
+
* with the folder you are standing in.
|
|
11
16
|
*/
|
|
12
17
|
import { readdir, stat } from "node:fs/promises";
|
|
13
18
|
import os from "node:os";
|
|
@@ -24,6 +29,18 @@ import { getCollectorRuntimePaths, inspectLocalCollectorStatus, readLocalCollect
|
|
|
24
29
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
25
30
|
export async function runStatus(command, io) {
|
|
26
31
|
const backfillCursor = await inspectBackfillCursor(command.homeDir);
|
|
32
|
+
// BLI-3731: "is this machine actually connected?" belongs beside "is it
|
|
33
|
+
// collecting?", and it is read back off the host rather than remembered.
|
|
34
|
+
// Refreshing here also keeps the heartbeat's cache warm without the 15-minute
|
|
35
|
+
// tick paying for the probe.
|
|
36
|
+
//
|
|
37
|
+
// BLI-3789: read it BEFORE the branch below, not after. The receipt is a fact
|
|
38
|
+
// about the MACHINE, so it cannot depend on whether the folder you happen to
|
|
39
|
+
// stand in holds one repo or six — and the multi-repo branch, which every
|
|
40
|
+
// agent machine takes, used to return before the receipt was ever built.
|
|
41
|
+
const setupReceipt = await refreshSetupReceipt(io, {
|
|
42
|
+
...(command.homeDir ? { homeDir: command.homeDir } : {}),
|
|
43
|
+
});
|
|
27
44
|
const worktrees = await discoverCommandWorktrees(command.repoRoot, { maxDepth: command.maxDepth, maxRepos: command.maxRepos, homeDir: command.homeDir }, io);
|
|
28
45
|
if (worktrees.length > 1) {
|
|
29
46
|
const statuses = await Promise.all(worktrees.map(async (worktree) => ({
|
|
@@ -34,7 +51,17 @@ export async function runStatus(command, io) {
|
|
|
34
51
|
head_sha: worktree.head_sha,
|
|
35
52
|
})));
|
|
36
53
|
if (command.json) {
|
|
37
|
-
writeLine(io.stdout, JSON.stringify({
|
|
54
|
+
writeLine(io.stdout, JSON.stringify({
|
|
55
|
+
mode: "multi_repo",
|
|
56
|
+
statuses,
|
|
57
|
+
backfill_cursor: backfillCursor,
|
|
58
|
+
// Beside `backfill_cursor`, and for the same reason: both are facts
|
|
59
|
+
// about this machine, so both sit at the top of the payload rather
|
|
60
|
+
// than once per repo row. Null means this run could not read the
|
|
61
|
+
// host — `refreshSetupReceipt` named the reason on stderr — never
|
|
62
|
+
// "this machine is not connected".
|
|
63
|
+
setup_receipt: setupReceipt?.receipt ?? null,
|
|
64
|
+
}, null, 2));
|
|
38
65
|
return 0;
|
|
39
66
|
}
|
|
40
67
|
writeLine(io.stdout, "Tower status — every repo below this folder");
|
|
@@ -42,16 +69,10 @@ export async function runStatus(command, io) {
|
|
|
42
69
|
for (const status of statuses) {
|
|
43
70
|
writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
|
|
44
71
|
}
|
|
72
|
+
writeConnectedBlock(io, setupReceipt);
|
|
45
73
|
return 0;
|
|
46
74
|
}
|
|
47
75
|
const status = await inspectLocalCollectorStatus(command);
|
|
48
|
-
// BLI-3731: "is this machine actually connected?" belongs beside "is it
|
|
49
|
-
// collecting?", and it is read back off the host rather than remembered.
|
|
50
|
-
// Refreshing here also keeps the heartbeat's cache warm without the 15-minute
|
|
51
|
-
// tick paying for the probe.
|
|
52
|
-
const setupReceipt = await refreshSetupReceipt(io, {
|
|
53
|
-
...(command.homeDir ? { homeDir: command.homeDir } : {}),
|
|
54
|
-
});
|
|
55
76
|
if (command.json) {
|
|
56
77
|
writeLine(io.stdout, JSON.stringify({
|
|
57
78
|
...status,
|
|
@@ -88,13 +109,22 @@ export async function runStatus(command, io) {
|
|
|
88
109
|
writeLine(io.stdout, `Stuck files: ${stuckEvidenceLine(status)}`);
|
|
89
110
|
for (const detail of status.details)
|
|
90
111
|
writeLine(io.stdout, `- ${detail}`);
|
|
112
|
+
writeConnectedBlock(io, setupReceipt);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The receipt block, in the words `cockpit doctor` prints, from the one reader
|
|
117
|
+
* (BLI-3789). Both branches of `status` call this: a person standing in a
|
|
118
|
+
* parent folder is asking about the same machine as a person standing in one
|
|
119
|
+
* repo, and only one of them used to be told.
|
|
120
|
+
*/
|
|
121
|
+
function writeConnectedBlock(io, setupReceipt) {
|
|
91
122
|
writeLine(io.stdout, "Connected:");
|
|
92
123
|
for (const line of setupReceipt
|
|
93
124
|
? setupReceiptBlock(setupReceipt, { indent: " " })
|
|
94
125
|
: [" unknown — run `cockpit doctor` to read this machine."]) {
|
|
95
126
|
writeLine(io.stdout, line);
|
|
96
127
|
}
|
|
97
|
-
return 0;
|
|
98
128
|
}
|
|
99
129
|
/**
|
|
100
130
|
* Done, still working, or never started — and when it is still working, how
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.66",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@bli-cockpit/memory-mcp": "0.1.9",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.8",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.30"
|
|
33
33
|
}
|
|
34
34
|
}
|