@bli-cockpit/cli 0.2.119 → 0.2.122
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.
- package/dist/commands/analyze.js +74 -54
- package/dist/commands/brief-rewrite.js +164 -101
- package/dist/commands/brief.js +38 -13
- package/dist/commands/careers.js +81 -9
- package/dist/commands/correct.js +38 -21
- package/dist/commands/docs.js +13 -10
- package/dist/commands/editor.js +59 -30
- package/dist/commands/install-receipts.js +106 -91
- package/dist/commands/local-args-tower-admin.js +4 -0
- package/dist/commands/local-args-tower-cal.js +28 -3
- package/dist/commands/local-args-tower-careers.js +56 -9
- package/dist/commands/local-args-tower-chat.js +55 -28
- package/dist/commands/local-args-tower-docs-msg.js +39 -8
- package/dist/commands/local-args-tower-mail.js +27 -1
- package/dist/commands/local-args-tower-models.js +14 -17
- package/dist/commands/local-args-tower-work.js +37 -6
- package/dist/commands/local-help-commands-tower.js +15 -1
- package/dist/commands/local-help-commands.js +2 -1
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/mcp-stdio-probe.js +92 -73
- package/dist/commands/memory-hook-performance.js +135 -101
- package/dist/commands/memory-install-claude.js +15 -14
- package/dist/commands/memory-install-codex.js +10 -6
- package/dist/commands/memory-install-config.js +5 -4
- package/dist/commands/memory-install-contract.js +56 -10
- package/dist/commands/memory-install-report.js +16 -11
- package/dist/commands/memory-install-skills.js +11 -11
- package/dist/commands/memory-log.js +22 -5
- package/dist/commands/msg.js +11 -5
- package/dist/commands/onboard-setup.js +16 -1
- package/dist/commands/ops-sections.js +89 -0
- package/dist/commands/ops.js +117 -120
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout.js +90 -68
- package/dist/commands/session-sync-failures.js +19 -13
- package/dist/commands/session-sync-record.js +53 -52
- package/dist/commands/session-sync-upload.js +15 -11
- package/dist/commands/sessions.js +61 -51
- package/dist/commands/slack.js +90 -61
- package/dist/commands/status.js +53 -41
- package/dist/commands/workbook.js +23 -20
- package/package.json +2 -2
package/dist/commands/ops.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { colorEnabled, dim, writeLine } from "./cli-io.js";
|
|
36
36
|
import { renderOpsStatus } from "./ops-render.js";
|
|
37
37
|
import { renderMemoryHooks, renderMemoryUsage, } from "./ops-render-memory.js";
|
|
38
|
+
import { readMemoryUsage, readModelBoardLines, readToolRouterBoard, toolRouterLine, turnAggregateLine, turnAggregateSection, } from "./ops-sections.js";
|
|
38
39
|
import { asRecord, callTower, openTower, writeCommandFailure } from "./tower-command.js";
|
|
39
40
|
/**
|
|
40
41
|
* A whole compile, plus a little. `maxDuration` on `/api/ops/recompile` is 800
|
|
@@ -51,16 +52,7 @@ export async function runOps(command, io) {
|
|
|
51
52
|
: runOpsRecompile(command, io, tower);
|
|
52
53
|
}
|
|
53
54
|
async function runOpsStatus(command, io, tower) {
|
|
54
|
-
const
|
|
55
|
-
if (command.job)
|
|
56
|
-
params.set("job", command.job);
|
|
57
|
-
if (command.skips)
|
|
58
|
-
params.set("skips", "1");
|
|
59
|
-
const query = params.toString();
|
|
60
|
-
const result = await callTower(tower, {
|
|
61
|
-
path: `/api/ops/status${query ? `?${query}` : ""}`,
|
|
62
|
-
label: "ops-status",
|
|
63
|
-
});
|
|
55
|
+
const result = await readOpsStatus(command, tower);
|
|
64
56
|
if (!result.ok) {
|
|
65
57
|
writeLine(io.stderr, `[ops cli] status not read ${JSON.stringify({
|
|
66
58
|
reason: result.reason,
|
|
@@ -82,72 +74,122 @@ async function runOpsStatus(command, io, tower) {
|
|
|
82
74
|
// different question from "is the pipeline healthy", and a card shelf that
|
|
83
75
|
// could not be read is not a collection outage.
|
|
84
76
|
const models = command.models ? await readModelBoardLines(io, tower) : null;
|
|
77
|
+
const turns = command.turns ? turnAggregateSection(payload) : null;
|
|
78
|
+
const toolRouter = command.toolRouter ? await readToolRouterBoard(io, tower) : null;
|
|
79
|
+
const unhealthy = rows.filter(isUnhealthyPipeline);
|
|
80
|
+
writeOpsStatusOutput(command, io, payload, memory, models, turns, toolRouter);
|
|
81
|
+
writeOpsStatusReadLog(command, io, payload, rows, unhealthy, memory, models, turns, toolRouter);
|
|
82
|
+
return unhealthy.length > 0 ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
async function readOpsStatus(command, tower) {
|
|
85
|
+
const params = new URLSearchParams();
|
|
86
|
+
if (command.job)
|
|
87
|
+
params.set("job", command.job);
|
|
88
|
+
if (command.skips)
|
|
89
|
+
params.set("skips", "1");
|
|
90
|
+
// The turn aggregates ride on the same status read (BLI-10136): one round
|
|
91
|
+
// trip, one board, never a second read of the same door for one section.
|
|
92
|
+
if (command.turns)
|
|
93
|
+
params.set("turns", "1");
|
|
94
|
+
const query = params.toString();
|
|
95
|
+
return callTower(tower, {
|
|
96
|
+
path: `/api/ops/status${query ? `?${query}` : ""}`,
|
|
97
|
+
label: "ops-status",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function isUnhealthyPipeline(row) {
|
|
85
101
|
// BLI-3723: `failing` joins the list. An older CLI that has never heard of it
|
|
86
102
|
// simply does not count it — which is why the server keeps the sentence in
|
|
87
103
|
// `detail`, so an un-upgraded terminal still PRINTS the outage even when it
|
|
88
104
|
// exits 0 over it.
|
|
89
|
-
|
|
105
|
+
return (row.verdict === "stale" ||
|
|
90
106
|
row.verdict === "never_produced" ||
|
|
91
107
|
row.verdict === "unreadable" ||
|
|
92
108
|
row.verdict === "failing" ||
|
|
93
109
|
// BLI-3762: a job that ran and wrote less than it owed is not healthy.
|
|
94
110
|
row.verdict === "degraded");
|
|
111
|
+
}
|
|
112
|
+
function writeOpsStatusOutput(command, io, payload, memory, models, turns, toolRouter) {
|
|
95
113
|
if (command.json) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
...(memory
|
|
99
|
-
? { memory: memory.section, hooks: memory.hooks, experience: memory.experience }
|
|
100
|
-
: {}),
|
|
101
|
-
...(models ? { models: models.board, modelsReason: models.reason } : {}),
|
|
102
|
-
}));
|
|
114
|
+
writeJsonOpsStatus(io, payload, memory, models, turns, toolRouter);
|
|
115
|
+
return;
|
|
103
116
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRouter);
|
|
118
|
+
}
|
|
119
|
+
function writeJsonOpsStatus(io, payload, memory, models, turns, toolRouter) {
|
|
120
|
+
writeLine(io.stdout, JSON.stringify({
|
|
121
|
+
...payload,
|
|
122
|
+
...(memory
|
|
123
|
+
? { memory: memory.section, hooks: memory.hooks, experience: memory.experience }
|
|
124
|
+
: {}),
|
|
125
|
+
...(models ? { models: models.board, modelsReason: models.reason } : {}),
|
|
126
|
+
...(turns ? { turns: turns.section, turnsReason: turns.reason } : {}),
|
|
127
|
+
...(toolRouter ? { toolRouter: toolRouter.board, toolRouterReason: toolRouter.reason } : {}),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
function writeHumanOpsStatus(command, io, payload, memory, models, turns, toolRouter) {
|
|
131
|
+
const styled = colorEnabled(io);
|
|
132
|
+
for (const line of renderOpsStatus(payload, (text) => dim(text, styled), {
|
|
133
|
+
alwaysShowBuckets: command.coverage === true,
|
|
134
|
+
})) {
|
|
135
|
+
writeLine(io.stdout, line);
|
|
136
|
+
}
|
|
137
|
+
if (memory?.section) {
|
|
138
|
+
for (const line of renderMemoryUsage(memory.section, (text) => dim(text, styled))) {
|
|
109
139
|
writeLine(io.stdout, line);
|
|
110
140
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
}
|
|
142
|
+
// BLI-3788. Printed whenever the door sent it, including when the usage
|
|
143
|
+
// half could not be read: "are the recalls arriving" and "is anybody
|
|
144
|
+
// saving" are two questions, and one being unreadable does not silence
|
|
145
|
+
// the other.
|
|
146
|
+
if (memory?.hooks) {
|
|
147
|
+
for (const line of renderMemoryHooks(memory.hooks, (text) => dim(text, styled))) {
|
|
148
|
+
writeLine(io.stdout, line);
|
|
115
149
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
150
|
+
}
|
|
151
|
+
if (memory?.experience) {
|
|
152
|
+
writeLine(io.stdout, `\nEXPERIENCE ${memory.experience.summary}`);
|
|
153
|
+
for (const line of memory.experience.lines)
|
|
154
|
+
writeLine(io.stdout, ` ${line}`);
|
|
155
|
+
}
|
|
156
|
+
if (models) {
|
|
157
|
+
writeLine(io.stdout, "");
|
|
158
|
+
if (models.lines.length > 0) {
|
|
159
|
+
for (const line of models.lines)
|
|
122
160
|
writeLine(io.stdout, line);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (memory?.experience) {
|
|
126
|
-
writeLine(io.stdout, `\nEXPERIENCE ${memory.experience.summary}`);
|
|
127
|
-
for (const line of memory.experience.lines)
|
|
128
|
-
writeLine(io.stdout, ` ${line}`);
|
|
129
|
-
}
|
|
130
|
-
if (models) {
|
|
131
|
-
writeLine(io.stdout, "");
|
|
132
|
-
if (models.lines.length > 0) {
|
|
133
|
-
for (const line of models.lines)
|
|
134
|
-
writeLine(io.stdout, line);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
// Never a silent gap where a section was asked for.
|
|
138
|
-
writeLine(io.stdout, `MODELS not read (${models.reason})`);
|
|
139
|
-
}
|
|
140
161
|
}
|
|
141
|
-
|
|
142
|
-
// Never a silent gap where a section was asked for
|
|
143
|
-
|
|
144
|
-
writeLine(io.stdout, "");
|
|
145
|
-
writeLine(io.stdout, `MEMORY not read (${memory.reason})`);
|
|
162
|
+
else {
|
|
163
|
+
// Never a silent gap where a section was asked for.
|
|
164
|
+
writeLine(io.stdout, `MODELS not read (${models.reason})`);
|
|
146
165
|
}
|
|
147
166
|
}
|
|
167
|
+
if (turns) {
|
|
168
|
+
writeLine(io.stdout, "");
|
|
169
|
+
writeLine(io.stdout, turns.section ? turnAggregateLine(turns.section) : `TURNS not read (${turns.reason})`);
|
|
170
|
+
}
|
|
171
|
+
if (toolRouter) {
|
|
172
|
+
writeLine(io.stdout, "");
|
|
173
|
+
writeLine(io.stdout, toolRouter.board
|
|
174
|
+
? toolRouterLine(toolRouter.board)
|
|
175
|
+
: `TOOL ROUTER not read (${toolRouter.reason})`);
|
|
176
|
+
}
|
|
177
|
+
if (memory && !memory.section) {
|
|
178
|
+
// Never a silent gap where a section was asked for: the reason the gauge
|
|
179
|
+
// could not be read is printed where the gauge would have been.
|
|
180
|
+
writeLine(io.stdout, "");
|
|
181
|
+
writeLine(io.stdout, `MEMORY not read (${memory.reason})`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function writeOpsStatusReadLog(command, io, payload, rows, unhealthy, memory, models, turns, toolRouter) {
|
|
148
185
|
// Both branches log — an all-green board that says nothing cannot answer
|
|
149
186
|
// "did anybody look today?".
|
|
150
187
|
writeLine(io.stderr, `[ops cli] status read ${JSON.stringify({
|
|
188
|
+
...createOpsStatusReadLog(command, payload, rows, unhealthy, memory, models, turns, toolRouter),
|
|
189
|
+
})}`);
|
|
190
|
+
}
|
|
191
|
+
function createOpsStatusReadLog(command, payload, rows, unhealthy, memory, models, turns, toolRouter) {
|
|
192
|
+
return {
|
|
151
193
|
job: command.job ?? "all",
|
|
152
194
|
pipelines: rows.length,
|
|
153
195
|
unhealthy: unhealthy.length,
|
|
@@ -187,63 +229,13 @@ async function runOpsStatus(command, io, tower) {
|
|
|
187
229
|
models_reason: models?.reason ?? null,
|
|
188
230
|
models_counted: models?.board?.rows?.length ?? null,
|
|
189
231
|
models_cost_today_usd: models?.board?.total_cost_today_usd ?? null,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* unreadable still prints the pipelines, the fleet and the coverage, and says
|
|
198
|
-
* in one line which half is missing and why. The status board's exit code is
|
|
199
|
-
* about collection health, and adoption is not that.
|
|
200
|
-
*/
|
|
201
|
-
async function readMemoryUsage(command, io, tower) {
|
|
202
|
-
const params = new URLSearchParams();
|
|
203
|
-
if (command.memoryDays !== undefined)
|
|
204
|
-
params.set("days", String(command.memoryDays));
|
|
205
|
-
const query = params.toString();
|
|
206
|
-
const result = await callTower(tower, {
|
|
207
|
-
path: `/api/ops/memory-usage${query ? `?${query}` : ""}`,
|
|
208
|
-
label: "ops-memory-usage",
|
|
209
|
-
});
|
|
210
|
-
if (!result.ok) {
|
|
211
|
-
writeLine(io.stderr, `[ops cli] memory usage not read ${JSON.stringify({
|
|
212
|
-
reason: result.reason,
|
|
213
|
-
http_status: result.httpStatus ?? null,
|
|
214
|
-
})}`);
|
|
215
|
-
return { section: null, hooks: null, experience: null, reason: result.reason };
|
|
216
|
-
}
|
|
217
|
-
const body = asRecord(result.body);
|
|
218
|
-
// BLI-3788: the hook counts ride the same answer, and their absence is a
|
|
219
|
-
// fact about the SERVER's version rather than about this fleet — an older
|
|
220
|
-
// dashboard sends no `hooks` key, and printing nothing is right there.
|
|
221
|
-
if (!body.memory)
|
|
222
|
-
return { section: null, hooks: body.hooks ?? null, experience: body.experience ?? null, reason: "memory_section_absent" };
|
|
223
|
-
return { section: body.memory, hooks: body.hooks ?? null, experience: body.experience ?? null, reason: "ok" };
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* The per-model board, read through its own door (BLI-3912).
|
|
227
|
-
*
|
|
228
|
-
* Same posture as the adoption gauge next door: it never fails the status
|
|
229
|
-
* board, and when it cannot be read the reason is printed where the section
|
|
230
|
-
* would have been. The LINES come from the server, so a terminal and the
|
|
231
|
-
* dashboard cannot disagree about what a model cost today.
|
|
232
|
-
*/
|
|
233
|
-
async function readModelBoardLines(io, tower) {
|
|
234
|
-
const result = await callTower(tower, { path: "/api/ops/models", label: "ops-models" });
|
|
235
|
-
if (!result.ok) {
|
|
236
|
-
writeLine(io.stderr, `[ops cli] model board not read ${JSON.stringify({
|
|
237
|
-
reason: result.reason,
|
|
238
|
-
http_status: result.httpStatus ?? null,
|
|
239
|
-
})}`);
|
|
240
|
-
return { lines: [], board: null, reason: result.reason };
|
|
241
|
-
}
|
|
242
|
-
const body = asRecord(result.body);
|
|
243
|
-
if (!body.lines || body.lines.length === 0) {
|
|
244
|
-
return { lines: [], board: body.board ?? null, reason: "models_section_absent" };
|
|
245
|
-
}
|
|
246
|
-
return { lines: body.lines, board: body.board ?? null, reason: "ok" };
|
|
232
|
+
turns_asked: Boolean(command.turns),
|
|
233
|
+
turns_reason: turns?.reason ?? null,
|
|
234
|
+
turns_count: turns?.section?.turns ?? null,
|
|
235
|
+
tool_router_asked: Boolean(command.toolRouter),
|
|
236
|
+
tool_router_reason: toolRouter?.reason ?? null,
|
|
237
|
+
tool_router_turns: toolRouter?.board?.turnsRouted ?? null,
|
|
238
|
+
};
|
|
247
239
|
}
|
|
248
240
|
async function runOpsRecompile(command, io, tower) {
|
|
249
241
|
const person = command.person ?? "";
|
|
@@ -278,11 +270,23 @@ async function runOpsRecompile(command, io, tower) {
|
|
|
278
270
|
})}`);
|
|
279
271
|
return writeCommandFailure(io, command.json, failure);
|
|
280
272
|
}
|
|
281
|
-
|
|
273
|
+
return writeOpsRecompileResult(command, io, asRecord(result.body), person);
|
|
274
|
+
}
|
|
275
|
+
function writeOpsRecompileResult(command, io, body, person) {
|
|
282
276
|
const status = typeof body["status"] === "string" ? body["status"] : "compiled";
|
|
283
277
|
const displayName = typeof body["displayName"] === "string" ? body["displayName"] : person;
|
|
284
278
|
const pageId = typeof body["pageId"] === "string" ? body["pageId"] : null;
|
|
285
279
|
const isNowLive = body["isNowLive"] === true;
|
|
280
|
+
writeRecompileOutput(command, io, body, status, displayName, pageId, isNowLive);
|
|
281
|
+
writeLine(io.stderr, `[ops cli] recompile done ${JSON.stringify({
|
|
282
|
+
status,
|
|
283
|
+
page_id: pageId,
|
|
284
|
+
is_now_live: status === "compiled" ? isNowLive : null,
|
|
285
|
+
dry_run: command.dryRun === true,
|
|
286
|
+
})}`);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
function writeRecompileOutput(command, io, body, status, displayName, pageId, isNowLive) {
|
|
286
290
|
if (command.json) {
|
|
287
291
|
writeLine(io.stdout, JSON.stringify(body));
|
|
288
292
|
}
|
|
@@ -301,11 +305,4 @@ async function runOpsRecompile(command, io, tower) {
|
|
|
301
305
|
: "It is NOT the page being served: a scheduled compile is stamped for a later delivery window, " +
|
|
302
306
|
"so that one still wins. Nothing was lost — this is a new version alongside it.");
|
|
303
307
|
}
|
|
304
|
-
writeLine(io.stderr, `[ops cli] recompile done ${JSON.stringify({
|
|
305
|
-
status,
|
|
306
|
-
page_id: pageId,
|
|
307
|
-
is_now_live: status === "compiled" ? isNowLive : null,
|
|
308
|
-
dry_run: command.dryRun === true,
|
|
309
|
-
})}`);
|
|
310
|
-
return 0;
|
|
311
308
|
}
|
|
@@ -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.122");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/dist/commands/scout.js
CHANGED
|
@@ -25,9 +25,20 @@ export async function runScout(command, io) {
|
|
|
25
25
|
const session = await loadPairedSession("scout", command.homeDir);
|
|
26
26
|
const dashboardUrl = command.dashboardUrl ?? session.dashboard_url;
|
|
27
27
|
const log = (line) => writeLine(io.stderr, line);
|
|
28
|
+
const read = await readScoutBoard(command, io, session, dashboardUrl, log);
|
|
29
|
+
if (!read.ok) {
|
|
30
|
+
writeFailure(command, io, read);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
if (command.action === "board") {
|
|
34
|
+
return writeScoutBoard(command, io, read.body);
|
|
35
|
+
}
|
|
36
|
+
return runScoutAction(command, command.action, io, read.body, session, dashboardUrl, log);
|
|
37
|
+
}
|
|
38
|
+
async function readScoutBoard(command, io, session, dashboardUrl, log) {
|
|
28
39
|
// The board is read first even for an action: the id prefix is resolved
|
|
29
40
|
// against it locally, and a refused action still leaves a readable board.
|
|
30
|
-
|
|
41
|
+
return towerJsonRequest({
|
|
31
42
|
dashboardUrl,
|
|
32
43
|
path: command.days ? `/api/cockpit/scout?days=${command.days}` : "/api/cockpit/scout",
|
|
33
44
|
method: "GET",
|
|
@@ -37,53 +48,41 @@ export async function runScout(command, io) {
|
|
|
37
48
|
timeoutMs: READ_DEADLINE_MS,
|
|
38
49
|
log,
|
|
39
50
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return 1;
|
|
43
|
-
}
|
|
44
|
-
const payload = read.body;
|
|
51
|
+
}
|
|
52
|
+
function writeScoutBoard(command, io, payload) {
|
|
45
53
|
const board = payload.board ?? {};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
writeLine(io.stdout, JSON.stringify({ ok: true, audience: payload.audience ?? null, windowDays: payload.windowDays ?? board.windowDays ?? null, board, lines }));
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
53
|
-
writeLine(io.stdout, line);
|
|
54
|
-
}
|
|
55
|
-
writeLine(io.stderr, `[scout cli] board read ${JSON.stringify({
|
|
54
|
+
if (command.json) {
|
|
55
|
+
writeLine(io.stdout, JSON.stringify({
|
|
56
|
+
ok: true,
|
|
56
57
|
audience: payload.audience ?? null,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
truncated: truncationLines(board).length > 0,
|
|
62
|
-
})}`);
|
|
63
|
-
return 0;
|
|
58
|
+
windowDays: payload.windowDays ?? board.windowDays ?? null,
|
|
59
|
+
board,
|
|
60
|
+
lines: payload.lines ?? {},
|
|
61
|
+
}));
|
|
64
62
|
}
|
|
63
|
+
else {
|
|
64
|
+
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
65
|
+
writeLine(io.stdout, line);
|
|
66
|
+
}
|
|
67
|
+
writeLine(io.stderr, `[scout cli] board read ${JSON.stringify({
|
|
68
|
+
audience: payload.audience ?? null,
|
|
69
|
+
window_days: payload.windowDays ?? board.windowDays ?? null,
|
|
70
|
+
experiments: board.experiments?.length ?? 0,
|
|
71
|
+
settled: board.settled?.length ?? 0,
|
|
72
|
+
signals: board.signals?.length ?? 0,
|
|
73
|
+
truncated: truncationLines(board).length > 0,
|
|
74
|
+
})}`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
async function runScoutAction(command, action, io, payload, session, dashboardUrl, log) {
|
|
78
|
+
const board = payload.board ?? {};
|
|
79
|
+
const lines = payload.lines ?? {};
|
|
65
80
|
const ref = command.experimentRef ?? "";
|
|
66
81
|
const resolution = resolveExperimentRef(board, ref);
|
|
67
82
|
if (resolution.status !== "ok") {
|
|
68
|
-
|
|
69
|
-
writeLine(io.stderr, `[scout cli] action refused ${JSON.stringify({
|
|
70
|
-
reason: resolution.status,
|
|
71
|
-
action: command.action,
|
|
72
|
-
// The typed reference is a person's own input; only its shape is logged.
|
|
73
|
-
ref_length: ref.length,
|
|
74
|
-
candidates: resolution.status === "ambiguous_prefix" ? resolution.candidates.length : 0,
|
|
75
|
-
})}`);
|
|
76
|
-
if (command.json) {
|
|
77
|
-
writeLine(io.stdout, JSON.stringify({ ok: false, error: resolution.status, detail: sentence }));
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
writeLine(io.stderr, sentence);
|
|
81
|
-
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
82
|
-
writeLine(io.stdout, line);
|
|
83
|
-
}
|
|
84
|
-
return 1;
|
|
83
|
+
return writeScoutRefusal(command, action, io, payload, ref, resolution);
|
|
85
84
|
}
|
|
86
|
-
const serverAction =
|
|
85
|
+
const serverAction = action === "undo" ? "undo_dismiss" : action;
|
|
87
86
|
const applied = await towerJsonRequest({
|
|
88
87
|
dashboardUrl,
|
|
89
88
|
path: "/api/cockpit/scout",
|
|
@@ -96,32 +95,7 @@ export async function runScout(command, io) {
|
|
|
96
95
|
log,
|
|
97
96
|
});
|
|
98
97
|
if (!applied.ok) {
|
|
99
|
-
|
|
100
|
-
reason: applied.reason,
|
|
101
|
-
action: serverAction,
|
|
102
|
-
http_status: applied.httpStatus ?? null,
|
|
103
|
-
})}`);
|
|
104
|
-
if (command.json) {
|
|
105
|
-
writeLine(io.stdout, JSON.stringify({
|
|
106
|
-
ok: false,
|
|
107
|
-
error: applied.reason,
|
|
108
|
-
detail: failureDetail(applied),
|
|
109
|
-
httpStatus: applied.httpStatus ?? null,
|
|
110
|
-
board,
|
|
111
|
-
lines,
|
|
112
|
-
}));
|
|
113
|
-
return 1;
|
|
114
|
-
}
|
|
115
|
-
// The server's own refusal first, then one line saying which gate it was —
|
|
116
|
-
// and the board underneath, because being refused an action never costs a
|
|
117
|
-
// person the read they already had.
|
|
118
|
-
writeLine(io.stderr, `Tower refused that action: ${failureDetail(applied)}`);
|
|
119
|
-
if (applied.httpStatus === 403) {
|
|
120
|
-
writeLine(io.stderr, "Deciding a Scout card is a super_admin action; reading the board is not.");
|
|
121
|
-
}
|
|
122
|
-
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
123
|
-
writeLine(io.stdout, line);
|
|
124
|
-
return 1;
|
|
98
|
+
return writeScoutActionFailure(command, io, payload, board, lines, serverAction, applied);
|
|
125
99
|
}
|
|
126
100
|
const card = resolution.card;
|
|
127
101
|
writeLine(io.stderr, `[scout cli] action applied ${JSON.stringify({ action: serverAction, http_status: applied.httpStatus })}`);
|
|
@@ -129,10 +103,58 @@ export async function runScout(command, io) {
|
|
|
129
103
|
writeLine(io.stdout, JSON.stringify({ ok: true, action: serverAction, experiment: applied.body?.experiment ?? null }));
|
|
130
104
|
}
|
|
131
105
|
else {
|
|
132
|
-
writeLine(io.stdout, `${pastTense(
|
|
106
|
+
writeLine(io.stdout, `${pastTense(action)} ${shortId(resolution.id)} · ${card.claimSummary ?? card.title ?? "that card"}`);
|
|
133
107
|
}
|
|
134
108
|
return 0;
|
|
135
109
|
}
|
|
110
|
+
function writeScoutRefusal(command, action, io, payload, ref, resolution) {
|
|
111
|
+
const sentence = refusalSentence(resolution, ref);
|
|
112
|
+
writeLine(io.stderr, `[scout cli] action refused ${JSON.stringify({
|
|
113
|
+
reason: resolution.status,
|
|
114
|
+
action,
|
|
115
|
+
// The typed reference is a person's own input; only its shape is logged.
|
|
116
|
+
ref_length: ref.length,
|
|
117
|
+
candidates: resolution.status === "ambiguous_prefix" ? resolution.candidates.length : 0,
|
|
118
|
+
})}`);
|
|
119
|
+
if (command.json) {
|
|
120
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: resolution.status, detail: sentence }));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
writeLine(io.stderr, sentence);
|
|
124
|
+
for (const line of renderScoutBoard(payload, colorEnabled(io)))
|
|
125
|
+
writeLine(io.stdout, line);
|
|
126
|
+
}
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
function writeScoutActionFailure(command, io, payload, board, lines, serverAction, applied) {
|
|
130
|
+
writeLine(io.stderr, `[scout cli] action failed ${JSON.stringify({
|
|
131
|
+
reason: applied.reason,
|
|
132
|
+
action: serverAction,
|
|
133
|
+
http_status: applied.httpStatus ?? null,
|
|
134
|
+
})}`);
|
|
135
|
+
if (command.json) {
|
|
136
|
+
writeLine(io.stdout, JSON.stringify({
|
|
137
|
+
ok: false,
|
|
138
|
+
error: applied.reason,
|
|
139
|
+
detail: failureDetail(applied),
|
|
140
|
+
httpStatus: applied.httpStatus ?? null,
|
|
141
|
+
board,
|
|
142
|
+
lines,
|
|
143
|
+
}));
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
// The server's own refusal first, then one line saying which gate it was —
|
|
147
|
+
// and the board underneath, because being refused an action never costs a
|
|
148
|
+
// person the read they already had.
|
|
149
|
+
writeLine(io.stderr, `Tower refused that action: ${failureDetail(applied)}`);
|
|
150
|
+
if (applied.httpStatus === 403) {
|
|
151
|
+
writeLine(io.stderr, "Deciding a Scout card is a super_admin action; reading the board is not.");
|
|
152
|
+
}
|
|
153
|
+
for (const line of renderScoutBoard(payload, colorEnabled(io))) {
|
|
154
|
+
writeLine(io.stdout, line);
|
|
155
|
+
}
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
136
158
|
// ----------------------------------------------------------------- small parts
|
|
137
159
|
function writeFailure(command, io, failure) {
|
|
138
160
|
const detail = failureDetail(failure);
|
|
@@ -57,19 +57,9 @@ export function createSyncFailureLedger() {
|
|
|
57
57
|
* silenced; only the verdict changed.
|
|
58
58
|
*/
|
|
59
59
|
export function recordWorktreeDeliveryFailures(ledger, outcomes) {
|
|
60
|
-
for (const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
? {
|
|
64
|
-
label: sync.failure_class,
|
|
65
|
-
rendered: `${worktree.worktree_label}:${sync.failure_reason}`,
|
|
66
|
-
http_status: sync.failure_http_status,
|
|
67
|
-
}
|
|
68
|
-
: {
|
|
69
|
-
label: "upload_not_completed",
|
|
70
|
-
rendered: `${worktree.worktree_label}:upload_${sync.status}`,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
60
|
+
for (const outcome of outcomes) {
|
|
61
|
+
recordWorktreeSyncFailure(ledger, outcome);
|
|
62
|
+
const { sync } = outcome;
|
|
73
63
|
for (const reason of sync.raw_evidence_failure_reasons ?? []) {
|
|
74
64
|
if (isDeliveryHoldReason(reason))
|
|
75
65
|
continue;
|
|
@@ -90,6 +80,22 @@ export function recordWorktreeDeliveryFailures(ledger, outcomes) {
|
|
|
90
80
|
ledger.fail(sync.raw_evidence_deferred_object_budget > 0, "deferred_object_budget");
|
|
91
81
|
}
|
|
92
82
|
}
|
|
83
|
+
function recordWorktreeSyncFailure(ledger, { worktree, sync }) {
|
|
84
|
+
if (sync.status === "uploaded")
|
|
85
|
+
return;
|
|
86
|
+
if (sync.status === "spooled" && sync.failure_reason) {
|
|
87
|
+
ledger.add({
|
|
88
|
+
label: sync.failure_class,
|
|
89
|
+
rendered: `${worktree.worktree_label}:${sync.failure_reason}`,
|
|
90
|
+
http_status: sync.failure_http_status,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ledger.add({
|
|
95
|
+
label: "upload_not_completed",
|
|
96
|
+
rendered: `${worktree.worktree_label}:upload_${sync.status}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
93
99
|
/**
|
|
94
100
|
* What the scan itself got wrong: a window that hit its cap, sessions that
|
|
95
101
|
* could not be read, or a session store that could not be read at all.
|
|
@@ -220,60 +220,61 @@ async function recordClaudeSessionObservationsForSync(options) {
|
|
|
220
220
|
*/
|
|
221
221
|
function recordSourceObservations(options) {
|
|
222
222
|
const seen = new Set(options.results.map((result) => options.sessionIdOf(result)));
|
|
223
|
-
|
|
224
|
-
for (const [sessionId, entry] of Object.entries(options.cursor.sessions)) {
|
|
225
|
-
if (seen.has(sessionId) ||
|
|
226
|
-
!liveSyncCursorEntryRequiresRetry(entry)) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
options.cursor.sessions[sessionId] = {
|
|
230
|
-
...entry,
|
|
231
|
-
state: "skipped",
|
|
232
|
-
reason: "retry_source_missing",
|
|
233
|
-
last_seen_at: options.now.toISOString(),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
223
|
+
markMissingRetrySourcesSkipped(options.cursor, seen, options.now, options.terminalizeMissingUndurable);
|
|
237
224
|
const stale = countStaleSessions(options.cursor, seen);
|
|
238
225
|
for (const result of options.results) {
|
|
239
|
-
|
|
240
|
-
const reported = options.sessions.find((session) => session.source === options.source &&
|
|
241
|
-
session.codex_session_id === sessionId);
|
|
242
|
-
const uploadedThisSync = reported?.upload_state === "uploaded";
|
|
243
|
-
const durableThisSync = reported?.upload_state === "uploaded" ||
|
|
244
|
-
reported?.upload_state === "reused_existing";
|
|
245
|
-
const prior = options.priorCursor?.sessions[sessionId];
|
|
246
|
-
// D21 / no-flip-flop: a sync that is spooled (offline), budget-deferred, or
|
|
247
|
-
// upload-failed for a session that was ALREADY durable must NOT wipe the
|
|
248
|
-
// prior durable state — otherwise damping is forfeited forever and the
|
|
249
|
-
// store row oscillates uploaded -> not_uploaded hourly. Carry the prior
|
|
250
|
-
// durable pointer/timestamp/size forward unless we durably uploaded anew.
|
|
251
|
-
const uploadedObjectKey = durableThisSync
|
|
252
|
-
? (reported?.raw_evidence_pointer_id ?? prior?.uploaded_object_key ?? null)
|
|
253
|
-
: (prior?.uploaded_object_key ?? null);
|
|
254
|
-
const uploadedAt = uploadedThisSync
|
|
255
|
-
? options.now.toISOString()
|
|
256
|
-
: (prior?.uploaded_at ?? (durableThisSync ? options.now.toISOString() : null));
|
|
257
|
-
const uploadedByteSize = uploadedThisSync
|
|
258
|
-
? result.byte_size
|
|
259
|
-
: (prior?.uploaded_byte_size ??
|
|
260
|
-
(durableThisSync ? result.byte_size : null));
|
|
261
|
-
const entry = {
|
|
262
|
-
file_hash_sha256: result.content_hash_sha256,
|
|
263
|
-
file_mtime_ms: result.session_file_mtime_ms,
|
|
264
|
-
byte_size: result.byte_size,
|
|
265
|
-
// Durable byte offset reflects how many bytes are durable remotely (the
|
|
266
|
-
// last uploaded size), not the current file size.
|
|
267
|
-
byte_offset: uploadedByteSize ?? 0,
|
|
268
|
-
state: result.state,
|
|
269
|
-
reason: result.reason,
|
|
270
|
-
worktree_fingerprint: result.worktree?.worktree_fingerprint ?? null,
|
|
271
|
-
uploaded_object_key: uploadedObjectKey,
|
|
272
|
-
uploaded_at: uploadedAt,
|
|
273
|
-
uploaded_byte_size: uploadedByteSize,
|
|
274
|
-
last_seen_at: options.now.toISOString(),
|
|
275
|
-
};
|
|
276
|
-
recordSessionObservation(options.cursor, sessionId, entry);
|
|
226
|
+
recordSourceObservation(options, result);
|
|
277
227
|
}
|
|
278
228
|
return stale;
|
|
229
|
+
}
|
|
230
|
+
function markMissingRetrySourcesSkipped(cursor, seen, now, terminalizeMissingUndurable) {
|
|
231
|
+
if (!terminalizeMissingUndurable)
|
|
232
|
+
return;
|
|
233
|
+
for (const [sessionId, entry] of Object.entries(cursor.sessions)) {
|
|
234
|
+
if (seen.has(sessionId) || !liveSyncCursorEntryRequiresRetry(entry))
|
|
235
|
+
continue;
|
|
236
|
+
cursor.sessions[sessionId] = {
|
|
237
|
+
...entry,
|
|
238
|
+
state: "skipped",
|
|
239
|
+
reason: "retry_source_missing",
|
|
240
|
+
last_seen_at: now.toISOString(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function recordSourceObservation(options, result) {
|
|
245
|
+
const sessionId = options.sessionIdOf(result);
|
|
246
|
+
const reported = options.sessions.find((session) => session.source === options.source && session.codex_session_id === sessionId);
|
|
247
|
+
const uploadedThisSync = reported?.upload_state === "uploaded";
|
|
248
|
+
const durableThisSync = reported?.upload_state === "uploaded" || reported?.upload_state === "reused_existing";
|
|
249
|
+
const prior = options.priorCursor?.sessions[sessionId];
|
|
250
|
+
// D21 / no-flip-flop: a sync that is spooled (offline), budget-deferred, or
|
|
251
|
+
// upload-failed for a session that was ALREADY durable must NOT wipe the
|
|
252
|
+
// prior durable state — otherwise damping is forfeited forever and the
|
|
253
|
+
// store row oscillates uploaded -> not_uploaded hourly. Carry the prior
|
|
254
|
+
// durable pointer/timestamp/size forward unless we durably uploaded anew.
|
|
255
|
+
const uploadedObjectKey = durableThisSync
|
|
256
|
+
? (reported?.raw_evidence_pointer_id ?? prior?.uploaded_object_key ?? null)
|
|
257
|
+
: (prior?.uploaded_object_key ?? null);
|
|
258
|
+
const uploadedAt = uploadedThisSync
|
|
259
|
+
? options.now.toISOString()
|
|
260
|
+
: (prior?.uploaded_at ?? (durableThisSync ? options.now.toISOString() : null));
|
|
261
|
+
const uploadedByteSize = uploadedThisSync
|
|
262
|
+
? result.byte_size
|
|
263
|
+
: (prior?.uploaded_byte_size ?? (durableThisSync ? result.byte_size : null));
|
|
264
|
+
const entry = {
|
|
265
|
+
file_hash_sha256: result.content_hash_sha256,
|
|
266
|
+
file_mtime_ms: result.session_file_mtime_ms,
|
|
267
|
+
byte_size: result.byte_size,
|
|
268
|
+
// Durable byte offset reflects how many bytes are durable remotely (the
|
|
269
|
+
// last uploaded size), not the current file size.
|
|
270
|
+
byte_offset: uploadedByteSize ?? 0,
|
|
271
|
+
state: result.state,
|
|
272
|
+
reason: result.reason,
|
|
273
|
+
worktree_fingerprint: result.worktree?.worktree_fingerprint ?? null,
|
|
274
|
+
uploaded_object_key: uploadedObjectKey,
|
|
275
|
+
uploaded_at: uploadedAt,
|
|
276
|
+
uploaded_byte_size: uploadedByteSize,
|
|
277
|
+
last_seen_at: options.now.toISOString(),
|
|
278
|
+
};
|
|
279
|
+
recordSessionObservation(options.cursor, sessionId, entry);
|
|
279
280
|
}
|