@bli-cockpit/cli 0.2.241 → 0.2.243
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/git-state.js +1 -0
- package/dist/adapters/raw-evidence-collection-state.js +22 -11
- package/dist/adapters/raw-evidence-git-diff.js +1 -0
- package/dist/agent-rules.js +8 -0
- package/dist/autostart-contract.js +12 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-darwin.js +1 -1
- package/dist/autostart-remind.js +245 -0
- package/dist/autostart-self-heal.js +17 -1
- package/dist/autostart-windows-scripts.js +7 -7
- package/dist/autostart.js +16 -15
- package/dist/commands/approvals-check-where.js +14 -0
- package/dist/commands/approvals-render.js +8 -1
- package/dist/commands/approvals.js +3 -2
- package/dist/commands/autostart-command.js +10 -0
- package/dist/commands/autostart-heal.js +36 -1
- package/dist/commands/brain.js +1 -1
- package/dist/commands/cal-agenda.js +44 -0
- package/dist/commands/cal-availability-share.js +97 -0
- package/dist/commands/cal-booking.js +147 -0
- package/dist/commands/cal-join.js +62 -0
- package/dist/commands/cal-links.js +68 -0
- package/dist/commands/cal-people.js +136 -0
- package/dist/commands/cal-remind.js +176 -0
- package/dist/commands/cal-reminders.js +33 -0
- package/dist/commands/cal-suggest.js +89 -0
- package/dist/commands/cal.js +41 -1
- package/dist/commands/cycle-args.js +127 -0
- package/dist/commands/cycles.js +117 -0
- package/dist/commands/docs-comments-args.js +85 -0
- package/dist/commands/docs-comments.js +128 -0
- package/dist/commands/docs-files-args.js +49 -0
- package/dist/commands/docs-files.js +179 -0
- package/dist/commands/docs-formatting.js +45 -0
- package/dist/commands/docs-inbox-args.js +93 -0
- package/dist/commands/docs-inbox.js +126 -0
- package/dist/commands/docs-notion.js +5 -0
- package/dist/commands/docs-read.js +2 -1
- package/dist/commands/docs-recovery.js +173 -0
- package/dist/commands/docs-refs-args.js +104 -0
- package/dist/commands/docs-refs.js +153 -0
- package/dist/commands/docs-search-args.js +79 -0
- package/dist/commands/docs-search.js +64 -0
- package/dist/commands/docs-sync-args.js +82 -0
- package/dist/commands/docs-sync.js +197 -0
- package/dist/commands/docs-tree.js +136 -0
- package/dist/commands/docs.js +85 -51
- package/dist/commands/doctor-update.js +4 -1
- package/dist/commands/inbox-args.js +81 -0
- package/dist/commands/inbox.js +88 -0
- package/dist/commands/issue-batch.js +61 -0
- package/dist/commands/issue-relations.js +151 -0
- package/dist/commands/issue-write.js +38 -5
- package/dist/commands/issue.js +32 -1
- package/dist/commands/local-args-tower-approvals.js +16 -4
- package/dist/commands/local-args-tower-cal-booking.js +81 -0
- package/dist/commands/local-args-tower-cal-links.js +42 -0
- package/dist/commands/local-args-tower-cal-remind.js +74 -0
- package/dist/commands/local-args-tower-cal-share.js +41 -0
- package/dist/commands/local-args-tower-cal.js +81 -8
- package/dist/commands/local-args-tower-docs-msg.js +98 -9
- package/dist/commands/local-args-tower-msg.js +279 -10
- package/dist/commands/local-args-tower-search.js +32 -5
- package/dist/commands/local-args-tower-work.js +110 -34
- package/dist/commands/local-args-tower.js +1 -0
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +2 -0
- package/dist/commands/local-help-commands-tower.js +48 -32
- package/dist/commands/local-help-commands.js +1 -0
- package/dist/commands/local-help-cycles.js +9 -0
- package/dist/commands/local-help-docs-files.js +10 -0
- package/dist/commands/local-help-docs-formatting.js +7 -0
- package/dist/commands/local-help-docs-inbox.js +10 -0
- package/dist/commands/local-help-docs-recovery.js +10 -0
- package/dist/commands/local-help-docs-refs.js +11 -0
- package/dist/commands/local-help-docs-sync.js +9 -0
- package/dist/commands/local-help-inbox.js +9 -0
- package/dist/commands/local-help-project.js +21 -0
- package/dist/commands/local-help-templates.js +9 -0
- package/dist/commands/local-help-views.js +10 -0
- package/dist/commands/local-help-work-notify.js +6 -0
- package/dist/commands/local-help.js +4 -2
- package/dist/commands/local.js +2 -0
- package/dist/commands/mcp-stdio-probe.js +1 -0
- package/dist/commands/msg-activity.js +134 -0
- package/dist/commands/msg-channel-args.js +92 -0
- package/dist/commands/msg-channel.js +125 -0
- package/dist/commands/msg-context-args.js +67 -0
- package/dist/commands/msg-context.js +65 -0
- package/dist/commands/msg-dm.js +28 -0
- package/dist/commands/msg-draft.js +193 -0
- package/dist/commands/msg-file.js +185 -0
- package/dist/commands/msg-mark-args.js +78 -0
- package/dist/commands/msg-mark.js +148 -0
- package/dist/commands/msg-notify-args.js +100 -0
- package/dist/commands/msg-notify.js +122 -0
- package/dist/commands/msg-schedule-args.js +114 -0
- package/dist/commands/msg-schedule.js +226 -0
- package/dist/commands/msg-search-args.js +71 -0
- package/dist/commands/msg-search.js +48 -0
- package/dist/commands/msg-write.js +15 -1
- package/dist/commands/msg.js +77 -39
- package/dist/commands/project-args.js +82 -0
- package/dist/commands/project.js +71 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/push.js +50 -0
- package/dist/commands/search.js +23 -0
- package/dist/commands/session-sync-failures.js +17 -4
- package/dist/commands/session-sync-health.js +7 -2
- package/dist/commands/session-sync-hold.js +42 -1
- package/dist/commands/session-sync-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sync-doctor.js +14 -1
- package/dist/commands/sync-report.js +1 -1
- package/dist/commands/template-args.js +122 -0
- package/dist/commands/templates.js +162 -0
- package/dist/commands/view-args.js +131 -0
- package/dist/commands/views.js +138 -0
- package/dist/commands/work-notify-args.js +67 -0
- package/dist/commands/work-notify.js +63 -0
- package/dist/delivery-hold-notice.js +34 -0
- package/dist/log-rotation.js +2 -2
- package/dist/notifier.js +83 -0
- package/dist/process-runner.js +4 -0
- package/dist/repo-identity-git.js +1 -0
- package/package.json +4 -4
package/dist/commands/project.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cockpit project list` — the projects issues are filed under
|
|
2
|
+
* `cockpit project list|show|create|update|archive|restore` — the projects issues are filed under
|
|
3
|
+
* (BLI-3716 list; BLI-21659 the rest).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* command rather than `cockpit issue projects` because a person says
|
|
8
|
-
* "projects", and `cockpit issue list --project <name>` resolves a name
|
|
9
|
-
* against exactly this list.
|
|
5
|
+
* Its own command rather than `cockpit issue projects` because a person says "projects", and
|
|
6
|
+
* `cockpit issue list --project <name>` resolves a name against exactly this list. Writes go through
|
|
7
|
+
* `/api/work/projects` (receipt, `tower_only` delivery: nothing reaches Linear); no verb deletes a project.
|
|
10
8
|
*/
|
|
11
9
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
12
10
|
import { writeLine } from "./cli-io.js";
|
|
13
11
|
import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
|
|
12
|
+
import { readBody } from "./issue-contracts.js";
|
|
14
13
|
const TAG = "[project cli]";
|
|
15
14
|
const READ_DEADLINE_MS = 30_000;
|
|
16
15
|
export async function runProject(command, io) {
|
|
17
16
|
const door = await openAgentDoor("project", command, io);
|
|
17
|
+
if (command.action !== "list")
|
|
18
|
+
return await runProjectVerb(command, door);
|
|
18
19
|
const params = new URLSearchParams();
|
|
19
20
|
if (command.includeArchived)
|
|
20
21
|
params.set("include_archived", "true");
|
|
@@ -47,4 +48,67 @@ export async function runProject(command, io) {
|
|
|
47
48
|
if (footer)
|
|
48
49
|
writeLine(door.io.stdout, footer);
|
|
49
50
|
return 0;
|
|
51
|
+
}
|
|
52
|
+
async function runProjectVerb(command, door) {
|
|
53
|
+
const base = "/api/work/projects";
|
|
54
|
+
const ref = command.ref ? `${base}/${encodeURIComponent(command.ref)}` : base;
|
|
55
|
+
if (command.action === "show") {
|
|
56
|
+
const answer = await askAgentDoor(door, { path: ref, method: "GET", label: "project show", timeoutMs: READ_DEADLINE_MS });
|
|
57
|
+
if (!answer.ok)
|
|
58
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
59
|
+
const body = answer.body;
|
|
60
|
+
if (door.json)
|
|
61
|
+
return emitAgentDoor(door, { ok: true, ...body, ...pageFieldsOf(body) });
|
|
62
|
+
const project = body.project;
|
|
63
|
+
const out = door.io.stdout;
|
|
64
|
+
writeLine(out, `${project?.name ?? command.ref} (${project?.archived_at ? "archived" : "active"}, ${project?.source ?? "native"})`);
|
|
65
|
+
writeLine(out, `id ${project?.id ?? ""} updated ${project?.updated_at ?? ""}`);
|
|
66
|
+
if (project?.description)
|
|
67
|
+
writeLine(out, project.description);
|
|
68
|
+
for (const issue of body.issues ?? [])
|
|
69
|
+
writeLine(out, `${issue.identifier ?? ""} ${issue.state ?? ""} ${issue.title ?? ""}`);
|
|
70
|
+
const footer = pageFooter(body, "tickets");
|
|
71
|
+
if (footer)
|
|
72
|
+
writeLine(out, footer);
|
|
73
|
+
if (body.href)
|
|
74
|
+
writeLine(out, `Filtered list: ${body.href}`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const payload = {
|
|
78
|
+
...(command.requestId ? { request_id: command.requestId } : {}),
|
|
79
|
+
...(command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}),
|
|
80
|
+
};
|
|
81
|
+
if (command.action === "create" || command.action === "update") {
|
|
82
|
+
if (command.name !== undefined)
|
|
83
|
+
payload["name"] = command.name;
|
|
84
|
+
if (command.color !== undefined)
|
|
85
|
+
payload["color"] = command.color;
|
|
86
|
+
if (command.filePath) {
|
|
87
|
+
const read = await readBody({ filePath: command.filePath }, door.io);
|
|
88
|
+
if (!read.ok)
|
|
89
|
+
return failAgentDoor(door, TAG, read.reason, read.detail);
|
|
90
|
+
payload["description"] = read.text;
|
|
91
|
+
}
|
|
92
|
+
else if (command.description !== undefined) {
|
|
93
|
+
payload["description"] = command.description;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const path = command.action === "create" ? base : command.action === "update" ? ref : `${ref}/${command.action}`;
|
|
97
|
+
const answer = await askAgentDoor(door, {
|
|
98
|
+
path,
|
|
99
|
+
method: command.action === "update" ? "PATCH" : "POST",
|
|
100
|
+
label: `project ${command.action}`,
|
|
101
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
102
|
+
body: payload,
|
|
103
|
+
});
|
|
104
|
+
if (!answer.ok) {
|
|
105
|
+
const mutation = answer.body?.mutation;
|
|
106
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
107
|
+
}
|
|
108
|
+
const body = answer.body;
|
|
109
|
+
console.error(`${TAG} ${command.action} committed`, JSON.stringify({ project_id: body.project?.id, delivery: body.mutation?.delivery?.status }));
|
|
110
|
+
if (door.json)
|
|
111
|
+
return emitAgentDoor(door, { ok: true, project: body.project, mutation: body.mutation });
|
|
112
|
+
writeLine(door.io.stdout, `${command.action} committed: ${body.project?.name ?? ""} (${body.project?.id ?? ""}), delivery ${body.mutation?.delivery?.status ?? "tower_only"}.`);
|
|
113
|
+
return 0;
|
|
50
114
|
}
|
|
@@ -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.243");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit push status` and `cockpit push test`: phone notifications from the terminal (BLI-21497).
|
|
3
|
+
*
|
|
4
|
+
* `status` reads `GET /api/push/subscriptions` (the caller's own saved devices, and whether the server has its VAPID
|
|
5
|
+
* keys); `test` posts `POST /api/push/test` (a test push to every saved device). The same doors the Settings page and the
|
|
6
|
+
* `push_status` and `push_test` MCP tools use. Turning a phone on is the browser's own permission prompt, so there is no
|
|
7
|
+
* terminal verb for it. `--json` prints the door's body untouched.
|
|
8
|
+
*/
|
|
9
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
10
|
+
import { writeLine } from "./cli-io.js";
|
|
11
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
12
|
+
const USAGE = "Usage: cockpit push status | test [--dashboard-url <url>] [--json]";
|
|
13
|
+
export function parsePushArgs(args) {
|
|
14
|
+
const values = parseNamedArgs(args, { allowedFlags: ["--json", "--home", "--dashboard-url"], valueFlags: ["--home", "--dashboard-url"] });
|
|
15
|
+
const [action, ...rest] = values.positionals;
|
|
16
|
+
if ((action !== "status" && action !== "test") || rest.length > 0)
|
|
17
|
+
throw new Error(USAGE);
|
|
18
|
+
return { kind: "push", action, json: values.booleans.has("--json"), homeDir: optionalNonEmpty(values.flags.get("--home")), dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")) };
|
|
19
|
+
}
|
|
20
|
+
export function renderPushStatus(body) {
|
|
21
|
+
const devices = body.devices ?? [];
|
|
22
|
+
const lines = [`Server push keys: ${body.configured ? "set" : "not set (nothing can be sent yet)"}.`, `Saved devices: ${devices.length}.`];
|
|
23
|
+
for (const device of devices)
|
|
24
|
+
lines.push(` ${device.created_at.slice(0, 16).replace("T", " ")}Z ${device.user_agent ?? "unknown browser"}`);
|
|
25
|
+
if (devices.length === 0)
|
|
26
|
+
lines.push("Turn notifications on from Settings, on the phone (add TOWER to the Home Screen first on iPhone).");
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
export function renderPushTest(body) {
|
|
30
|
+
if (body.skipped === "push_not_configured")
|
|
31
|
+
return ["Nothing sent: the server has no VAPID keys yet."];
|
|
32
|
+
if (body.skipped === "no_devices")
|
|
33
|
+
return ["Nothing sent: no device is saved. Turn notifications on from Settings on the phone."];
|
|
34
|
+
if (body.skipped)
|
|
35
|
+
return [`Nothing sent: ${body.skipped}.`];
|
|
36
|
+
const reasons = Object.entries(body.reasons ?? {}).map(([reason, count]) => `${reason} x${count}`).join(", ");
|
|
37
|
+
return [`Test push: ${body.accepted ?? 0} of ${body.devices ?? 0} devices accepted, ${body.failed ?? 0} failed${reasons ? ` (${reasons})` : ""}.`];
|
|
38
|
+
}
|
|
39
|
+
export async function runPush(command, io) {
|
|
40
|
+
const door = await openAgentDoor("push", command, io);
|
|
41
|
+
const status = command.action === "status";
|
|
42
|
+
const answer = await askAgentDoor(door, { path: status ? "/api/push/subscriptions" : "/api/push/test", method: status ? "GET" : "POST", label: `push ${command.action}`, timeoutMs: 60_000, ...(status ? {} : { body: {} }) });
|
|
43
|
+
if (!answer.ok)
|
|
44
|
+
return failAgentDoor(door, "[push cli]", answer.reason, answer.detail);
|
|
45
|
+
if (door.json)
|
|
46
|
+
return emitAgentDoor(door, answer.body);
|
|
47
|
+
for (const line of status ? renderPushStatus(answer.body) : renderPushTest(answer.body))
|
|
48
|
+
writeLine(door.io.stdout, line);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
package/dist/commands/search.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
28
28
|
import { writeLine } from "./cli-io.js";
|
|
29
|
+
import { appendDocSearchParams } from "./docs-search-args.js";
|
|
30
|
+
import { appendMsgSearchParams } from "./msg-search-args.js";
|
|
29
31
|
const TAG = "[search cli]";
|
|
30
32
|
const READ_DEADLINE_MS = 30_000;
|
|
31
33
|
/** Group headings, in the order a person reads them. Mirrors the overlay. */
|
|
@@ -56,6 +58,12 @@ export async function runSearch(command, io) {
|
|
|
56
58
|
params.set("kinds", command.kinds.join(","));
|
|
57
59
|
if (command.limit !== undefined)
|
|
58
60
|
params.set("limit", String(command.limit));
|
|
61
|
+
// BLI-21658: message filters ride the same door, which answers them only for kinds=msg.
|
|
62
|
+
if (command.msgFilters)
|
|
63
|
+
appendMsgSearchParams(params, command.msgFilters);
|
|
64
|
+
// BLI-21697: document filters ride the same door, which answers them only for kinds=doc.
|
|
65
|
+
if (command.docFilters)
|
|
66
|
+
appendDocSearchParams(params, command.docFilters);
|
|
59
67
|
const answer = await askAgentDoor(door, {
|
|
60
68
|
path: `/api/search?${params.toString()}`,
|
|
61
69
|
method: "GET",
|
|
@@ -139,6 +147,21 @@ function render(door, body) {
|
|
|
139
147
|
writeLine(door.io.stdout, `${KIND_LABELS[kind] ?? kind.toUpperCase()} did not answer (${reason}). Nothing from there is in this list.`);
|
|
140
148
|
}
|
|
141
149
|
}
|
|
150
|
+
// Named, so a filtered list is not read as the ranked, semantic one (BLI-21658).
|
|
151
|
+
if (body.degraded?.msg === "filtered_lexical_only") {
|
|
152
|
+
writeLine(door.io.stdout, "");
|
|
153
|
+
writeLine(door.io.stdout, "MESSAGES were filtered: keyword matching only (filtered_lexical_only), newest first, not ranked.");
|
|
154
|
+
if (body.msgPaging?.hasMore && body.msgPaging.nextCursor) {
|
|
155
|
+
writeLine(door.io.stdout, `More messages exist: \`cockpit msg search ... --before ${body.msgPaging.nextCursor}\` continues with the same filters.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (body.degraded?.doc === "filtered_lexical_only") {
|
|
159
|
+
writeLine(door.io.stdout, "");
|
|
160
|
+
writeLine(door.io.stdout, "DOCUMENTS were filtered: keyword matching only (filtered_lexical_only), not ranked by meaning.");
|
|
161
|
+
if (body.docCoverage && body.docCoverage.coverage !== "exhausted") {
|
|
162
|
+
writeLine(door.io.stdout, `Documents coverage: ${body.docCoverage.coverage}${body.docCoverage.reason ? ` (${body.docCoverage.reason})` : ""}; ${body.docCoverage.matched} matched.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
142
165
|
writeLine(door.io.stdout, "");
|
|
143
166
|
writeLine(door.io.stdout, `${hits.length} result(s) in ${body.elapsedMs ?? 0} ms.`);
|
|
144
167
|
return 0;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* without saying why.
|
|
14
14
|
*/
|
|
15
15
|
import { claudeAttributionReadFailureCount, codexAttributionReadFailureCount, } from "./agent-session-report.js";
|
|
16
|
-
import { isDeliveryHoldReason } from "./session-sync-hold.js";
|
|
16
|
+
import { budgetStarved, isDeliveryHoldReason } from "./session-sync-hold.js";
|
|
17
17
|
import { sourceScanFailureReason } from "./session-sync-scan.js";
|
|
18
18
|
/**
|
|
19
19
|
* The label used when a sync fails and nothing on the way there said why.
|
|
@@ -55,8 +55,15 @@ export function createSyncFailureLedger() {
|
|
|
55
55
|
* hold in `session-sync-hold.ts` and rides the same receipt — and a tick that
|
|
56
56
|
* ALSO failed for a real reason still fails on that reason. Nothing is
|
|
57
57
|
* silenced; only the verdict changed.
|
|
58
|
+
*
|
|
59
|
+
* Ticket 21952 does the same for the per-tick size budget. Overflow defers to the next tick by design, so a tick that
|
|
60
|
+
* sent something and held the rest reports a named hold (`held_budget:` in `session-sync-hold.ts`) instead of failing:
|
|
61
|
+
* one machine was RED on every tick from 2026-10-15 while it uploaded 2 GiB each time. Only a STARVED tick, one that
|
|
62
|
+
* deferred evidence and let nothing through, still fails `deferred_byte_budget` / `deferred_object_budget`. Without
|
|
63
|
+
* `budgetUse` (an older caller) the old rule stands: any deferral fails.
|
|
58
64
|
*/
|
|
59
|
-
export function recordWorktreeDeliveryFailures(ledger, outcomes) {
|
|
65
|
+
export function recordWorktreeDeliveryFailures(ledger, outcomes, budgetUse) {
|
|
66
|
+
const budgetHolds = budgetUse !== undefined && !budgetStarved(budgetUse);
|
|
60
67
|
for (const outcome of outcomes) {
|
|
61
68
|
recordWorktreeSyncFailure(ledger, outcome);
|
|
62
69
|
const { sync } = outcome;
|
|
@@ -71,15 +78,21 @@ export function recordWorktreeDeliveryFailures(ledger, outcomes) {
|
|
|
71
78
|
for (const reason of sync.raw_evidence_retry_reasons ?? []) {
|
|
72
79
|
if (isDeliveryHoldReason(reason))
|
|
73
80
|
continue;
|
|
81
|
+
if (budgetHolds && isBudgetDeferralReason(reason))
|
|
82
|
+
continue;
|
|
74
83
|
ledger.add({
|
|
75
84
|
label: "raw_evidence_retry_required",
|
|
76
85
|
rendered: `raw_evidence_retry:${reason}`,
|
|
77
86
|
});
|
|
78
87
|
}
|
|
79
|
-
ledger.fail(sync.raw_evidence_deferred_byte_budget > 0, "deferred_byte_budget");
|
|
80
|
-
ledger.fail(sync.raw_evidence_deferred_object_budget > 0, "deferred_object_budget");
|
|
88
|
+
ledger.fail(!budgetHolds && sync.raw_evidence_deferred_byte_budget > 0, "deferred_byte_budget");
|
|
89
|
+
ledger.fail(!budgetHolds && sync.raw_evidence_deferred_object_budget > 0, "deferred_object_budget");
|
|
81
90
|
}
|
|
82
91
|
}
|
|
92
|
+
/** The two reasons the per-tick budget writes when it defers a file. Exact labels, never a pattern. */
|
|
93
|
+
function isBudgetDeferralReason(reason) {
|
|
94
|
+
return reason === "deferred_byte_budget" || reason === "deferred_object_budget";
|
|
95
|
+
}
|
|
83
96
|
function recordWorktreeSyncFailure(ledger, { worktree, sync }) {
|
|
84
97
|
if (sync.status === "uploaded")
|
|
85
98
|
return;
|
|
@@ -13,7 +13,7 @@ import { collectionRootLabel } from "./heartbeat.js";
|
|
|
13
13
|
import { workDirectories } from "../root-work.js";
|
|
14
14
|
import { createSyncFailureLedger, recordSourceScanFailures, recordUnexplainedFailure, recordUnpostedSessionReportFailure, recordWorktreeDeliveryFailures, } from "./session-sync-failures.js";
|
|
15
15
|
import { countSessionsNewThisTick, countSessionsPendingUpload, } from "./session-sync-counters.js";
|
|
16
|
-
import { logDeliveryHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
16
|
+
import { logBudgetHold, logDeliveryHold, summarizeBudgetHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
17
17
|
/**
|
|
18
18
|
* Decide whether this tick failed, and record every condition that decided it.
|
|
19
19
|
*
|
|
@@ -23,7 +23,7 @@ import { logDeliveryHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
|
23
23
|
export function decideSyncHealth(options) {
|
|
24
24
|
const { outcomes } = options.worktreePass;
|
|
25
25
|
const ledger = createSyncFailureLedger();
|
|
26
|
-
recordWorktreeDeliveryFailures(ledger, outcomes);
|
|
26
|
+
recordWorktreeDeliveryFailures(ledger, outcomes, options.worktreePass.budgetUse);
|
|
27
27
|
recordSourceScanFailures(ledger, options.scan);
|
|
28
28
|
const sessionsOutsideRoot = options.sessions.filter((session) => OUTSIDE_APPROVED_ROOT_REASONS.has(session.attribution_reason)).length;
|
|
29
29
|
const nothingInRoot = nothingInRootCount({
|
|
@@ -54,6 +54,10 @@ export function decideSyncHealth(options) {
|
|
|
54
54
|
const hold = summarizeDeliveryHold(outcomes);
|
|
55
55
|
if (hold && ok)
|
|
56
56
|
logDeliveryHold(hold);
|
|
57
|
+
// Ticket 21952: the size-budget hold, said on every tick that has one, ok or not.
|
|
58
|
+
const budgetHold = summarizeBudgetHold(options.worktreePass.budgetUse);
|
|
59
|
+
if (budgetHold)
|
|
60
|
+
logBudgetHold(budgetHold);
|
|
57
61
|
const records = ledger.sortedRecords();
|
|
58
62
|
return {
|
|
59
63
|
ok,
|
|
@@ -61,6 +65,7 @@ export function decideSyncHealth(options) {
|
|
|
61
65
|
failure_records: records,
|
|
62
66
|
notice,
|
|
63
67
|
hold,
|
|
68
|
+
budgetHold,
|
|
64
69
|
sessions_outside_root: sessionsOutsideRoot,
|
|
65
70
|
outside_root_directories: workDirectories([...(options.scan.codexAttribution.results ?? []), ...(options.scan.claudeAttribution.results ?? [])].filter(result => OUTSIDE_APPROVED_ROOT_REASONS.has(result.reason)))
|
|
66
71
|
.map(entry => ({ ...collectionRootLabel(entry.directory), count: entry.count }))
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* reads every `session-sync*.ts` sibling and demands that only the failure
|
|
12
12
|
* ledger writes one, and this file is not it.
|
|
13
13
|
*/
|
|
14
|
-
import { formatDeliveryHoldNotice, } from "../delivery-hold-notice.js";
|
|
14
|
+
import { formatBudgetHoldNotice, formatDeliveryHoldNotice, } from "../delivery-hold-notice.js";
|
|
15
15
|
import { DELIVERY_BACKOFF_HOLDING_REASON } from "../raw-evidence-staging.js";
|
|
16
16
|
/**
|
|
17
17
|
* Is this reason a delivery hold rather than a delivery failure?
|
|
@@ -87,4 +87,45 @@ export function logDeliveryHold(hold) {
|
|
|
87
87
|
last_reason: hold.lastReason,
|
|
88
88
|
next_action: "nothing until the next attempt is due; run `cockpit doctor` if it is already past",
|
|
89
89
|
}));
|
|
90
|
+
}
|
|
91
|
+
/** The token's leading word, owned with its grammar by `delivery-hold-notice.ts` so the fleet board parses what this writes. */
|
|
92
|
+
export { BUDGET_HOLD_NOTICE_PREFIX } from "../delivery-hold-notice.js";
|
|
93
|
+
/** Reads the shared budget's counters, or null when the pass carried none. */
|
|
94
|
+
export function budgetUseOf(budget) {
|
|
95
|
+
if (!budget)
|
|
96
|
+
return undefined;
|
|
97
|
+
return {
|
|
98
|
+
admittedBytes: budget.admittedBytes ?? 0,
|
|
99
|
+
admittedObjects: budget.admittedObjects ?? 0,
|
|
100
|
+
deferredBytes: budget.deferredBytes ?? 0,
|
|
101
|
+
deferredObjects: budget.deferredObjects ?? 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Did the budget hold anything back while still letting something through?
|
|
106
|
+
*
|
|
107
|
+
* The collector's design is that overflow defers and converges: the next tick starts with a fresh budget and picks up
|
|
108
|
+
* what this one held. A tick that admitted at least one object made progress, so what it deferred is a hold. A tick
|
|
109
|
+
* that deferred and admitted NOTHING made no progress at all, which stays a failure (`budgetStarved`).
|
|
110
|
+
*/
|
|
111
|
+
export function summarizeBudgetHold(use) {
|
|
112
|
+
if (!use || use.deferredObjects <= 0 || budgetStarved(use))
|
|
113
|
+
return null;
|
|
114
|
+
const notice = formatBudgetHoldNotice(use);
|
|
115
|
+
return notice ? { ...use, notice } : null;
|
|
116
|
+
}
|
|
117
|
+
/** True when the budget deferred evidence and let nothing through this tick. */
|
|
118
|
+
export function budgetStarved(use) {
|
|
119
|
+
return Boolean(use && use.deferredObjects > 0 && use.admittedObjects === 0);
|
|
120
|
+
}
|
|
121
|
+
/** The run's line for a size-budget hold: bytes sent, bytes held, and that the next tick takes the rest. */
|
|
122
|
+
export function logBudgetHold(hold) {
|
|
123
|
+
console.error("[session-sync] raw evidence held back by the per-tick size budget, the next tick takes the rest", JSON.stringify({
|
|
124
|
+
reason: hold.notice,
|
|
125
|
+
sent_bytes: hold.admittedBytes,
|
|
126
|
+
sent_objects: hold.admittedObjects,
|
|
127
|
+
held_bytes: hold.deferredBytes,
|
|
128
|
+
held_objects: hold.deferredObjects,
|
|
129
|
+
next_action: "none; the next tick starts with a fresh budget",
|
|
130
|
+
}));
|
|
90
131
|
}
|
|
@@ -13,6 +13,7 @@ import { liveSyncTargetKey, liveSyncTargetWorktrees, matchesLiveSyncWorktree, }
|
|
|
13
13
|
import { isOutsideRootsWorkspace } from "./backfill-outside-roots.js";
|
|
14
14
|
import { giveOutsideRootsTheirOwnAllowance, outsideRootsLast, } from "./session-sync-outside-roots.js";
|
|
15
15
|
import { runYieldingAtBoundaries } from "./sync-yield.js";
|
|
16
|
+
import { budgetUseOf } from "./session-sync-hold.js";
|
|
16
17
|
const CLAUDE_DAMP_GROWTH_BYTES = 256 * 1024;
|
|
17
18
|
const CLAUDE_DAMP_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
18
19
|
/**
|
|
@@ -54,6 +55,7 @@ export async function syncEveryTargetWorktree(options) {
|
|
|
54
55
|
claudePriorDurablePointers: claudeDurablePointersFromCursor(options.plan.claudeCursorBefore),
|
|
55
56
|
growthDampedSessionCount: dampedClaudePointers.size,
|
|
56
57
|
yieldedWorktreesLeft: loop.left,
|
|
58
|
+
budgetUse: budgetUseOf(rawEvidenceBudget),
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
function syncTargetWithAllowance(options, worktree, syncWorktrees, rawEvidenceBudget, discoveredWorktreeKeys, dampedClaudePointers) {
|
|
@@ -190,6 +190,7 @@ async function finishAttributedSync(options, context) {
|
|
|
190
190
|
failure_records: health.failure_records,
|
|
191
191
|
notice: health.notice,
|
|
192
192
|
hold: health.hold,
|
|
193
|
+
budgetHold: health.budgetHold ?? null,
|
|
193
194
|
yielded_worktrees_left: worktreePass.yieldedWorktreesLeft,
|
|
194
195
|
sessions_observed: sessions.length,
|
|
195
196
|
sessions_outside_root: health.sessions_outside_root,
|
|
@@ -14,6 +14,7 @@ import { doctorNeedsPerson, sanitizeEventCode } from "./doctor-report.js";
|
|
|
14
14
|
import { runSelfUpdate } from "./install-update.js";
|
|
15
15
|
import { reportInstallEventsBestEffort } from "./install-receipts.js";
|
|
16
16
|
import { clearDoctorRequest, requestedDoctorId } from "./sync-doctor-request.js";
|
|
17
|
+
import { envWithNodeRuntimeOnPath } from "../scheduled-self-update.js";
|
|
17
18
|
export async function runDoctorAfterSync(command, io, dashboardUrl, overrides = {}, facts) {
|
|
18
19
|
if (shouldSuppressFleetReceipts({ dashboardUrl }).suppressed) {
|
|
19
20
|
console.error("[sync-doctor] checkout does not repair the fleet machine", JSON.stringify({ reason: "receipt_suppressed:dev_build" }));
|
|
@@ -69,11 +70,23 @@ export async function runDoctorAfterSync(command, io, dashboardUrl, overrides =
|
|
|
69
70
|
await lock.handle.release();
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Ticket 21952. launchd hands the tick `PATH=/usr/bin:/bin:/usr/sbin:/sbin`, where neither `node` nor `npm` lives, so
|
|
75
|
+
* the tick doctor's `npm view` and `npm install` could not start on any Mac: every macOS receipt read
|
|
76
|
+
* `latest_version_unknown -> fail:npm_install_failed` while already on the newest CLI, and Windows (whose scheduler
|
|
77
|
+
* keeps the user's PATH) read `already_latest`. The scheduled self-update already runs npm with the running node's bin
|
|
78
|
+
* dir on PATH (`envWithNodeRuntimeOnPath`); the tick doctor now does the same. A caller's explicit env still wins.
|
|
79
|
+
*/
|
|
80
|
+
export function withNodeRuntimeOnPath(io) {
|
|
81
|
+
const env = envWithNodeRuntimeOnPath(io.env ?? process.env);
|
|
82
|
+
const wrap = (runner) => runner ? (cmd, args, options) => runner(cmd, args, { ...options, env: options?.env ?? env }) : undefined;
|
|
83
|
+
return { exec: wrap(io.exec), interactiveExec: wrap(io.interactiveExec) };
|
|
84
|
+
}
|
|
72
85
|
async function collectDoctorReceipt(command, io, dashboardUrl, overrides, facts, paths, requestId) {
|
|
73
86
|
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
74
87
|
let receipt = null;
|
|
75
88
|
const quiet = new Writable({ write(_chunk, _encoding, done) { done(); } });
|
|
76
|
-
const doctorIo = { ...io, stdout: quiet };
|
|
89
|
+
const doctorIo = { ...io, stdout: quiet, ...withNodeRuntimeOnPath(io) };
|
|
77
90
|
await runDoctor({ kind: "doctor", alias: "doctor", automaticTick: true, homeDir: command.homeDir, dashboardUrl, json: true, dryRun: false }, doctorIo, {
|
|
78
91
|
selfUpdate: (updateIo, options) => runSelfUpdate({ ...updateIo, stdout: quiet }, options),
|
|
79
92
|
runLogin: async () => { throw new Error("automatic_doctor_cannot_prompt"); },
|
|
@@ -152,7 +152,7 @@ function syncResult(run) {
|
|
|
152
152
|
notice: run.notice,
|
|
153
153
|
// BLI-4022: unlike the reasons, the hold survives a successful run — it is
|
|
154
154
|
// what the tick withheld, not why it failed.
|
|
155
|
-
holdNotice: run.hold?.notice ?? null,
|
|
155
|
+
holdNotice: run.hold?.notice ?? run.budgetHold?.notice ?? null,
|
|
156
156
|
sessionsObserved: run.sessions_observed,
|
|
157
157
|
sessionsOutsideRoot: run.sessions_outside_root,
|
|
158
158
|
outsideRootDirectories: run.outside_root_directories,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit ticket templates [list|show|versions|save|update|apply]` argument parsing (BLI-21779, P11).
|
|
3
|
+
* `templates` is an `issue` action; its flags are read here so `local-args-tower-work.ts` only dispatches,
|
|
4
|
+
* the way `view-args.ts` does for views.
|
|
5
|
+
*
|
|
6
|
+
* Property flags mean the same on save, update and apply: `--description <text|->` (`-` reads stdin),
|
|
7
|
+
* `--state`, `--priority 0-4`, `--size S|M|L`, `--project <name|id>`, `--label <name>` (repeatable).
|
|
8
|
+
* On save and update, the value `none` clears a preset; on apply, `none` clears what the template would
|
|
9
|
+
* have copied. Nothing here carries a title except `apply`, and nothing stores one.
|
|
10
|
+
*/
|
|
11
|
+
import { estimateForSize } from "./issue-contracts.js";
|
|
12
|
+
import { optionalNonEmpty, optionalUrl } from "./local-arg-values.js";
|
|
13
|
+
export const TEMPLATE_VERBS = ["list", "show", "versions", "save", "update", "apply"];
|
|
14
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
const STATES = ["backlog", "todo", "in_progress", "in_review", "done", "canceled"];
|
|
16
|
+
const NONE = (value) => value.trim().toLowerCase() === "none";
|
|
17
|
+
export function parseTemplateArgs(values, rest) {
|
|
18
|
+
const first = rest[0];
|
|
19
|
+
const verb = (first === undefined ? "list" : first);
|
|
20
|
+
if (!TEMPLATE_VERBS.includes(verb)) {
|
|
21
|
+
throw new Error(`Unknown templates command: ${first}. Try list, show, versions, save, update or apply.`);
|
|
22
|
+
}
|
|
23
|
+
const positional = rest.slice(first === undefined ? 0 : 1);
|
|
24
|
+
const needsRef = verb !== "list" && verb !== "save";
|
|
25
|
+
if (needsRef && positional.length !== 1)
|
|
26
|
+
throw new Error(`ticket templates ${verb} takes exactly one template id or name.`);
|
|
27
|
+
if (!needsRef && positional.length > 0)
|
|
28
|
+
throw new Error(`ticket templates ${verb} does not take "${positional[0]}".`);
|
|
29
|
+
const given = (flag) => values.flags.has(flag) || values.booleans.has(flag);
|
|
30
|
+
const writes = verb === "save" || verb === "update" || verb === "apply";
|
|
31
|
+
for (const flag of ["--description", "--state", "--priority", "--size", "--project", "--label"]) {
|
|
32
|
+
if (given(flag) && !writes)
|
|
33
|
+
throw new Error(`${flag} belongs to \`cockpit ticket templates save\`, \`update\` and \`apply\`.`);
|
|
34
|
+
}
|
|
35
|
+
if (given("--name") && verb !== "save" && verb !== "update")
|
|
36
|
+
throw new Error("--name belongs to `cockpit ticket templates save` and `update`.");
|
|
37
|
+
if (verb !== "update" && (given("--archive") || given("--restore") || given("--expected-updated-at"))) {
|
|
38
|
+
throw new Error("--archive, --restore and --expected-updated-at belong to `cockpit ticket templates update`.");
|
|
39
|
+
}
|
|
40
|
+
if (verb !== "apply" && (given("--title") || given("--assignee") || given("--parent") || given("--due"))) {
|
|
41
|
+
throw new Error("--title, --assignee, --parent and --due belong to `cockpit ticket templates apply`; a template never stores them.");
|
|
42
|
+
}
|
|
43
|
+
if (verb !== "list" && given("--include-archived"))
|
|
44
|
+
throw new Error("--include-archived belongs to `cockpit ticket templates list`.");
|
|
45
|
+
if (given("--archive") && given("--restore"))
|
|
46
|
+
throw new Error("Give --archive or --restore, not both.");
|
|
47
|
+
if ((verb === "save" || verb === "apply") === false && given("--request-id")) {
|
|
48
|
+
throw new Error("--request-id belongs to `cockpit ticket templates save` and `apply`.");
|
|
49
|
+
}
|
|
50
|
+
if (given("--expected-revision") || given("--limit") || given("--shared") || given("--private")) {
|
|
51
|
+
throw new Error("That flag does not belong to ticket templates.");
|
|
52
|
+
}
|
|
53
|
+
const name = optionalNonEmpty(values.flags.get("--name"));
|
|
54
|
+
if (verb === "save" && !name)
|
|
55
|
+
throw new Error('ticket templates save needs --name "<name>".');
|
|
56
|
+
const rawDescription = values.flags.get("--description");
|
|
57
|
+
const rawState = optionalNonEmpty(values.flags.get("--state"));
|
|
58
|
+
if (rawState && !NONE(rawState) && !STATES.includes(rawState))
|
|
59
|
+
throw new Error(`--state must be one of: ${STATES.join(", ")} (or none).`);
|
|
60
|
+
const rawPriority = optionalNonEmpty(values.flags.get("--priority"));
|
|
61
|
+
let priority;
|
|
62
|
+
if (rawPriority !== undefined) {
|
|
63
|
+
if (NONE(rawPriority))
|
|
64
|
+
priority = null;
|
|
65
|
+
else {
|
|
66
|
+
priority = Number(rawPriority);
|
|
67
|
+
if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
|
|
68
|
+
throw new Error("--priority must be 0-4 (0 none, 1 urgent, 2 high, 3 medium, 4 low), or the word none to clear it.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const rawSize = optionalNonEmpty(values.flags.get("--size"));
|
|
73
|
+
const estimate = rawSize === undefined ? undefined : NONE(rawSize) ? null : estimateForSize(rawSize);
|
|
74
|
+
const rawProject = optionalNonEmpty(values.flags.get("--project"));
|
|
75
|
+
const labelValues = (values.flagValues.get("--label") ?? []).map((v) => v.trim()).filter(Boolean);
|
|
76
|
+
const labels = labelValues.length === 1 && NONE(labelValues[0]) ? [] : labelValues;
|
|
77
|
+
const assignee = optionalNonEmpty(values.flags.get("--assignee"));
|
|
78
|
+
if (assignee !== undefined && assignee !== "me" && !UUID.test(assignee))
|
|
79
|
+
throw new Error('--assignee must be "me" or a person\'s uuid.');
|
|
80
|
+
const due = optionalNonEmpty(values.flags.get("--due"));
|
|
81
|
+
if (due && !NONE(due) && !/^\d{4}-\d{2}-\d{2}$/.test(due))
|
|
82
|
+
throw new Error("--due must be YYYY-MM-DD (or none).");
|
|
83
|
+
const expectedUpdatedAt = optionalNonEmpty(values.flags.get("--expected-updated-at"));
|
|
84
|
+
if (expectedUpdatedAt && Number.isNaN(Date.parse(expectedUpdatedAt))) {
|
|
85
|
+
throw new Error("--expected-updated-at must be the updated_at timestamp you last read.");
|
|
86
|
+
}
|
|
87
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
88
|
+
if (requestId && !UUID.test(requestId))
|
|
89
|
+
throw new Error("--request-id must be a UUID.");
|
|
90
|
+
const changes = rawDescription !== undefined || rawState !== undefined || priority !== undefined || estimate !== undefined
|
|
91
|
+
|| rawProject !== undefined || labelValues.length > 0 || name !== undefined;
|
|
92
|
+
if (verb === "update" && !changes && !given("--archive") && !given("--restore")) {
|
|
93
|
+
throw new Error("ticket templates update needs a field to change: --name, --description, --state, --priority, --size, --project, --label, --archive or --restore.");
|
|
94
|
+
}
|
|
95
|
+
const templates = {
|
|
96
|
+
verb,
|
|
97
|
+
...(needsRef ? { ref: positional[0] } : {}),
|
|
98
|
+
...(name ? { name } : {}),
|
|
99
|
+
...(rawDescription === undefined ? {} : { description: NONE(rawDescription) ? null : rawDescription }),
|
|
100
|
+
...(rawState === undefined ? {} : { state: NONE(rawState) ? null : rawState }),
|
|
101
|
+
...(priority === undefined ? {} : { priority }),
|
|
102
|
+
...(estimate === undefined ? {} : { estimate }),
|
|
103
|
+
...(rawProject === undefined ? {} : { project: NONE(rawProject) ? null : rawProject }),
|
|
104
|
+
...(labelValues.length > 0 ? { labels } : {}),
|
|
105
|
+
...(optionalNonEmpty(values.flags.get("--title")) ? { title: optionalNonEmpty(values.flags.get("--title")) } : {}),
|
|
106
|
+
...(assignee ? { assignee } : {}),
|
|
107
|
+
...(optionalNonEmpty(values.flags.get("--parent")) ? { parentRef: optionalNonEmpty(values.flags.get("--parent")) } : {}),
|
|
108
|
+
...(due === undefined ? {} : { due: NONE(due) ? null : due }),
|
|
109
|
+
...(given("--archive") ? { archived: true } : given("--restore") ? { archived: false } : {}),
|
|
110
|
+
...(given("--include-archived") ? { includeArchived: true } : {}),
|
|
111
|
+
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
kind: "issue",
|
|
115
|
+
action: "templates",
|
|
116
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
117
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
118
|
+
...(requestId ? { requestId } : {}),
|
|
119
|
+
json: values.booleans.has("--json"),
|
|
120
|
+
templates,
|
|
121
|
+
};
|
|
122
|
+
}
|