@bli-cockpit/cli 0.2.179 → 0.2.180
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/brain.js +35 -0
- package/dist/commands/local-args-tower-brain.js +38 -0
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +3 -1
- package/dist/commands/local-command-shapes-brain.js +5 -0
- package/dist/commands/local-help-commands-tower-ops.js +17 -0
- package/dist/commands/local-help.js +2 -0
- package/dist/commands/local.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/local-state-status.js +5 -1
- package/dist/raw-evidence-stuck-summary.js +21 -1
- package/package.json +4 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[brain cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 55_000;
|
|
5
|
+
export async function runBrain(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("brain", command, io);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
12
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
13
|
+
if (command.json)
|
|
14
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
15
|
+
else
|
|
16
|
+
writeLine(io.stderr, detail);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const query = new URLSearchParams();
|
|
20
|
+
if (command.project)
|
|
21
|
+
query.set("project", command.project);
|
|
22
|
+
if (command.show)
|
|
23
|
+
query.set("status", command.show);
|
|
24
|
+
const asked = query.toString();
|
|
25
|
+
const path = asked ? `/api/brain/incidents?${asked}` : "/api/brain/incidents";
|
|
26
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "brain status", timeoutMs: READ_DEADLINE_MS });
|
|
27
|
+
if (!answer.ok)
|
|
28
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
29
|
+
const body = answer.body;
|
|
30
|
+
writeLine(io.stderr, `${TAG} status read ${JSON.stringify({ projects: body.projects.length, rows: body.coverage.rows, pages: body.coverage.pages, end: body.coverage.end })}`);
|
|
31
|
+
if (command.json)
|
|
32
|
+
return emitAgentDoor(door, body);
|
|
33
|
+
writeLine(io.stdout, body.text);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit brain` argument parsing (BLI-12859).
|
|
3
|
+
*
|
|
4
|
+
* cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--json]
|
|
5
|
+
*
|
|
6
|
+
* Tower's incident queue: per project, how many incidents are open, being worked, waiting on a
|
|
7
|
+
* person, fixed, not actionable or cleared, the newest ones with their Tower issue, and the last
|
|
8
|
+
* 7 days of verdicts. It reads `GET /api/brain/incidents` with this machine's device token.
|
|
9
|
+
*/
|
|
10
|
+
import { LOG_PROJECT_PATTERN } from "@bli-cockpit/telemetry-core/log-digest";
|
|
11
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
12
|
+
/** The statuses the queue's door accepts for `--show`; the migration's CHECK constraint is the other copy. */
|
|
13
|
+
export const BRAIN_SHOW_STATUSES = ["open", "running", "needs_human", "fixed", "not_actionable", "cleared"];
|
|
14
|
+
export function parseBrainArgs(args) {
|
|
15
|
+
const values = parseNamedArgs(args, {
|
|
16
|
+
allowedFlags: ["--project", "--show", "--home", "--dashboard-url", "--json"],
|
|
17
|
+
valueFlags: ["--project", "--show", "--home", "--dashboard-url"],
|
|
18
|
+
});
|
|
19
|
+
const action = values.positionals[0];
|
|
20
|
+
if (action !== "status" || values.positionals.length > 1) {
|
|
21
|
+
throw new Error("brain takes one verb: `status`. Example: `cockpit brain status` or `cockpit brain status --project pearl-portal`.");
|
|
22
|
+
}
|
|
23
|
+
const project = optionalNonEmpty(values.flags.get("--project"));
|
|
24
|
+
if (project && !LOG_PROJECT_PATTERN.test(project))
|
|
25
|
+
throw new Error("--project wants a registry project id like pearl-portal (`cockpit logs --list` names them).");
|
|
26
|
+
const show = optionalNonEmpty(values.flags.get("--show"));
|
|
27
|
+
if (show && !BRAIN_SHOW_STATUSES.includes(show))
|
|
28
|
+
throw new Error(`--show wants one of ${BRAIN_SHOW_STATUSES.join(", ")}.`);
|
|
29
|
+
return {
|
|
30
|
+
kind: "brain",
|
|
31
|
+
action: "status",
|
|
32
|
+
...(project ? { project } : {}),
|
|
33
|
+
...(show ? { show } : {}),
|
|
34
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
35
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
36
|
+
json: values.booleans.has("--json"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -39,5 +39,6 @@ export { parseMailArgs } from "./local-args-tower-mail.js";
|
|
|
39
39
|
export { parseCalArgs } from "./local-args-tower-cal.js";
|
|
40
40
|
export { parseUsageArgs } from "./local-args-tower-usage.js";
|
|
41
41
|
export { parseLogsArgs } from "./local-args-tower-logs.js";
|
|
42
|
+
export { parseBrainArgs } from "./local-args-tower-brain.js";
|
|
42
43
|
export { parseCareersArgs } from './local-args-tower-careers.js';
|
|
43
44
|
export { parseRenameArgs } from './local-args-tower-rename.js';
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { parseRootsArgs } from "./roots.js";
|
|
19
19
|
import { SURFACE_ALIASES } from "./local-help.js";
|
|
20
20
|
import { parseAgentRulesArgs, parseAnalyzeArgs, parseAutostartArgs, parseBackfillArgs, parseCleanArgs, parseDoctorArgs, parseInstallArgs, parseLoginArgs, parseMemoryArgs, parseLogoutArgs, parseOnboardArgs, parseReleaseArgs, parseServeArgs, parseSessionsArgs, parseSourcesArgs, parseStartArgs, parseStatusArgs, parseSyncArgs, parseUpdateArgs, } from "./local-args-collector.js";
|
|
21
|
-
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseLogsArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
21
|
+
import { parseBriefArgs, parseCorrectArgs, parseDocsArgs, parseIssueArgs, parseJarvisArgs, parseMailArgs, parseCalArgs, parseModelArgs, parseMsgArgs, parseNotesArgs, parseOpsArgs, parseProjectArgs, parseModelsArgs, parseScoutArgs, parseGithubArgs, parseSearchArgs, parseSettingsArgs, parseSlackArgs, parseTeamArgs, parseWorkbookArgs, parseUsageArgs, parseLogsArgs, parseBrainArgs, parseCareersArgs, parseRenameArgs, } from "./local-args-tower.js";
|
|
22
22
|
// `normalizeUrl` has always been part of this module's surface — `local.ts` and
|
|
23
23
|
// `local-auth.ts` import it from here — so it stays exported from this address
|
|
24
24
|
// even though it now lives next door. The same goes for the four names the
|
|
@@ -135,6 +135,8 @@ export function parseLocalArgs(argv) {
|
|
|
135
135
|
return parseUsageArgs(argv.slice(1));
|
|
136
136
|
case "logs":
|
|
137
137
|
return parseLogsArgs(argv.slice(1));
|
|
138
|
+
case "brain":
|
|
139
|
+
return parseBrainArgs(argv.slice(1));
|
|
138
140
|
default:
|
|
139
141
|
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
140
142
|
}
|
|
@@ -44,6 +44,23 @@ export const TOWER_OPS_COMMAND_HELP = [
|
|
|
44
44
|
"The bli-tower MCP server has the twin: logs_digest.",
|
|
45
45
|
],
|
|
46
46
|
],
|
|
47
|
+
[
|
|
48
|
+
"brain",
|
|
49
|
+
[
|
|
50
|
+
"Usage: cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
51
|
+
"",
|
|
52
|
+
"Tower's incident queue. Every 15 minutes the log-digest cron turns each recurring problem in a project's logs (classes",
|
|
53
|
+
"our_bug, config_drift, capacity, unknown) into ONE incident with ONE Tower issue; a repeat adds to its count, never a twin.",
|
|
54
|
+
"A template quiet for 24 hours is marked cleared. This prints, per project, how many incidents are open, being worked,",
|
|
55
|
+
"waiting on a person, fixed, not actionable or cleared, the newest ones in the --show status (default open) with their Tower",
|
|
56
|
+
"issue, how often each was seen and what it said, and the last 7 days of verdicts. Needs only this machine's Tower login.",
|
|
57
|
+
"--project <id> one project (`cockpit logs --list` names them).",
|
|
58
|
+
"--show <status> list the newest incidents in that status instead of open.",
|
|
59
|
+
"--json writes one object to stdout: { ok, generated_at, projects[{ project, counts, incidents[], verdicts_7d }], totals, coverage, text };",
|
|
60
|
+
"a refusal is { ok: false, error, detail } and exits 1 (unknown_project, invalid_status, stored_read_failed, not_paired, ...).",
|
|
61
|
+
"Teaching: docs/architecture/tower-brain.md. The bli-tower MCP server has the twin: brain_status.",
|
|
62
|
+
],
|
|
63
|
+
],
|
|
47
64
|
[
|
|
48
65
|
"github",
|
|
49
66
|
[
|
|
@@ -79,6 +79,7 @@ export const rootCommandNames = new Set([
|
|
|
79
79
|
"rename",
|
|
80
80
|
"usage",
|
|
81
81
|
"logs",
|
|
82
|
+
"brain",
|
|
82
83
|
...SURFACE_ALIASES.keys(),
|
|
83
84
|
]);
|
|
84
85
|
/** Public names omit compatibility spellings while the router keeps accepting them. */
|
|
@@ -134,6 +135,7 @@ export function localCommandHelp(command) {
|
|
|
134
135
|
" cockpit careers [list|show <id>|rescreen <id>|send-takehome <id>|decide <id> --decision <passed_on|archived|reopen>|followup <id> [--record-sent-at <iso>]|interview <id> <--at <iso>|--clear>|grade <id> [--now]|resume <id> [--out <path>]|takehome show <role>|takehome set <role> --body-file <path>|settings show|settings set --enabled <true|false> --followup-enabled <true|false> --followup-after-days <n> --takehome-window-hours <n>] [--role <slug>] [--min-score <n>] [--since <date>] [--stage <s>] [--force] [--json]",
|
|
135
136
|
" cockpit rename [list|resolve <id> --scope <page|everywhere|undo>] [--dashboard-url <url>] [--json]",
|
|
136
137
|
" cockpit logs <collector|dashboard|railway|railway:<service>|-> [--since 15m|1h|2d|<iso>] [--raw <template-id> [--limit <n>]] [--all-levels] [--table <file>] [--json]",
|
|
138
|
+
" cockpit brain status [--project <id>] [--show open|running|needs_human|fixed|not_actionable|cleared] [--dashboard-url <url>] [--json]",
|
|
137
139
|
" cockpit usage sessions [--task <BLI-NNNN> | --subject <name>] [--repo <label>] [--since <window>] [--json]",
|
|
138
140
|
" cockpit usage sessions --session <id> [--from-window <n>] [--json]",
|
|
139
141
|
" --by-topic groups work types; --by-subject groups open subjects from session summaries.",
|
package/dist/commands/local.js
CHANGED
|
@@ -50,6 +50,7 @@ import { runCareers } from "./careers.js";
|
|
|
50
50
|
import { runRename } from "./rename-scope.js";
|
|
51
51
|
import { runUsage } from "./usage.js";
|
|
52
52
|
import { runLogs } from "./logs.js";
|
|
53
|
+
import { runBrain } from "./brain.js";
|
|
53
54
|
import { parseLocalArgs } from "./local-args.js";
|
|
54
55
|
// `./local.js` is the published entry point for this command surface: the
|
|
55
56
|
// public CLI's generated root, commands/root.ts, doctor.ts and the test suite
|
|
@@ -245,6 +246,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
245
246
|
return await runUsage(command, io);
|
|
246
247
|
case "logs":
|
|
247
248
|
return await runLogs(command, io);
|
|
249
|
+
case "brain":
|
|
250
|
+
return await runBrain(command, io);
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
catch (error) {
|
|
@@ -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.180");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/dist/commands/status.js
CHANGED
|
@@ -80,7 +80,7 @@ async function writeMultiRepoStatus(command, io, backfillCursor, setupReceipt, w
|
|
|
80
80
|
writeLine(io.stdout, `Old sessions: ${backfillCursorLine(backfillCursor)}`);
|
|
81
81
|
writeLine(io.stdout, await statusDiskLine(command.homeDir));
|
|
82
82
|
for (const status of statuses) {
|
|
83
|
-
writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${status.upload_state}`);
|
|
83
|
+
writeLine(io.stdout, `- ${status.repo_label ?? status.repo}/${status.worktree_label ?? "worktree"} · ${status.branch} · head:${shortSha(status.head_sha)} · ${uploadsLine(status.upload_state)}`);
|
|
84
84
|
}
|
|
85
85
|
writeConnectedBlock(io, setupReceipt);
|
|
86
86
|
return 0;
|
|
@@ -53,6 +53,7 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
53
53
|
oldest_pending_health_receipt_at: healthOutbox.oldest_created_at,
|
|
54
54
|
last_health_receipt_failure_reason: healthOutbox.last_failure_reason,
|
|
55
55
|
stuck_evidence_object_count: stuckEvidence.stuck_object_count,
|
|
56
|
+
stuck_evidence_ghost_record_count: stuckEvidence.ghost_record_count,
|
|
56
57
|
stuck_evidence_held_count: stuckEvidence.held_object_count,
|
|
57
58
|
stuck_evidence_max_attempts: stuckEvidence.max_attempts,
|
|
58
59
|
stuck_evidence_oldest_failure_at: stuckEvidence.oldest_first_failed_at,
|
|
@@ -148,7 +149,7 @@ function describeCollectorStatus(reading) {
|
|
|
148
149
|
details.push(`Last upload failure: ${uploadSpool.last_upload_failure_reason}.`);
|
|
149
150
|
}
|
|
150
151
|
if (uploadSpool.pending_upload_count > 0) {
|
|
151
|
-
details.push(`Upload retry pending: ${uploadSpool.pending_upload_count} safe metadata record(s)
|
|
152
|
+
details.push(`Upload retry pending: ${uploadSpool.pending_upload_count} safe metadata record(s) are saved on this machine and go out on the next background sync.`);
|
|
152
153
|
}
|
|
153
154
|
if (healthOutbox.pending_count > 0) {
|
|
154
155
|
details.push(`Collector health retry pending: ${healthOutbox.pending_count} sanitized receipt(s) queued since ${healthOutbox.oldest_created_at ?? "unknown"}.`);
|
|
@@ -168,6 +169,9 @@ function describeCollectorStatus(reading) {
|
|
|
168
169
|
if (stuckEvidence.stuck_object_count > 0) {
|
|
169
170
|
details.push(`Raw evidence stuck: ${stuckEvidence.stuck_object_count} object(s) have never been accepted (${stuckEvidence.held_object_count} waiting on backoff, worst ${stuckEvidence.max_attempts} attempt(s) since ${stuckEvidence.oldest_first_failed_at ?? "unknown"}) — reasons: ${stuckEvidence.reasons.join(", ") || "unknown"}.`);
|
|
170
171
|
}
|
|
172
|
+
if (stuckEvidence.ghost_record_count > 0) {
|
|
173
|
+
details.push(`${stuckEvidence.ghost_record_count} old delivery record(s) have nothing left to send; no action needed.`);
|
|
174
|
+
}
|
|
171
175
|
return details;
|
|
172
176
|
}
|
|
173
177
|
/**
|
|
@@ -20,7 +20,26 @@ import { deliveryRetryAtMs, terminalDeliveryReason } from "./raw-evidence-stagin
|
|
|
20
20
|
* read green while an object has been failing for nine days.
|
|
21
21
|
*/
|
|
22
22
|
export function summarizeStuckEvidence(state, now) {
|
|
23
|
-
const entries =
|
|
23
|
+
const entries = [];
|
|
24
|
+
let ghostCount = 0;
|
|
25
|
+
for (const [hash, entry] of Object.entries(state.delivery_attempts)) {
|
|
26
|
+
if (terminalDeliveryReason(entry.last_reason))
|
|
27
|
+
continue;
|
|
28
|
+
// A row whose object left staging (pruned after commit, or reconciled by a
|
|
29
|
+
// committed 409) has nothing to offer; counting it as stuck was BLI-12910.
|
|
30
|
+
if (!state.staged[hash]) {
|
|
31
|
+
ghostCount += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
entries.push(entry);
|
|
35
|
+
}
|
|
36
|
+
if (ghostCount > 0) {
|
|
37
|
+
console.error("[raw-evidence-stuck] delivery records with no staged object left out of the stuck count", JSON.stringify({
|
|
38
|
+
ghost_record_count: ghostCount,
|
|
39
|
+
stuck_object_count: entries.length,
|
|
40
|
+
reason: "object_no_longer_staged",
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
24
43
|
const reasons = new Set();
|
|
25
44
|
let heldCount = 0;
|
|
26
45
|
let maxAttempts = 0;
|
|
@@ -52,6 +71,7 @@ export function summarizeStuckEvidence(state, now) {
|
|
|
52
71
|
return {
|
|
53
72
|
stuck_object_count: entries.length,
|
|
54
73
|
held_object_count: heldCount,
|
|
74
|
+
ghost_record_count: ghostCount,
|
|
55
75
|
max_attempts: maxAttempts,
|
|
56
76
|
oldest_first_failed_at: oldest,
|
|
57
77
|
reasons: [...reasons].sort(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.180",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.53",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.95",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.65"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|