@bli-cockpit/cli 0.2.111 → 0.2.113
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/adapters/codex-attribution.js +5 -143
- package/dist/adapters/codex-session-signals.js +161 -0
- package/dist/agent-rules-unmanaged-block.js +97 -0
- package/dist/agent-rules.js +1 -82
- package/dist/commands/agent-session-report.js +3 -0
- package/dist/commands/local-args-tower-admin.js +8 -151
- package/dist/commands/local-args-tower-settings.js +186 -0
- package/dist/commands/local-args-tower-usage.js +6 -6
- package/dist/commands/local-help-commands-tower.js +2 -2
- package/dist/commands/local-help.js +2 -2
- package/dist/commands/onboard-report-blockers.js +58 -0
- package/dist/commands/onboard-report.js +2 -57
- package/dist/commands/ops-render-ladder.js +25 -0
- package/dist/commands/ops-render.js +7 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync.js +10 -0
- package/dist/commands/settings-overview.js +94 -0
- package/dist/commands/settings.js +2 -82
- package/dist/commands/usage.js +12 -6
- package/dist/onboarding-root-guidance.js +60 -0
- package/dist/onboarding-roots.js +6 -59
- package/package.json +4 -4
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The commands that manage this machine's account or watch the pipeline
|
|
3
3
|
* rather than carry on a conversation: `cockpit scout`, `cockpit ops`,
|
|
4
4
|
* `cockpit slack`, `cockpit settings`, `cockpit team`, and `cockpit model`.
|
|
5
|
-
* Split out of local-args-tower.ts (BLI-3636)
|
|
5
|
+
* Split out of local-args-tower.ts (BLI-3636); the `settings` sections moved
|
|
6
|
+
* once more, into `./local-args-tower-settings.js`, and are re-exported below.
|
|
6
7
|
*
|
|
7
8
|
* Every parser moved verbatim: same flags, same defaults, same refusal
|
|
8
9
|
* sentences. The two constants this family publishes
|
|
@@ -11,6 +12,12 @@
|
|
|
11
12
|
*/
|
|
12
13
|
import { optionalEmail, optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
13
14
|
import { isTeamDeviceRevokeReasonLabel, TEAM_DEVICE_REVOKE_REASON_LABELS, } from "./team-device-reasons.js";
|
|
15
|
+
/**
|
|
16
|
+
* `cockpit settings` has its own file: five sections, one set of rules each.
|
|
17
|
+
* It is re-exported here because this is the address every caller imports it
|
|
18
|
+
* from.
|
|
19
|
+
*/
|
|
20
|
+
export { parseSettingsArgs } from "./local-args-tower-settings.js";
|
|
14
21
|
/** The shortest prefix `cockpit scout start` will resolve. Below this, ids collide. */
|
|
15
22
|
export const SCOUT_MIN_PREFIX_LENGTH = 6;
|
|
16
23
|
/**
|
|
@@ -220,156 +227,6 @@ export function parseSlackArgs(args) {
|
|
|
220
227
|
...base,
|
|
221
228
|
};
|
|
222
229
|
}
|
|
223
|
-
const SETTINGS_SECTIONS = ["personal", "switches", "models", "env", "cli-floor"];
|
|
224
|
-
/**
|
|
225
|
-
* `cockpit settings [section] [verb] [args]` (BLI-3461).
|
|
226
|
-
*
|
|
227
|
-
* The flag set is the UNION across sections and the combinations are checked
|
|
228
|
-
* afterwards, per section — the same shape `parseDoctorArgs` uses. A flag that
|
|
229
|
-
* belongs to another section is refused by name rather than silently ignored,
|
|
230
|
-
* because a dropped `--project` on an env write is a change a person believes
|
|
231
|
-
* they made.
|
|
232
|
-
*/
|
|
233
|
-
export function parseSettingsArgs(args) {
|
|
234
|
-
const values = parseNamedArgs(args, {
|
|
235
|
-
allowedFlags: [
|
|
236
|
-
"--home",
|
|
237
|
-
"--dashboard-url",
|
|
238
|
-
"--json",
|
|
239
|
-
"--chat-model",
|
|
240
|
-
"--brief-model",
|
|
241
|
-
"--chat",
|
|
242
|
-
"--memory",
|
|
243
|
-
"--project",
|
|
244
|
-
"--file",
|
|
245
|
-
"--id",
|
|
246
|
-
"--content-stdin",
|
|
247
|
-
"--yes",
|
|
248
|
-
],
|
|
249
|
-
valueFlags: [
|
|
250
|
-
"--home",
|
|
251
|
-
"--dashboard-url",
|
|
252
|
-
"--chat-model",
|
|
253
|
-
"--brief-model",
|
|
254
|
-
"--chat",
|
|
255
|
-
"--memory",
|
|
256
|
-
"--project",
|
|
257
|
-
"--file",
|
|
258
|
-
"--id",
|
|
259
|
-
],
|
|
260
|
-
});
|
|
261
|
-
const [rawSection, ...rest] = values.positionals;
|
|
262
|
-
const common = {
|
|
263
|
-
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
264
|
-
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
265
|
-
yes: values.booleans.has("--yes"),
|
|
266
|
-
json: values.booleans.has("--json"),
|
|
267
|
-
};
|
|
268
|
-
if (!rawSection) {
|
|
269
|
-
if (rest.length > 0)
|
|
270
|
-
throw new Error("settings does not accept that argument.");
|
|
271
|
-
return { kind: "settings", section: "overview", action: "show", ...common };
|
|
272
|
-
}
|
|
273
|
-
if (!SETTINGS_SECTIONS.includes(rawSection)) {
|
|
274
|
-
throw new Error(`settings section must be one of ${SETTINGS_SECTIONS.join(", ")} — or nothing, for all of them.`);
|
|
275
|
-
}
|
|
276
|
-
const section = rawSection;
|
|
277
|
-
if (section === "personal") {
|
|
278
|
-
if (rest.length > 0)
|
|
279
|
-
throw new Error("settings personal does not accept positional arguments.");
|
|
280
|
-
const chatModel = optionalNonEmpty(values.flags.get("--chat-model"));
|
|
281
|
-
const briefModel = optionalNonEmpty(values.flags.get("--brief-model"));
|
|
282
|
-
return {
|
|
283
|
-
kind: "settings",
|
|
284
|
-
section,
|
|
285
|
-
action: chatModel || briefModel ? "set" : "show",
|
|
286
|
-
chatModel,
|
|
287
|
-
briefModel,
|
|
288
|
-
...common,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
if (section === "switches") {
|
|
292
|
-
if (rest.length === 0) {
|
|
293
|
-
return { kind: "settings", section, action: "show", ...common };
|
|
294
|
-
}
|
|
295
|
-
if (rest[0] !== "set") {
|
|
296
|
-
throw new Error("settings switches takes no verb, or `set <key> <value>`.");
|
|
297
|
-
}
|
|
298
|
-
const [, key, value, ...extra] = rest;
|
|
299
|
-
if (!key || !value || extra.length > 0) {
|
|
300
|
-
throw new Error("settings switches set needs exactly a key and a value.");
|
|
301
|
-
}
|
|
302
|
-
return { kind: "settings", section, action: "set", switchKey: key, switchValue: value, ...common };
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* BLI-3557: `settings cli-floor` shows the fleet forced-update floor,
|
|
306
|
-
* `settings cli-floor <version>` raises it. There is no `set` verb because
|
|
307
|
-
* there is nothing else to do to a floor, and no flag for the version because
|
|
308
|
-
* a bare positional is how a release says it: `cockpit settings cli-floor
|
|
309
|
-
* 0.2.48`. The server refuses a LOWER version; the terminal does not
|
|
310
|
-
* second-guess that decision locally.
|
|
311
|
-
*/
|
|
312
|
-
if (section === "cli-floor") {
|
|
313
|
-
if (rest.length === 0) {
|
|
314
|
-
return { kind: "settings", section, action: "show", ...common };
|
|
315
|
-
}
|
|
316
|
-
const [version, ...extra] = rest;
|
|
317
|
-
if (!version || extra.length > 0) {
|
|
318
|
-
throw new Error("settings cli-floor takes no argument, or one version: `cli-floor 0.2.48`.");
|
|
319
|
-
}
|
|
320
|
-
return { kind: "settings", section, action: "set", floorVersion: version, ...common };
|
|
321
|
-
}
|
|
322
|
-
if (section === "models") {
|
|
323
|
-
if (rest.length === 0) {
|
|
324
|
-
return { kind: "settings", section, action: "show", ...common };
|
|
325
|
-
}
|
|
326
|
-
if (rest[0] !== "set" || rest.length > 1) {
|
|
327
|
-
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>`.");
|
|
328
|
-
}
|
|
329
|
-
const orgChatModel = optionalNonEmpty(values.flags.get("--chat"));
|
|
330
|
-
const orgMemoryModel = optionalNonEmpty(values.flags.get("--memory"));
|
|
331
|
-
if (!orgChatModel && !orgMemoryModel) {
|
|
332
|
-
throw new Error("settings models set needs --chat <key>, --memory <id>, or both.");
|
|
333
|
-
}
|
|
334
|
-
return { kind: "settings", section, action: "set", orgChatModel, orgMemoryModel, ...common };
|
|
335
|
-
}
|
|
336
|
-
// env
|
|
337
|
-
const verb = rest[0] ?? "list";
|
|
338
|
-
if (rest.length > 1)
|
|
339
|
-
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
340
|
-
if (verb === "list") {
|
|
341
|
-
return { kind: "settings", section, action: "list", ...common };
|
|
342
|
-
}
|
|
343
|
-
if (verb === "set") {
|
|
344
|
-
const envProject = optionalNonEmpty(values.flags.get("--project"));
|
|
345
|
-
const envFile = optionalNonEmpty(values.flags.get("--file"));
|
|
346
|
-
if (!envProject || !envFile) {
|
|
347
|
-
throw new Error("settings env set needs --project <project> and --file <file name>.");
|
|
348
|
-
}
|
|
349
|
-
if (!values.booleans.has("--content-stdin")) {
|
|
350
|
-
// Deliberate: there is no `--content <value>` flag and never will be. A
|
|
351
|
-
// secret on a command line lands in shell history and in every process
|
|
352
|
-
// listing on the machine.
|
|
353
|
-
throw new Error("settings env set reads the file contents from stdin: add --content-stdin and pipe the file in.");
|
|
354
|
-
}
|
|
355
|
-
return {
|
|
356
|
-
kind: "settings",
|
|
357
|
-
section,
|
|
358
|
-
action: "set",
|
|
359
|
-
envProject,
|
|
360
|
-
envFile,
|
|
361
|
-
contentStdin: true,
|
|
362
|
-
...common,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
if (verb === "delete") {
|
|
366
|
-
const envId = optionalNonEmpty(values.flags.get("--id"));
|
|
367
|
-
if (!envId)
|
|
368
|
-
throw new Error("settings env delete needs --id <uuid>.");
|
|
369
|
-
return { kind: "settings", section, action: "delete", envId, ...common };
|
|
370
|
-
}
|
|
371
|
-
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
372
|
-
}
|
|
373
230
|
/**
|
|
374
231
|
* `cockpit team [members|invite <email>|role <userId>|device list|device
|
|
375
232
|
* revoke <id|name>]` (BLI-3461; `device` added BLI-3559).
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person may type at `cockpit settings` (BLI-3461), split out of
|
|
3
|
+
* local-args-tower-admin.ts by section so each section's rules fit on one
|
|
4
|
+
* screen. Every flag, default and refusal sentence moved verbatim;
|
|
5
|
+
* `parseSettingsArgs` is still exported from `./local-args-tower-admin.js`,
|
|
6
|
+
* the address it has always had.
|
|
7
|
+
*
|
|
8
|
+
* The flag set is the UNION across sections and the combinations are checked
|
|
9
|
+
* afterwards, per section — the same shape `parseDoctorArgs` uses. A flag that
|
|
10
|
+
* belongs to another section is refused by name rather than silently ignored,
|
|
11
|
+
* because a dropped `--project` on an env write is a change a person believes
|
|
12
|
+
* they made.
|
|
13
|
+
*/
|
|
14
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
15
|
+
const SETTINGS_SECTIONS = ["personal", "switches", "models", "env", "cli-floor"];
|
|
16
|
+
/** `cockpit settings [section] [verb] [args]`: the section picker. */
|
|
17
|
+
export function parseSettingsArgs(args) {
|
|
18
|
+
const values = parseNamedArgs(args, {
|
|
19
|
+
allowedFlags: [
|
|
20
|
+
"--home",
|
|
21
|
+
"--dashboard-url",
|
|
22
|
+
"--json",
|
|
23
|
+
"--chat-model",
|
|
24
|
+
"--brief-model",
|
|
25
|
+
"--chat",
|
|
26
|
+
"--memory",
|
|
27
|
+
"--project",
|
|
28
|
+
"--file",
|
|
29
|
+
"--id",
|
|
30
|
+
"--content-stdin",
|
|
31
|
+
"--yes",
|
|
32
|
+
],
|
|
33
|
+
valueFlags: [
|
|
34
|
+
"--home",
|
|
35
|
+
"--dashboard-url",
|
|
36
|
+
"--chat-model",
|
|
37
|
+
"--brief-model",
|
|
38
|
+
"--chat",
|
|
39
|
+
"--memory",
|
|
40
|
+
"--project",
|
|
41
|
+
"--file",
|
|
42
|
+
"--id",
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
const [rawSection, ...rest] = values.positionals;
|
|
46
|
+
const common = {
|
|
47
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
48
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
49
|
+
yes: values.booleans.has("--yes"),
|
|
50
|
+
json: values.booleans.has("--json"),
|
|
51
|
+
};
|
|
52
|
+
if (!rawSection) {
|
|
53
|
+
if (rest.length > 0)
|
|
54
|
+
throw new Error("settings does not accept that argument.");
|
|
55
|
+
return { kind: "settings", section: "overview", action: "show", ...common };
|
|
56
|
+
}
|
|
57
|
+
if (!SETTINGS_SECTIONS.includes(rawSection)) {
|
|
58
|
+
throw new Error(`settings section must be one of ${SETTINGS_SECTIONS.join(", ")} — or nothing, for all of them.`);
|
|
59
|
+
}
|
|
60
|
+
switch (rawSection) {
|
|
61
|
+
case "personal":
|
|
62
|
+
return parsePersonalSection(rest, values.flags, common);
|
|
63
|
+
case "switches":
|
|
64
|
+
return parseSwitchesSection(rest, common);
|
|
65
|
+
case "cli-floor":
|
|
66
|
+
return parseCliFloorSection(rest, common);
|
|
67
|
+
case "models":
|
|
68
|
+
return parseModelsSection(rest, values.flags, common);
|
|
69
|
+
case "env":
|
|
70
|
+
return parseEnvSection(rest, values.flags, values.booleans, common);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** `settings personal [--chat-model <key>] [--brief-model <key>]`. */
|
|
74
|
+
function parsePersonalSection(rest, flags, common) {
|
|
75
|
+
if (rest.length > 0)
|
|
76
|
+
throw new Error("settings personal does not accept positional arguments.");
|
|
77
|
+
const chatModel = optionalNonEmpty(flags.get("--chat-model"));
|
|
78
|
+
const briefModel = optionalNonEmpty(flags.get("--brief-model"));
|
|
79
|
+
return {
|
|
80
|
+
kind: "settings",
|
|
81
|
+
section: "personal",
|
|
82
|
+
action: chatModel || briefModel ? "set" : "show",
|
|
83
|
+
chatModel,
|
|
84
|
+
briefModel,
|
|
85
|
+
...common,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** `settings switches` or `settings switches set <key> <value>`. */
|
|
89
|
+
function parseSwitchesSection(rest, common) {
|
|
90
|
+
if (rest.length === 0) {
|
|
91
|
+
return { kind: "settings", section: "switches", action: "show", ...common };
|
|
92
|
+
}
|
|
93
|
+
if (rest[0] !== "set") {
|
|
94
|
+
throw new Error("settings switches takes no verb, or `set <key> <value>`.");
|
|
95
|
+
}
|
|
96
|
+
const [, key, value, ...extra] = rest;
|
|
97
|
+
if (!key || !value || extra.length > 0) {
|
|
98
|
+
throw new Error("settings switches set needs exactly a key and a value.");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
kind: "settings",
|
|
102
|
+
section: "switches",
|
|
103
|
+
action: "set",
|
|
104
|
+
switchKey: key,
|
|
105
|
+
switchValue: value,
|
|
106
|
+
...common,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* BLI-3557: `settings cli-floor` shows the fleet forced-update floor,
|
|
111
|
+
* `settings cli-floor <version>` raises it. There is no `set` verb because
|
|
112
|
+
* there is nothing else to do to a floor, and no flag for the version because
|
|
113
|
+
* a bare positional is how a release says it: `cockpit settings cli-floor
|
|
114
|
+
* 0.2.48`. The server refuses a LOWER version; the terminal does not
|
|
115
|
+
* second-guess that decision locally.
|
|
116
|
+
*/
|
|
117
|
+
function parseCliFloorSection(rest, common) {
|
|
118
|
+
if (rest.length === 0) {
|
|
119
|
+
return { kind: "settings", section: "cli-floor", action: "show", ...common };
|
|
120
|
+
}
|
|
121
|
+
const [version, ...extra] = rest;
|
|
122
|
+
if (!version || extra.length > 0) {
|
|
123
|
+
throw new Error("settings cli-floor takes no argument, or one version: `cli-floor 0.2.48`.");
|
|
124
|
+
}
|
|
125
|
+
return { kind: "settings", section: "cli-floor", action: "set", floorVersion: version, ...common };
|
|
126
|
+
}
|
|
127
|
+
/** `settings models` or `settings models set --chat <key> / --memory <id>`. */
|
|
128
|
+
function parseModelsSection(rest, flags, common) {
|
|
129
|
+
if (rest.length === 0) {
|
|
130
|
+
return { kind: "settings", section: "models", action: "show", ...common };
|
|
131
|
+
}
|
|
132
|
+
if (rest[0] !== "set" || rest.length > 1) {
|
|
133
|
+
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>`.");
|
|
134
|
+
}
|
|
135
|
+
const orgChatModel = optionalNonEmpty(flags.get("--chat"));
|
|
136
|
+
const orgMemoryModel = optionalNonEmpty(flags.get("--memory"));
|
|
137
|
+
if (!orgChatModel && !orgMemoryModel) {
|
|
138
|
+
throw new Error("settings models set needs --chat <key>, --memory <id>, or both.");
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind: "settings",
|
|
142
|
+
section: "models",
|
|
143
|
+
action: "set",
|
|
144
|
+
orgChatModel,
|
|
145
|
+
orgMemoryModel,
|
|
146
|
+
...common,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** `settings env [list|set --project <p> --file <f> --content-stdin|delete --id <uuid>]`. */
|
|
150
|
+
function parseEnvSection(rest, flags, booleans, common) {
|
|
151
|
+
const verb = rest[0] ?? "list";
|
|
152
|
+
if (rest.length > 1)
|
|
153
|
+
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
154
|
+
if (verb === "list") {
|
|
155
|
+
return { kind: "settings", section: "env", action: "list", ...common };
|
|
156
|
+
}
|
|
157
|
+
if (verb === "set") {
|
|
158
|
+
const envProject = optionalNonEmpty(flags.get("--project"));
|
|
159
|
+
const envFile = optionalNonEmpty(flags.get("--file"));
|
|
160
|
+
if (!envProject || !envFile) {
|
|
161
|
+
throw new Error("settings env set needs --project <project> and --file <file name>.");
|
|
162
|
+
}
|
|
163
|
+
if (!booleans.has("--content-stdin")) {
|
|
164
|
+
// Deliberate: there is no `--content <value>` flag and never will be. A
|
|
165
|
+
// secret on a command line lands in shell history and in every process
|
|
166
|
+
// listing on the machine.
|
|
167
|
+
throw new Error("settings env set reads the file contents from stdin: add --content-stdin and pipe the file in.");
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
kind: "settings",
|
|
171
|
+
section: "env",
|
|
172
|
+
action: "set",
|
|
173
|
+
envProject,
|
|
174
|
+
envFile,
|
|
175
|
+
contentStdin: true,
|
|
176
|
+
...common,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (verb === "delete") {
|
|
180
|
+
const envId = optionalNonEmpty(flags.get("--id"));
|
|
181
|
+
if (!envId)
|
|
182
|
+
throw new Error("settings env delete needs --id <uuid>.");
|
|
183
|
+
return { kind: "settings", section: "env", action: "delete", envId, ...common };
|
|
184
|
+
}
|
|
185
|
+
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
186
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
2
2
|
export function parseUsageArgs(args) {
|
|
3
|
-
const values = parseNamedArgs(args, { allowedFlags: ["--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
|
-
if (["--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
|
|
5
|
-
throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject.");
|
|
3
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--by-task", "--task", "--by-subject", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--by-topic", "--by-repo", "--person", "--all", "--since", "--until", "--include-automated", "--detail", "--home", "--dashboard-url", "--json"], valueFlags: ["--task", "--subject", "--repo", "--session", "--from-window", "--min-confidence", "--person", "--since", "--until", "--home", "--dashboard-url"] });
|
|
4
|
+
if (["--by-task", "--by-repo", "--by-topic", "--by-subject"].filter(flag => values.booleans.has(flag)).length > 1)
|
|
5
|
+
throw new Error("--by-repo and --by-topic cannot be used together or with --by-subject or --by-task.");
|
|
6
6
|
const minConfidence = values.flags.get("--min-confidence");
|
|
7
7
|
if (minConfidence !== undefined && !["high", "medium", "low"].includes(minConfidence))
|
|
8
8
|
throw new Error("--min-confidence accepts high, medium or low.");
|
|
9
9
|
const action = values.positionals[0] ?? "people";
|
|
10
10
|
if (!["people", "sessions"].includes(action) || values.positionals.length > 1)
|
|
11
11
|
throw new Error("usage takes people or sessions.");
|
|
12
|
-
if (action === "sessions" && !values.flags.get("--subject") && !values.flags.get("--session"))
|
|
13
|
-
throw new Error("usage sessions requires --subject or --session.");
|
|
14
|
-
return { bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
12
|
+
if (action === "sessions" && !values.flags.get("--subject") && !values.flags.get("--session") && !values.flags.get("--task"))
|
|
13
|
+
throw new Error("usage sessions requires --task, --subject or --session.");
|
|
14
|
+
return { byTask: values.booleans.has("--by-task"), task: optionalNonEmpty(values.flags.get("--task")), bySubject: values.booleans.has("--by-subject"), subject: optionalNonEmpty(values.flags.get("--subject")), repo: optionalNonEmpty(values.flags.get("--repo")), session: optionalNonEmpty(values.flags.get("--session")), fromWindow: optionalNonEmpty(values.flags.get("--from-window")), kind: "usage", ...(minConfidence ? { minConfidence: minConfidence } : {}), byTopic: values.booleans.has("--by-topic"), byRepo: values.booleans.has("--by-repo"), person: optionalNonEmpty(values.flags.get("--person")), all: values.booleans.has("--all"), action: action, since: optionalNonEmpty(values.flags.get("--since")) ?? "30d", until: optionalNonEmpty(values.flags.get("--until")), detail: values.booleans.has("--detail"), includeAutomated: values.booleans.has("--include-automated"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), json: values.booleans.has("--json") };
|
|
15
15
|
}
|
|
@@ -143,8 +143,8 @@ export const TOWER_COMMAND_HELP = [
|
|
|
143
143
|
[
|
|
144
144
|
"usage",
|
|
145
145
|
[
|
|
146
|
-
"Usage: cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
147
|
-
" cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
|
|
146
|
+
"Usage: cockpit usage people [--by-repo | --by-topic | --by-subject | --by-task] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--since <n>d|<n>h|<iso>] [--detail] [--until <iso>] [--include-automated] [--json]",
|
|
147
|
+
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
148
148
|
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
149
149
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
150
150
|
"",
|
|
@@ -104,10 +104,10 @@ export function localCommandHelp(command) {
|
|
|
104
104
|
` cockpit search "<words>" [--kind ${SEARCH_KINDS.join(",")}] [--limit <n>] [--dashboard-url <url>] [--json]`,
|
|
105
105
|
" cockpit release [--dry-run] [--skip-checks] [--no-floor] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
106
106
|
" cockpit careers [list|show <id>|rescreen <id>] [--role <slug>] [--min-score <n>] [--since <date>] [--json]",
|
|
107
|
-
" cockpit usage sessions --subject <name> [--repo <label>] [--since <window>] [--json]",
|
|
107
|
+
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
108
108
|
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
109
109
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
|
110
|
-
" cockpit usage people [--by-repo | --by-topic | --by-subject] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--detail] [--since <n>d|<n>h|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
|
|
110
|
+
" cockpit usage people [--by-repo | --by-topic | --by-subject | --by-task] [--min-confidence high|medium|low] [--person <email|me>] [--all] [--detail] [--since <n>d|<n>h|<iso>] [--until <iso>] [--include-automated] [--dashboard-url <url>] [--json]",
|
|
111
111
|
"",
|
|
112
112
|
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
113
113
|
].join("\n");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { writeLine } from "./cli-io.js";
|
|
2
|
+
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, } from "../onboarding-roots.js";
|
|
3
|
+
export function writeOnboardAutostartBlocker(io, result) {
|
|
4
|
+
writeLine(io.stderr, "BLOCKED: initial collection succeeded, but recurring background collection is not running.");
|
|
5
|
+
if (result?.message)
|
|
6
|
+
writeLine(io.stderr, `Failure: ${result.message}`);
|
|
7
|
+
writeLine(io.stderr, "Next: run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The blocked-backfill result, identical in both onboarding arms. `extra`
|
|
11
|
+
* carries the arm-specific keys and is spread where those keys appeared before,
|
|
12
|
+
* so the `--json` key order is unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export function writeOnboardBackfillBlockedResult(io, options) {
|
|
15
|
+
if (!options.json) {
|
|
16
|
+
writeOnboardBackfillBlocker(io, options.backfill);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
writeLine(io.stdout, JSON.stringify({
|
|
20
|
+
...options.base,
|
|
21
|
+
...options.extra,
|
|
22
|
+
backfill: onboardBackfillPayload(options.backfill),
|
|
23
|
+
blocker: "backfill_incomplete",
|
|
24
|
+
backfill_failure_reason: options.backfill.failureReason,
|
|
25
|
+
next_step: options.backfill.retryCommand,
|
|
26
|
+
}, null, 2));
|
|
27
|
+
}
|
|
28
|
+
function writeOnboardBackfillBlocker(io, outcome) {
|
|
29
|
+
writeLine(io.stderr, "BLOCKED: all-history Codex and Claude backfill is incomplete for the approved collection roots.");
|
|
30
|
+
writeLine(io.stderr, `Failure: ${outcome.failureReason ?? `backfill_${outcome.status}`}`);
|
|
31
|
+
writeLine(io.stderr, `Retry: ${outcome.retryCommand}`);
|
|
32
|
+
}
|
|
33
|
+
function onboardBackfillPayload(outcome) {
|
|
34
|
+
return (outcome.result ?? {
|
|
35
|
+
status: "blocked",
|
|
36
|
+
failure_reason: outcome.failureReason,
|
|
37
|
+
retry_command: outcome.retryCommand,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function nextStepForOnboardBlocker(blocker, options = {}) {
|
|
41
|
+
switch (blocker) {
|
|
42
|
+
case COLLECTION_ROOT_REQUIRED:
|
|
43
|
+
return missingCollectionRootMessage(options);
|
|
44
|
+
case "ticket":
|
|
45
|
+
case "ticket_binding":
|
|
46
|
+
return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
|
|
47
|
+
case "device_pairing":
|
|
48
|
+
return "Ask Edward to approve this machine in the dashboard under Ambient -> Collector approvals, then run `cockpit doctor` again.";
|
|
49
|
+
case "network_or_ingest":
|
|
50
|
+
return "Check dashboard URL/network, then run `cockpit doctor`.";
|
|
51
|
+
case "install":
|
|
52
|
+
return "Rerun `cockpit doctor` from the repo root; it will reinstall local config.";
|
|
53
|
+
case "work_context":
|
|
54
|
+
return "Run `cockpit start --ticket <id> --workspace \"$PWD\"`, then retry `cockpit sync`.";
|
|
55
|
+
default:
|
|
56
|
+
return "Run `cockpit status --json` and report the blocker label plus last failure reason.";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeLine } from "./cli-io.js";
|
|
2
2
|
import { attributedSyncRunStatus, displayTicketId, rawEvidenceSyncLine, writeAgentSessionSummary, } from "./collection-report.js";
|
|
3
|
-
import {
|
|
3
|
+
import { writeOnboardAutostartBlocker } from "./onboard-report-blockers.js";
|
|
4
|
+
export { nextStepForOnboardBlocker, writeOnboardAutostartBlocker, writeOnboardBackfillBlockedResult, } from "./onboard-report-blockers.js";
|
|
4
5
|
export function writeOnboardBanner(command, io) {
|
|
5
6
|
writeLine(io.stdout, "Setting up Tower");
|
|
6
7
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
@@ -54,43 +55,6 @@ export function onboardAgentRulesInstallLine(result) {
|
|
|
54
55
|
return "not installed.";
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
export function writeOnboardAutostartBlocker(io, result) {
|
|
58
|
-
writeLine(io.stderr, "BLOCKED: initial collection succeeded, but recurring background collection is not running.");
|
|
59
|
-
if (result?.message)
|
|
60
|
-
writeLine(io.stderr, `Failure: ${result.message}`);
|
|
61
|
-
writeLine(io.stderr, "Next: run `cockpit autostart install`, then require `cockpit autostart status --json` to exit 0.");
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* The blocked-backfill result, identical in both onboarding arms. `extra`
|
|
65
|
-
* carries the arm-specific keys and is spread where those keys appeared before,
|
|
66
|
-
* so the `--json` key order is unchanged.
|
|
67
|
-
*/
|
|
68
|
-
export function writeOnboardBackfillBlockedResult(io, options) {
|
|
69
|
-
if (!options.json) {
|
|
70
|
-
writeOnboardBackfillBlocker(io, options.backfill);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
writeLine(io.stdout, JSON.stringify({
|
|
74
|
-
...options.base,
|
|
75
|
-
...options.extra,
|
|
76
|
-
backfill: onboardBackfillPayload(options.backfill),
|
|
77
|
-
blocker: "backfill_incomplete",
|
|
78
|
-
backfill_failure_reason: options.backfill.failureReason,
|
|
79
|
-
next_step: options.backfill.retryCommand,
|
|
80
|
-
}, null, 2));
|
|
81
|
-
}
|
|
82
|
-
function writeOnboardBackfillBlocker(io, outcome) {
|
|
83
|
-
writeLine(io.stderr, "BLOCKED: all-history Codex and Claude backfill is incomplete for the approved collection roots.");
|
|
84
|
-
writeLine(io.stderr, `Failure: ${outcome.failureReason ?? `backfill_${outcome.status}`}`);
|
|
85
|
-
writeLine(io.stderr, `Retry: ${outcome.retryCommand}`);
|
|
86
|
-
}
|
|
87
|
-
function onboardBackfillPayload(outcome) {
|
|
88
|
-
return (outcome.result ?? {
|
|
89
|
-
status: "blocked",
|
|
90
|
-
failure_reason: outcome.failureReason,
|
|
91
|
-
retry_command: outcome.retryCommand,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
58
|
/**
|
|
95
59
|
* Every onboard `--json` payload is `onboardResult` plus the roots this pass
|
|
96
60
|
* resolved plus whatever extra keys that arm of onboarding adds. Assembling it
|
|
@@ -129,25 +93,6 @@ function onboardResult(resultStatus, command, install, pair, sync, status) {
|
|
|
129
93
|
next_dashboard_path: `${command.dashboardUrl}/my-work`,
|
|
130
94
|
};
|
|
131
95
|
}
|
|
132
|
-
export function nextStepForOnboardBlocker(blocker, options = {}) {
|
|
133
|
-
switch (blocker) {
|
|
134
|
-
case COLLECTION_ROOT_REQUIRED:
|
|
135
|
-
return missingCollectionRootMessage(options);
|
|
136
|
-
case "ticket":
|
|
137
|
-
case "ticket_binding":
|
|
138
|
-
return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
|
|
139
|
-
case "device_pairing":
|
|
140
|
-
return "Ask Edward to approve this machine in the dashboard under Ambient -> Collector approvals, then run `cockpit doctor` again.";
|
|
141
|
-
case "network_or_ingest":
|
|
142
|
-
return "Check dashboard URL/network, then run `cockpit doctor`.";
|
|
143
|
-
case "install":
|
|
144
|
-
return "Rerun `cockpit doctor` from the repo root; it will reinstall local config.";
|
|
145
|
-
case "work_context":
|
|
146
|
-
return "Run `cockpit start --ticket <id> --workspace \"$PWD\"`, then retry `cockpit sync`.";
|
|
147
|
-
default:
|
|
148
|
-
return "Run `cockpit status --json` and report the blocker label plus last failure reason.";
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
96
|
/** Step 3 of a single-repo onboard: the work context this machine just bound. */
|
|
152
97
|
export function writeOnboardWorkContextStarted(io, context) {
|
|
153
98
|
writeLine(io.stdout, "3/5 Work context active.");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LADDER block: the queue every session joins at ingest, and what the
|
|
3
|
+
* Railway drainer has done with it. Failures are grouped by the reason the
|
|
4
|
+
* rung named itself with, because "12 failed" sends nobody anywhere.
|
|
5
|
+
*/
|
|
6
|
+
export function renderLadder(ladder, dim) {
|
|
7
|
+
const lines = ["", `LADDER ${ladder.summary ?? "(no summary)"}`];
|
|
8
|
+
if (ladder.readError) {
|
|
9
|
+
lines.push(` queue not read (${ladder.readError})`);
|
|
10
|
+
return lines;
|
|
11
|
+
}
|
|
12
|
+
const reasons = Object.entries(ladder.failed_by_reason ?? {}).sort((left, right) => right[1] - left[1]);
|
|
13
|
+
for (const [reason, count] of reasons.slice(0, 8)) {
|
|
14
|
+
lines.push(dim(` ${String(count).padStart(5)} ${reason}`));
|
|
15
|
+
}
|
|
16
|
+
if (reasons.length > 8)
|
|
17
|
+
lines.push(dim(` ${reasons.length - 8} more reasons not shown`));
|
|
18
|
+
if ((ladder.stale_leases ?? 0) > 0) {
|
|
19
|
+
lines.push(dim(` ${ladder.stale_leases} stale lease(s) waiting to be taken over`));
|
|
20
|
+
}
|
|
21
|
+
if (ladder.oldest_pending_session_id) {
|
|
22
|
+
lines.push(dim(` oldest pending session ${ladder.oldest_pending_session_id}`));
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
@@ -16,7 +16,12 @@ import { renderCoverageBuckets, } from "./ops-render-coverage.js";
|
|
|
16
16
|
// BLI-3909: today's spend by model, its own renderer for the same reason the
|
|
17
17
|
// coverage table has one — this file is at the repo's readability floor.
|
|
18
18
|
import { renderSpend } from "./ops-render-spend.js";
|
|
19
|
+
// BLI-4341: the ladder queue block, its own renderer for the same reason the
|
|
20
|
+
// coverage table and the spend section have one: this file is at the repo's
|
|
21
|
+
// readability floor.
|
|
22
|
+
import { renderLadder } from "./ops-render-ladder.js";
|
|
19
23
|
export { renderSpend } from "./ops-render-spend.js";
|
|
24
|
+
export { renderLadder } from "./ops-render-ladder.js";
|
|
20
25
|
export { renderMemoryHooks, renderMemoryUsage } from "./ops-render-memory.js";
|
|
21
26
|
export { renderCoverageBuckets };
|
|
22
27
|
/** The word a person reads. Short, fixed width, and never a bare colour. */
|
|
@@ -134,6 +139,8 @@ options = {}) {
|
|
|
134
139
|
}
|
|
135
140
|
if (payload.spend)
|
|
136
141
|
lines.push(...renderSpend(payload.spend, dim));
|
|
142
|
+
if (payload.ladder)
|
|
143
|
+
lines.push(...renderLadder(payload.ladder, dim));
|
|
137
144
|
const slack = payload.skips?.slack;
|
|
138
145
|
const external = payload.skips?.external;
|
|
139
146
|
if (slack || external) {
|
|
@@ -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.113");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readLocalWorkContextForRepo } from "../local-state.js";
|
|
1
2
|
/**
|
|
2
3
|
* One attributed sync pass: scan both session stores, attribute what they hold,
|
|
3
4
|
* upload it through every target worktree, write down what happened, and judge
|
|
@@ -91,7 +92,16 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
91
92
|
plan,
|
|
92
93
|
scan,
|
|
93
94
|
});
|
|
95
|
+
const ticketByWorktree = new Map();
|
|
96
|
+
for (const worktree of options.worktrees) {
|
|
97
|
+
const context = await readLocalWorkContextForRepo(paths, worktree.repo_root).catch(() => null);
|
|
98
|
+
const ticket = context?.linear_ticket_id ?? context?.active_ticket_id;
|
|
99
|
+
if (ticket)
|
|
100
|
+
ticketByWorktree.set(worktree.worktree_fingerprint, ticket);
|
|
101
|
+
}
|
|
102
|
+
console.error("[session-sync] bindings read", JSON.stringify({ bound_worktrees: ticketByWorktree.size, reason: "active_work_context" }));
|
|
94
103
|
const sessions = buildAgentSessionReport({
|
|
104
|
+
ticketByWorktree,
|
|
95
105
|
codexResults: scan.codexAttribution.results,
|
|
96
106
|
claudeResults: scan.claudeAttribution.results,
|
|
97
107
|
outcomes: worktreePass.outcomes,
|