@bli-cockpit/cli 0.2.256 → 0.2.258
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/agent-session-root-scope.js +19 -0
- package/dist/commands/backfill-outside-roots.js +7 -3
- package/dist/commands/local-args-tower-admin.js +13 -1
- package/dist/commands/local-help-commands.js +5 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +10 -5
- package/dist/commands/team.js +33 -0
- package/package.json +2 -2
|
@@ -16,4 +16,23 @@ import { OUTSIDE_APPROVED_ROOT_REASONS } from "./session-sync-health.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export function rootScopeForReason(reason) {
|
|
18
18
|
return OUTSIDE_APPROVED_ROOT_REASONS.has(reason) ? "outside" : "inside";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The terminal attribution states: attribution finished and named no repo. They
|
|
22
|
+
* are labels, never a reason to withhold (session-first collection).
|
|
23
|
+
*/
|
|
24
|
+
const TERMINAL_ATTRIBUTION_STATES = new Set(["ambiguous", "unattributed", "skipped"]);
|
|
25
|
+
/**
|
|
26
|
+
* Reasons a session carries no workspace of its own that are NOT an outside-root
|
|
27
|
+
* fact, so `rootScopeForReason` keeps calling them "inside". Today: a transcript
|
|
28
|
+
* with no cwd, workspace or origin signal at all (`no_repo_signals`).
|
|
29
|
+
*/
|
|
30
|
+
export const NO_WORKSPACE_REASONS = new Set(["no_repo_signals"]);
|
|
31
|
+
/**
|
|
32
|
+
* Does this session still lack a workspace the upload can stand in? Every
|
|
33
|
+
* terminal state without a worktree rides the collector's synthetic workspace,
|
|
34
|
+
* whatever its reason; attribution stays exactly as scored.
|
|
35
|
+
*/
|
|
36
|
+
export function needsTransportWorkspace(result) {
|
|
37
|
+
return !result.worktree && TERMINAL_ATTRIBUTION_STATES.has(result.state);
|
|
19
38
|
}
|
|
@@ -24,7 +24,7 @@ import fs from "node:fs/promises";
|
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
26
26
|
import { repoFingerprintFromLocalRoot, stableWorktreeFingerprint, } from "../repo-identity.js";
|
|
27
|
-
import { rootScopeForReason } from "./agent-session-root-scope.js";
|
|
27
|
+
import { NO_WORKSPACE_REASONS, needsTransportWorkspace, rootScopeForReason } from "./agent-session-root-scope.js";
|
|
28
28
|
/**
|
|
29
29
|
* The label the sync path writes for a report with no real worktree
|
|
30
30
|
* (`session-sync-record.ts`). Reused verbatim: one label, not a second one.
|
|
@@ -64,18 +64,22 @@ export function isOutsideRootCandidate(candidate) {
|
|
|
64
64
|
export function labelOutsideRootCandidates(candidates, workspace) {
|
|
65
65
|
let codex = 0;
|
|
66
66
|
let claude = 0;
|
|
67
|
+
let noRepoSignals = 0;
|
|
67
68
|
const labelled = candidates.map((candidate) => {
|
|
68
|
-
if (
|
|
69
|
+
if (!needsTransportWorkspace(candidate))
|
|
69
70
|
return candidate;
|
|
70
71
|
if (candidate.source === "codex")
|
|
71
72
|
codex += 1;
|
|
72
73
|
else
|
|
73
74
|
claude += 1;
|
|
75
|
+
if (NO_WORKSPACE_REASONS.has(candidate.reason))
|
|
76
|
+
noRepoSignals += 1;
|
|
74
77
|
return { ...candidate, worktree: workspace };
|
|
75
78
|
});
|
|
76
79
|
console.error("[backfill] outside-root sessions labelled for upload", JSON.stringify({
|
|
77
|
-
|
|
80
|
+
reason: "unattributed_sessions_labelled_for_upload",
|
|
78
81
|
count: codex + claude,
|
|
82
|
+
no_repo_signals_count: noRepoSignals,
|
|
79
83
|
codex_count: codex,
|
|
80
84
|
claude_count: claude,
|
|
81
85
|
candidate_count: candidates.length,
|
|
@@ -375,6 +375,18 @@ function parseTeamDeviceArgs(rest, flags, common) {
|
|
|
375
375
|
}
|
|
376
376
|
return { kind: "team", action: "device-scope", deviceRef, deviceScope, ...common };
|
|
377
377
|
}
|
|
378
|
+
if (deviceVerb === "mark-service" || deviceVerb === "unmark-service") {
|
|
379
|
+
const reasonLabel = optionalNonEmpty(flags.get("--reason"));
|
|
380
|
+
if (!deviceRef || extra.length > 0)
|
|
381
|
+
throw new Error(`team device ${deviceVerb} takes one <id|name>${deviceVerb === "mark-service" ? " and --reason <label>" : ""}.`);
|
|
382
|
+
if (deviceVerb === "mark-service") {
|
|
383
|
+
if (!reasonLabel || !/^[a-z0-9][a-z0-9_-]{1,39}$/.test(reasonLabel)) {
|
|
384
|
+
throw new Error("team device mark-service needs --reason <label> (2-40 chars: a-z, 0-9, _ or -), e.g. --reason boat-agent.");
|
|
385
|
+
}
|
|
386
|
+
return { kind: "team", action: "device-mark-service", deviceRef, reasonLabel, ...common };
|
|
387
|
+
}
|
|
388
|
+
return { kind: "team", action: "device-unmark-service", deviceRef, ...common };
|
|
389
|
+
}
|
|
378
390
|
if (extra.length > 0)
|
|
379
391
|
throw new Error("team device does not accept that many arguments.");
|
|
380
392
|
if (!deviceVerb || deviceVerb === "list") {
|
|
@@ -406,7 +418,7 @@ function parseTeamDeviceArgs(rest, flags, common) {
|
|
|
406
418
|
...common,
|
|
407
419
|
};
|
|
408
420
|
}
|
|
409
|
-
throw new Error("team device takes one verb: list, doctor <id|name>, scope <id|name> notetaker_bot|none, or revoke <id|name>.");
|
|
421
|
+
throw new Error("team device takes one verb: list, doctor <id|name>, scope <id|name> notetaker_bot|none, mark-service <id|name> --reason <label>, unmark-service <id|name>, or revoke <id|name>.");
|
|
410
422
|
}
|
|
411
423
|
/** `cockpit model [show|set <key>]` — the alias surface for the personal chat model. */
|
|
412
424
|
export function parseModelArgs(args) {
|
|
@@ -211,6 +211,11 @@ const COMMAND_HELP = new Map([
|
|
|
211
211
|
" cockpit team device scope <id|name> notetaker_bot|none [--json]",
|
|
212
212
|
" Super admin only. Gives the meeting bot's device the scope Tower's claim door requires,",
|
|
213
213
|
" or takes it away. Without it the bot is refused every meeting (bot_scope_required).",
|
|
214
|
+
" cockpit team device mark-service <id|name> --reason <label> [--json]",
|
|
215
|
+
" cockpit team device unmark-service <id|name> [--json]",
|
|
216
|
+
" Super admin only. Marks a device as automation (cloud agents, alert senders) so the fleet board",
|
|
217
|
+
" lists it under service identities, judged only on when it last called Tower, instead of a red laptop.",
|
|
218
|
+
" --reason is a short label such as boat-agent. Only this door can set it; a device cannot.",
|
|
214
219
|
" cockpit team device revoke <id|name> --reason <label> [--note \"<text>\"] [--yes]",
|
|
215
220
|
" Admin only. Names the device, asks first unless --yes, then revokes it —",
|
|
216
221
|
" this cannot be undone. --reason must be one of: stale_test_pairing,",
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.258");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -47,7 +47,7 @@ import { isRedactionStubObjectKey } from "../cursors/raw-evidence-stub-entry.js"
|
|
|
47
47
|
import { describeError } from "../health-detail.js";
|
|
48
48
|
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
49
49
|
import { isMissingFileError, writeJsonFileAtomic, writeJsonFile } from "../local-state-files.js";
|
|
50
|
-
import { rootScopeForReason } from "./agent-session-root-scope.js";
|
|
50
|
+
import { NO_WORKSPACE_REASONS, needsTransportWorkspace, rootScopeForReason } from "./agent-session-root-scope.js";
|
|
51
51
|
import { ensureOutsideRootsWorkspace, isOutsideRootsWorkspace, outsideRootsWorkspace, } from "./backfill-outside-roots.js";
|
|
52
52
|
/** Most outside-root sessions that upload bytes in one tick. 4 ticks an hour, so up to 3,840 a day. */
|
|
53
53
|
export const OUTSIDE_ROOT_SESSIONS_PER_TICK = 40;
|
|
@@ -112,7 +112,7 @@ export function widenForCatchUp(sinceMinutes, catchUpPending, normalMinutes) {
|
|
|
112
112
|
* than the horizon (that is backfill's).
|
|
113
113
|
*/
|
|
114
114
|
export function outsideRootEntryOwesUpload(entry, now = new Date()) {
|
|
115
|
-
if (rootScopeForReason(entry.reason) !== "outside")
|
|
115
|
+
if (rootScopeForReason(entry.reason) !== "outside" && !NO_WORKSPACE_REASONS.has(entry.reason))
|
|
116
116
|
return false;
|
|
117
117
|
if (entry.uploaded_object_key && !isRedactionStubObjectKey(entry.uploaded_object_key))
|
|
118
118
|
return false;
|
|
@@ -120,9 +120,13 @@ export function outsideRootEntryOwesUpload(entry, now = new Date()) {
|
|
|
120
120
|
return false;
|
|
121
121
|
return now.getTime() - entry.file_mtime_ms <= OUTSIDE_ROOT_HORIZON_MINUTES * 60_000;
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Is this scan result a session with no worktree of its own that attribution
|
|
125
|
+
* left unattributed (outside every approved root, no repo signals, or any other
|
|
126
|
+
* terminal state)? All of them ride the synthetic workspace: a label is never a gate.
|
|
127
|
+
*/
|
|
124
128
|
function isUnclaimedOutsideRootResult(result) {
|
|
125
|
-
return
|
|
129
|
+
return needsTransportWorkspace(result);
|
|
126
130
|
}
|
|
127
131
|
/** Already durable remotely with the bytes the scan just read: it needs no upload. */
|
|
128
132
|
function isDurableAndUnchanged(result, entry) {
|
|
@@ -224,7 +228,8 @@ export async function admitOutsideRootSessions(options) {
|
|
|
224
228
|
},
|
|
225
229
|
outsideRoots: { workspace, holds, counts },
|
|
226
230
|
};
|
|
227
|
-
|
|
231
|
+
const noRepoSignals = admitted.filter((sighting) => NO_WORKSPACE_REASONS.has(sighting.result.reason)).length;
|
|
232
|
+
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "outside_root_sessions_admitted", no_repo_signals_riding: noRepoSignals, ...counts, ...budget }));
|
|
228
233
|
return labelled;
|
|
229
234
|
}
|
|
230
235
|
/** Session ids and queue turns only; no source paths or transcript content. */
|
package/dist/commands/team.js
CHANGED
|
@@ -41,6 +41,8 @@ export async function runTeam(command, io) {
|
|
|
41
41
|
return deviceDoctor(command, tower, io);
|
|
42
42
|
if (command.action === "device-scope")
|
|
43
43
|
return deviceScope(command, tower, io);
|
|
44
|
+
if (command.action === "device-mark-service" || command.action === "device-unmark-service")
|
|
45
|
+
return deviceMarkService(command, tower, io);
|
|
44
46
|
return deviceRevoke(command, tower, io);
|
|
45
47
|
}
|
|
46
48
|
async function showMembers(command, tower, io) {
|
|
@@ -246,6 +248,37 @@ async function deviceScope(command, tower, io) {
|
|
|
246
248
|
: command.deviceScope === "none" ? `Took the service scope away from ${name}.` : `${name} now holds the notetaker_bot scope. The bot's next claim will go through.`);
|
|
247
249
|
return 0;
|
|
248
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* `cockpit team device mark-service <id|name> --reason <label>` and `unmark-service <id|name>` (ticket 23031): the ONE
|
|
253
|
+
* door that tells the fleet board a device is automation, not a laptop. Super admin on the server.
|
|
254
|
+
*/
|
|
255
|
+
async function deviceMarkService(command, tower, io) {
|
|
256
|
+
const mark = command.action === "device-mark-service";
|
|
257
|
+
const listed = await callTower(tower, { path: "/api/team/devices", label: "team device mark-service lookup" });
|
|
258
|
+
if (!listed.ok)
|
|
259
|
+
return writeCommandFailure(io, command.json, listed);
|
|
260
|
+
const device = findDevice(devicesOf(asRecord(listed.body)), command.deviceRef ?? "");
|
|
261
|
+
if (!device)
|
|
262
|
+
return writeCommandFailure(io, command.json, { reason: "device_not_found", detail: "No unique device matches. Run cockpit team device list to see ids and names." });
|
|
263
|
+
const result = await callTower(tower, {
|
|
264
|
+
path: `/api/ambient/devices/${encodeURIComponent(device.device_id)}/service-mark`,
|
|
265
|
+
method: "POST", label: "team device mark-service", body: mark ? { mark: true, reason: command.reasonLabel } : { mark: false },
|
|
266
|
+
});
|
|
267
|
+
writeLine(io.stderr, `[team cli] device mark-service ${JSON.stringify({ device_id: device.device_id, mark, reason: mark ? command.reasonLabel : null, outcome: result.ok ? "answered" : result.reason })}`);
|
|
268
|
+
if (!result.ok)
|
|
269
|
+
return writeCommandFailure(io, command.json, result);
|
|
270
|
+
const body = asRecord(result.body);
|
|
271
|
+
if (command.json) {
|
|
272
|
+
writeLine(io.stdout, JSON.stringify({ ok: true, ...body }));
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const name = String(device.device_name ?? device.device_id);
|
|
276
|
+
writeLine(io.stdout, body.changed === false
|
|
277
|
+
? `${name} was already ${mark ? "marked as automation" : "not marked"}. Nothing changed.`
|
|
278
|
+
: mark ? `${name} is now marked as automation (${command.reasonLabel}). The fleet board lists it under service identities.`
|
|
279
|
+
: `Took the automation mark off ${name}. The fleet board grades it as a laptop again.`);
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
249
282
|
/** Exact directory labels locate a roster id; attribution remains the server resolver's job. */
|
|
250
283
|
async function githubLogin(command, tower, io) {
|
|
251
284
|
let personId = command.personRef ?? "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.258",
|
|
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.76",
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.158",
|
|
32
32
|
"@bli-cockpit/telemetry-core": "0.1.83"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|