@bli-cockpit/cli 0.2.237 → 0.2.238
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/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +36 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +68 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +52 -50
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +55 -45
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-work.js +52 -33
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +6 -2
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +91 -67
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +2 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `cockpit manager home` prints a client's Home, and the Today line `list --timeline` prints (BLI-5698).
|
|
3
|
+
*
|
|
4
|
+
* The words are the door's own (a headline, a bullet, a title); this file lays them out in the order the
|
|
5
|
+
* Home tab reads: how fresh the data is, "Where we are" with the live counts beside it, what needs attention
|
|
6
|
+
* by owner, what is coming up, what the newest read that changed anything did, and the review count. Never the brief: it is the
|
|
7
|
+
* AI's working notes, kept inside the manager server. "Today" is Vancouver's day, as the manager server counts it.
|
|
8
|
+
*/
|
|
9
|
+
import { dim } from "./cli-io.js";
|
|
10
|
+
/** The manager's day (`TOWER_MANAGER_TIMEZONE`): a task due today is not overdue at 5 pm Pacific. */
|
|
11
|
+
export const MANAGER_TIMEZONE = "America/Vancouver";
|
|
12
|
+
const HEALTH = { on_track: "On track", at_risk: "At risk", blocked: "Blocked" };
|
|
13
|
+
const FRESHNESS = {
|
|
14
|
+
reading: "being read now",
|
|
15
|
+
never_read: "never read yet",
|
|
16
|
+
failed: "the latest read failed",
|
|
17
|
+
stale: "may be out of date (no good read in the last 36 hours)",
|
|
18
|
+
fresh: "fresh",
|
|
19
|
+
};
|
|
20
|
+
const MISSING = {
|
|
21
|
+
never_checked: "Not written yet: the next read writes it.",
|
|
22
|
+
check_failed: "Not written yet: the last check failed; the next read tries again.",
|
|
23
|
+
nothing_to_summarise: "Nothing to summarise yet: Tower has no open actions or timeline for this client.",
|
|
24
|
+
};
|
|
25
|
+
const CHANGE = { added: "added", changed: "changed", proposal: "proposed", closed: "closed" };
|
|
26
|
+
const plural = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
27
|
+
/** `YYYY-MM-DD` of Vancouver's day for an instant. */
|
|
28
|
+
export function managerDay(at) {
|
|
29
|
+
return new Intl.DateTimeFormat("en-CA", { timeZone: MANAGER_TIMEZONE, year: "numeric", month: "2-digit", day: "2-digit" }).format(at);
|
|
30
|
+
}
|
|
31
|
+
/** "Oct 16" for an instant, on Vancouver's calendar. */
|
|
32
|
+
function shortDate(at) {
|
|
33
|
+
const date = new Date(at);
|
|
34
|
+
if (Number.isNaN(date.getTime()))
|
|
35
|
+
return at;
|
|
36
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: MANAGER_TIMEZONE, month: "short", day: "numeric" }).format(date);
|
|
37
|
+
}
|
|
38
|
+
/** "3 h ago", "2 d ago", "just now": how old an instant is. */
|
|
39
|
+
export function ageText(at, now) {
|
|
40
|
+
const ms = now.getTime() - new Date(at).getTime();
|
|
41
|
+
if (!Number.isFinite(ms))
|
|
42
|
+
return "at an unknown time";
|
|
43
|
+
const minutes = Math.floor(Math.max(0, ms) / 60_000);
|
|
44
|
+
if (minutes < 1)
|
|
45
|
+
return "just now";
|
|
46
|
+
if (minutes < 60)
|
|
47
|
+
return `${minutes} min ago`;
|
|
48
|
+
const hours = Math.floor(minutes / 60);
|
|
49
|
+
if (hours < 48)
|
|
50
|
+
return `${hours} h ago`;
|
|
51
|
+
return `${Math.floor(hours / 24)} d ago`;
|
|
52
|
+
}
|
|
53
|
+
const when = (at, now) => `${shortDate(at)} (${ageText(at, now)})`;
|
|
54
|
+
/** `── Today · Wed Oct 15 ──`, for the `YYYY-MM-DD` day given. */
|
|
55
|
+
export function todayLine(day) {
|
|
56
|
+
const [year, month, date] = day.split("-").map(Number);
|
|
57
|
+
const noon = new Date(Date.UTC(year, month - 1, date, 12));
|
|
58
|
+
const words = new Intl.DateTimeFormat("en-US", { timeZone: "UTC", weekday: "short", month: "short", day: "numeric" }).format(noon).replace(",", "");
|
|
59
|
+
return `── Today · ${words} ──`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The timeline's rows with the Today line in place. The server sends undated, then future (latest first),
|
|
63
|
+
* then today and the past: the line goes before the first item dated today or earlier, or last when none is.
|
|
64
|
+
*/
|
|
65
|
+
export function withTodayLine(rows, dates, today) {
|
|
66
|
+
const at = dates.findIndex((date) => typeof date === "string" && date.slice(0, 10) <= today);
|
|
67
|
+
const line = [todayLine(today)];
|
|
68
|
+
return at === -1 ? [...rows, line] : [...rows.slice(0, at), line, ...rows.slice(at)];
|
|
69
|
+
}
|
|
70
|
+
/** Who wrote the card and when: "written by AI", "computed from counts", or "edited by" a person. */
|
|
71
|
+
function authorLine(summary, now) {
|
|
72
|
+
const at = when(summary.writtenAt, now);
|
|
73
|
+
if (summary.source === "model")
|
|
74
|
+
return `Written by AI, ${at}.`;
|
|
75
|
+
if (summary.source === "computed")
|
|
76
|
+
return `Computed from counts, ${at} (the model could not write it).`;
|
|
77
|
+
// Never a user id in a person's terminal: the door fills the name, or says "a teammate".
|
|
78
|
+
return `Edited by ${summary.editedByName ?? "a teammate"}, ${at}.`;
|
|
79
|
+
}
|
|
80
|
+
const CAP_WORDS = {
|
|
81
|
+
max_depth: "depth cap",
|
|
82
|
+
max_pages: "page cap",
|
|
83
|
+
max_listings: "a page too long to search for child pages",
|
|
84
|
+
read_failed: "a page Notion did not return",
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* What the last read could not see, one sentence each, worded as the page's client bar words them
|
|
88
|
+
* (`readWarnings`, `manager-client-bar.tsx`), and the pages left for the next read unless one is
|
|
89
|
+
* reading or queued now. Empty when it saw and read everything, or the server is older.
|
|
90
|
+
*/
|
|
91
|
+
export function coverageLines(client) {
|
|
92
|
+
const walk = client.lastRead?.walk;
|
|
93
|
+
const idle = !client.running && !client.queued;
|
|
94
|
+
const incomplete = client.incompletePages ?? 0;
|
|
95
|
+
const waiting = client.pagesWaiting ?? 0;
|
|
96
|
+
return [
|
|
97
|
+
walk && !walk.complete ? `Walk incomplete (${walk.cap ? CAP_WORDS[walk.cap] : "stopped early"}): archiving skipped.` : null,
|
|
98
|
+
walk && walk.pageReadFailed > 0 ? `${plural(walk.pageReadFailed, "page")} could not be read this time; the next sync tries again.` : null,
|
|
99
|
+
walk && walk.unreadableSkipped > 0 ? `${plural(walk.unreadableSkipped, "page")} not shared with the Tower integration.` : null,
|
|
100
|
+
walk && walk.otherClientRoots > 0 ? `Stopped at ${walk.otherClientRoots} other ${walk.otherClientRoots === 1 ? "client's tree" : "clients' trees"}.` : null,
|
|
101
|
+
walk && walk.otherClientSkipped > 0 ? `${plural(walk.otherClientSkipped, "page")} held by another client.` : null,
|
|
102
|
+
incomplete > 0 ? `Notion handed over ${plural(incomplete, "page")} only in part.` : null,
|
|
103
|
+
idle && waiting > 0 ? `${plural(waiting, "page")} still waiting for the next read.` : null,
|
|
104
|
+
].filter((line) => line !== null);
|
|
105
|
+
}
|
|
106
|
+
function freshnessLines(home, now) {
|
|
107
|
+
const read = home.client.lastRead;
|
|
108
|
+
const synced = home.client.lastSyncedAt;
|
|
109
|
+
// A failed latest read names its reason, and the data shown is as old as the last good read (availability over completeness).
|
|
110
|
+
const failed = read && home.freshness.state === "failed"
|
|
111
|
+
? `failed ${when(read.finishedAt ?? read.startedAt, now)}${read.reason ? `: ${read.reason}` : ""}; ${synced ? `last good read ${when(synced, now)}` : "no good read yet"}`
|
|
112
|
+
: null;
|
|
113
|
+
const lastRead = failed ?? (read ? `last read ${when(read.finishedAt ?? read.startedAt, now)}${read.finishedAt ? "" : ", still running"}` : synced ? `last synced ${when(synced, now)}` : "never read");
|
|
114
|
+
const notion = home.freshness.notionLastEditedAt ? `Notion last changed ${when(home.freshness.notionLastEditedAt, now)}` : "no Notion page yet";
|
|
115
|
+
// What the read could not see sits under the Data line, so "fresh" never hides it.
|
|
116
|
+
return [`Data: ${FRESHNESS[home.freshness.state] ?? home.freshness.state}. ${lastRead[0].toUpperCase()}${lastRead.slice(1)}; ${notion}.`, ...coverageLines(home.client).map((line) => ` ${line}`)];
|
|
117
|
+
}
|
|
118
|
+
function summaryLines(home, now, color) {
|
|
119
|
+
const c = home.counts;
|
|
120
|
+
// The page's numbers beside the card: due this week is what needs attention beyond the overdue ones, and "to review" is
|
|
121
|
+
// the review queue's size (`review.total`), not `counts.toReview` (which also counts the first read's items).
|
|
122
|
+
const dueThisWeek = Math.max(0, home.needsAttention.total - c.overdue);
|
|
123
|
+
const counts = ` Live: ${plural(c.openActions, "open action")} (${c.overdue} overdue, ${dueThisWeek} due this week), ${plural(c.timeline, "timeline item")}, ${home.review.total} to review.`;
|
|
124
|
+
const summary = home.summary;
|
|
125
|
+
if (!summary)
|
|
126
|
+
return [`Where we are: ${MISSING[home.summaryMissingReason ?? "never_checked"]}`, counts];
|
|
127
|
+
const lines = [`Where we are: ${HEALTH[summary.health] ?? summary.health} — ${summary.headline}`];
|
|
128
|
+
for (const bullet of summary.bullets)
|
|
129
|
+
lines.push(` ${bullet.date ?? "(no date)"} ${bullet.text}`);
|
|
130
|
+
if (summary.next)
|
|
131
|
+
lines.push(` Next: ${summary.next}`);
|
|
132
|
+
if (summary.blocker)
|
|
133
|
+
lines.push(` Blocker: ${summary.blocker}`);
|
|
134
|
+
lines.push(` ${authorLine(summary, now)}`);
|
|
135
|
+
if (summary.inputsChanged)
|
|
136
|
+
lines.push(" Inputs changed since: the client's data changed or an action went overdue (for an AI-written card, also a meeting or milestone came within a week or passed) after this was written; the next check writes it again.");
|
|
137
|
+
lines.push(counts, dim(` version ${summary.updatedAt} (what \`summary --version\` takes)`, color));
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
function attentionLines(home, color) {
|
|
141
|
+
const attention = home.needsAttention;
|
|
142
|
+
const head = `Needs attention: ${plural(attention.total, "open action")} overdue or due within 7 days.${attention.truncated ? ` Showing ${attention.items.length} of ${attention.total}.` : ""}`;
|
|
143
|
+
const lines = [head];
|
|
144
|
+
const byId = new Map(attention.items.map((item) => [item.id, item]));
|
|
145
|
+
for (const group of attention.byOwner) {
|
|
146
|
+
lines.push(` ${group.owner ?? "No owner"}`);
|
|
147
|
+
for (const id of group.actionIds) {
|
|
148
|
+
const item = byId.get(id);
|
|
149
|
+
if (item)
|
|
150
|
+
lines.push(` ${item.dueAt ? `due ${item.dueAt}` : "no due date"}${item.overdue ? " (overdue)" : ""} ${item.title} ${dim(`id ${item.id}`, color)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (attention.undated > 0)
|
|
154
|
+
lines.push(` ${plural(attention.undated, "open action")} ${attention.undated === 1 ? "has" : "have"} no due date (\`list <client>\` shows them).`);
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
function comingUpLines(home) {
|
|
158
|
+
const coming = home.comingUp;
|
|
159
|
+
const lines = [`Coming up: ${`${coming.total} ${coming.total === 1 ? "meeting or milestone" : "meetings or milestones"}`} dated today or later.${coming.truncated ? ` Showing ${coming.items.length} of ${coming.total}.` : ""}`];
|
|
160
|
+
for (const item of coming.items)
|
|
161
|
+
lines.push(` ${item.occurredAt ?? "(no date)"} ${item.kind ?? "item"} ${item.title}`);
|
|
162
|
+
if (coming.undated > 0)
|
|
163
|
+
lines.push(` ${coming.undated} more ${coming.undated === 1 ? "has" : "have"} no date ("Needs a date").`);
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
|
166
|
+
function changedLines(home, now) {
|
|
167
|
+
const changed = home.whatChanged;
|
|
168
|
+
if (!changed.read)
|
|
169
|
+
return ["What changed: no read has changed anything yet."];
|
|
170
|
+
const counts = Object.keys(CHANGE).map((kind) => `${changed.counts[kind] ?? 0} ${CHANGE[kind]}`).join(", ");
|
|
171
|
+
const total = Object.values(changed.counts).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
172
|
+
const lines = [`What changed in the newest read that changed anything (${when(changed.read.finishedAt ?? changed.read.startedAt, now)}): ${counts}.${changed.truncated ? ` Showing ${changed.items.length} of ${total}.` : ""}`];
|
|
173
|
+
for (const item of changed.items)
|
|
174
|
+
lines.push(` ${CHANGE[item.change] ?? item.change} ${item.table} ${item.title}`);
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
/** The whole Home, top to bottom, for a person. `clientRef` is how they named the client, for the commands it suggests. */
|
|
178
|
+
export function homeLines(home, clientRef, color, now = new Date()) {
|
|
179
|
+
const ref = /\s/.test(clientRef) ? `"${clientRef}"` : clientRef;
|
|
180
|
+
const review = home.review.total;
|
|
181
|
+
return [
|
|
182
|
+
`${home.client.name}: Home (today ${home.today})`,
|
|
183
|
+
...freshnessLines(home, now),
|
|
184
|
+
"",
|
|
185
|
+
...summaryLines(home, now, color),
|
|
186
|
+
"",
|
|
187
|
+
...attentionLines(home, color),
|
|
188
|
+
"",
|
|
189
|
+
...comingUpLines(home),
|
|
190
|
+
"",
|
|
191
|
+
...changedLines(home, now),
|
|
192
|
+
"",
|
|
193
|
+
review === 0 ? "Review: no sync change waits for review." : `Review: ${plural(review, "sync change")} ${review === 1 ? "waits" : "wait"} for review: \`cockpit manager queue ${ref}\`.`,
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
/** What a summary edit printed: the card as written. */
|
|
197
|
+
export function summaryWrittenLines(summary, color, now = new Date()) {
|
|
198
|
+
const lines = [`Saved "Where we are": ${HEALTH[summary.health] ?? summary.health} — ${summary.headline}`];
|
|
199
|
+
for (const bullet of summary.bullets)
|
|
200
|
+
lines.push(` ${bullet.date ?? "(no date)"} ${bullet.text}`);
|
|
201
|
+
if (summary.next)
|
|
202
|
+
lines.push(` Next: ${summary.next}`);
|
|
203
|
+
if (summary.blocker)
|
|
204
|
+
lines.push(` Blocker: ${summary.blocker}`);
|
|
205
|
+
lines.push(` ${authorLine(summary, now)} It stands until the client's data changes or an action goes overdue; the next check after that writes it again.`);
|
|
206
|
+
lines.push(dim(` version ${summary.updatedAt}`, color));
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* change nobody read. Readable, not exhaustive: `--json` is the whole answer and never comes through here.
|
|
8
8
|
*/
|
|
9
9
|
import { dim } from "./cli-io.js";
|
|
10
|
+
import { managerDay, withTodayLine } from "./manager-render-home.js";
|
|
10
11
|
const QUEUE_REASON = {
|
|
11
12
|
added: "added by a sync, not looked at yet",
|
|
12
13
|
changed: "changed by a sync since you last saw it",
|
|
@@ -56,7 +57,8 @@ export function itemRowLines(item, timeline, color) {
|
|
|
56
57
|
: [ownersText(item, false), whenText(item, false), item.status ?? "open"];
|
|
57
58
|
return [`${item.title} [${review}]`, ` ${facts.join(" | ")}`, dim(` id ${item.id} version ${item.updatedAt}`, color)];
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
/** A list for a person; a timeline list carries the Today line between upcoming and past (Vancouver's day). */
|
|
61
|
+
export function listLines(list, timeline, color, now = new Date()) {
|
|
60
62
|
const noun = timeline ? "timeline item" : "action";
|
|
61
63
|
const shown = list.items.length;
|
|
62
64
|
const head = shown < list.total ? `Showing ${shown} of ${list.total} ${noun}s.` : `${list.total} ${noun}${list.total === 1 ? "" : "s"}.`;
|
|
@@ -65,8 +67,10 @@ export function listLines(list, timeline, color) {
|
|
|
65
67
|
const lines = [`${head}${cut}`, `The client has ${c.openActions} open ${c.openActions === 1 ? "action" : "actions"} (${c.overdue} overdue) and ${c.toReview} ${c.toReview === 1 ? "item" : "items"} to review.`];
|
|
66
68
|
if (shown === 0)
|
|
67
69
|
lines.push(`No ${noun}s match.`);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const rows = list.items.map((item) => itemRowLines(item, timeline, color));
|
|
71
|
+
const placed = timeline && shown > 0 ? withTodayLine(rows, list.items.map((item) => item.occurredAt), managerDay(now)) : rows;
|
|
72
|
+
for (const row of placed)
|
|
73
|
+
lines.push("", ...row);
|
|
70
74
|
return lines;
|
|
71
75
|
}
|
|
72
76
|
export function queueLines(queue, color) {
|
|
@@ -10,6 +10,22 @@ export function planManagerRequest(command, clientId, newRequestId) {
|
|
|
10
10
|
return { method: "GET", path: `${BASE}/clients`, logFields: (a) => ({ clients: arrayLength(a, "clients") }) };
|
|
11
11
|
case "client":
|
|
12
12
|
return { method: "GET", path: `${BASE}/clients/${enc(clientId ?? "")}`, logFields: () => ({}) };
|
|
13
|
+
case "home":
|
|
14
|
+
return { method: "GET", path: `${BASE}/clients/${enc(clientId ?? "")}/home`, logFields: homeLog };
|
|
15
|
+
case "summary": {
|
|
16
|
+
const fields = { expectedUpdatedAt: command.version ?? null };
|
|
17
|
+
if (command.health !== undefined)
|
|
18
|
+
fields["health"] = command.health;
|
|
19
|
+
if (command.headline !== undefined)
|
|
20
|
+
fields["headline"] = command.headline;
|
|
21
|
+
if (command.bullets !== undefined)
|
|
22
|
+
fields["bullets"] = command.bullets;
|
|
23
|
+
if (command.next !== undefined)
|
|
24
|
+
fields["next"] = command.next;
|
|
25
|
+
if (command.blocker !== undefined)
|
|
26
|
+
fields["blocker"] = command.blocker;
|
|
27
|
+
return { method: "PATCH", path: `${BASE}/clients/${enc(clientId ?? "")}/summary`, body: fields, logFields: (a) => ({ fields: Object.keys(fields).filter((key) => key !== "expectedUpdatedAt"), source: stringField(a, "source"), bullets: arrayLength(a, "bullets") }) };
|
|
28
|
+
}
|
|
13
29
|
case "run":
|
|
14
30
|
return { method: "GET", path: `${BASE}/runs/${enc(command.runId ?? "")}`, logFields: () => ({}) };
|
|
15
31
|
case "list":
|
|
@@ -90,6 +106,23 @@ function itemLog(table) {
|
|
|
90
106
|
return { table, item_id: item?.id ?? null, review_status: item?.reviewStatus ?? null };
|
|
91
107
|
};
|
|
92
108
|
}
|
|
109
|
+
/** Home's log line: which parts it holds and their sizes, never their words. */
|
|
110
|
+
function homeLog(answer) {
|
|
111
|
+
const home = answer && typeof answer === "object" ? answer : {};
|
|
112
|
+
const part = (key) => (home[key] && typeof home[key] === "object" ? home[key] : undefined);
|
|
113
|
+
return {
|
|
114
|
+
summary_source: stringField(part("summary"), "source"),
|
|
115
|
+
summary_missing: typeof home["summaryMissingReason"] === "string" ? home["summaryMissingReason"] : null,
|
|
116
|
+
freshness: stringField(part("freshness"), "state"),
|
|
117
|
+
needs_attention: numberField(part("needsAttention"), "total"),
|
|
118
|
+
coming_up: numberField(part("comingUp"), "total"),
|
|
119
|
+
review: numberField(part("review"), "total"),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function stringField(answer, key) {
|
|
123
|
+
const value = answer && typeof answer === "object" ? answer[key] : undefined;
|
|
124
|
+
return typeof value === "string" ? value : null;
|
|
125
|
+
}
|
|
93
126
|
function arrayLength(answer, key) {
|
|
94
127
|
const value = answer && typeof answer === "object" ? answer[key] : undefined;
|
|
95
128
|
return Array.isArray(value) ? value.length : null;
|
package/dist/commands/manager.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* them, adds and edits them, ticks them off, links an action to the meeting it came from, and
|
|
6
6
|
* decides the AI's suggestions, the same things the manager page does.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Eighteen verbs (`home` and `summary`, BLI-5698: a client's Home and its "Where we are" card), each one `/api/manager` door on this machine's device token, the same door the MCP
|
|
9
9
|
* twins (`manager_*`) use; the door passes Railway's answer through unchanged. Every write on an
|
|
10
10
|
* existing item carries `--version`, the item's `updatedAt` as `list`/`show` print it: a stale one is
|
|
11
11
|
* refused with the door's own sentence and nothing is written. `approve`, `dismiss` and `proposal`
|
|
@@ -17,6 +17,7 @@ import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./age
|
|
|
17
17
|
import { colorEnabled, writeLine } from "./cli-io.js";
|
|
18
18
|
import { localCommandHelp } from "./local-help.js";
|
|
19
19
|
import { clientDetailLines, clientLines, itemDetailLines, itemRowLines, runLines, listLines, queueLines, } from "./manager-render.js";
|
|
20
|
+
import { homeLines, summaryWrittenLines } from "./manager-render-home.js";
|
|
20
21
|
import { planManagerRequest } from "./manager-requests.js";
|
|
21
22
|
const TAG = "[manager cli]";
|
|
22
23
|
/**
|
|
@@ -28,7 +29,7 @@ export const MANAGER_DEADLINE_MS = 65_000;
|
|
|
28
29
|
const staleHint = (id, timeline) => `Run \`cockpit manager show ${id}${timeline ? " --timeline" : ""}\` for the current version and try again.`;
|
|
29
30
|
/** Refusals that mean "the item is not as you last saw it": reading it again is the way forward. */
|
|
30
31
|
/** The verbs that change an existing item. */
|
|
31
|
-
const WRITES = new Set(["edit", "done", "reopen", "link", "approve", "dismiss", "proposal", "archive", "unarchive"]);
|
|
32
|
+
const WRITES = new Set(["summary", "edit", "done", "reopen", "link", "approve", "dismiss", "proposal", "archive", "unarchive"]);
|
|
32
33
|
const RESEEN_REFUSALS = new Set(["item_changed", "item_busy", "proposal_decided", "no_proposal"]);
|
|
33
34
|
export async function runManager(command, io) {
|
|
34
35
|
if (command.help) {
|
|
@@ -103,6 +104,8 @@ function renderAnswer(command, body, color) {
|
|
|
103
104
|
switch (command.action) {
|
|
104
105
|
case "clients": return clientLines(body.clients, color);
|
|
105
106
|
case "client": return clientDetailLines(body, color);
|
|
107
|
+
case "home": return homeLines(body, command.client ?? "<client>", color);
|
|
108
|
+
case "summary": return summaryWrittenLines(body, color);
|
|
106
109
|
case "run": return runLines(body);
|
|
107
110
|
case "list": return listLines(body, timeline, color);
|
|
108
111
|
case "queue": return queueLines(body, color);
|
|
@@ -161,12 +164,17 @@ async function resolveClient(door, ref) {
|
|
|
161
164
|
/** What to do next after a refusal that is not "read it again", so every refusal ends with a way forward. */
|
|
162
165
|
function nextStep(reason, refusal, command) {
|
|
163
166
|
const id = command?.id ?? "<id>";
|
|
167
|
+
if (refusal?.startsWith("summary_") && command)
|
|
168
|
+
return summaryHint(refusal, command);
|
|
164
169
|
const flag = command?.timeline ? " --timeline" : "";
|
|
165
170
|
if (reason === "not_found" && command?.id) {
|
|
166
171
|
return command.timeline
|
|
167
172
|
? " Check the id with `cockpit manager list <client> --timeline`; for an action, drop --timeline."
|
|
168
173
|
: ` Check the id with \`cockpit manager list <client>\`; for a timeline item, add --timeline${command.action === "done" || command.action === "reopen" || command.action === "link" ? " (done, reopen and link are for actions only)" : ""}.`;
|
|
169
174
|
}
|
|
175
|
+
// A client-level command (home, summary, client, list, queue, add) the door cannot find the client of: "That client does not exist".
|
|
176
|
+
if (reason === "not_found" && !command?.id && command?.action !== "run")
|
|
177
|
+
return " Check it with `cockpit manager clients`.";
|
|
170
178
|
// The door's key check (no such key, or a revoked one): this machine needs signing in again.
|
|
171
179
|
if (reason === "http_401")
|
|
172
180
|
return " Sign this machine in to Tower again with `cockpit login`, then retry.";
|
|
@@ -201,11 +209,37 @@ function refuse(door, answer, command, requestId) {
|
|
|
201
209
|
const noAnswer = answer.httpStatus === undefined || answer.httpStatus >= 500;
|
|
202
210
|
// A change to an existing item with no answer may or may not have landed: look before trying again.
|
|
203
211
|
const changedUnknown = !unconfirmed && noAnswer && command?.id !== undefined && WRITES.has(command.action);
|
|
212
|
+
// A card edit with no answer may or may not have landed: read the Home before trying again.
|
|
213
|
+
const summaryUnknown = noAnswer && command?.action === "summary";
|
|
204
214
|
const retry = unconfirmed
|
|
205
215
|
? ` Nothing was confirmed: Tower did not say whether the item was made. Rerun the same command with \`--request-id ${requestId}\` to retry without making a duplicate.`
|
|
206
|
-
:
|
|
207
|
-
? ` It may or may not have been saved: run \`cockpit manager
|
|
208
|
-
:
|
|
216
|
+
: summaryUnknown
|
|
217
|
+
? ` It may or may not have been saved: run \`cockpit manager home ${clientWord(command)} --json\` to see, and use its summary.updatedAt if you try again.`
|
|
218
|
+
: changedUnknown
|
|
219
|
+
? ` It may or may not have been saved: run \`cockpit manager show ${command.id}${command.timeline ? " --timeline" : ""}\` to see, and use the version it prints if you try again.`
|
|
220
|
+
: "";
|
|
209
221
|
const stop = hint || retry ? (/[.!?]$/.test(sentence) ? "" : ".") : "";
|
|
210
222
|
return failAgentDoor(door, TAG, answer.reason, `${sentence}${stop}${hint}${retry}`, { ...(refusal ? { refusal } : {}), ...(unconfirmed ? { requestId } : {}) });
|
|
223
|
+
}
|
|
224
|
+
const clientWord = (command) => (command.client && /\s/.test(command.client) ? `"${command.client}"` : command.client ?? "<client>");
|
|
225
|
+
/** The way forward after each named refusal of a "Where we are" edit (`TOWER_MANAGER_SUMMARY_REFUSALS`). */
|
|
226
|
+
function summaryHint(refusal, command) {
|
|
227
|
+
const home = `\`cockpit manager home ${clientWord(command)} --json\``;
|
|
228
|
+
const hints = {
|
|
229
|
+
summary_changed: `Someone or the check wrote the card since: read it again with ${home} and retry with its summary.updatedAt as --version (none only when it has no card).`,
|
|
230
|
+
summary_empty: "Give at least one of --health, --headline, --bullet, --next, --blocker.",
|
|
231
|
+
summary_incomplete: "This client has no card yet: give --health, --headline and at least one --bullet together (with --version none).",
|
|
232
|
+
summary_health_invalid: "--health is on_track, at_risk or blocked.",
|
|
233
|
+
summary_headline_empty: "Give the headline some words.",
|
|
234
|
+
summary_headline_too_long: "Keep --headline to 140 characters.",
|
|
235
|
+
summary_bullets_count: "Give 1 to 5 --bullet flags; together they replace the whole list.",
|
|
236
|
+
summary_bullet_empty: "Every --bullet needs words (after its date, when it has one).",
|
|
237
|
+
summary_bullet_too_long: "Keep each --bullet to 160 characters.",
|
|
238
|
+
summary_bullet_date_invalid: "Start a dated bullet with a real day, \"YYYY-MM-DD: \", or leave the date out.",
|
|
239
|
+
summary_next_too_long: "Keep --next to 200 characters.",
|
|
240
|
+
summary_blocker_too_long: "Keep --blocker to 200 characters.",
|
|
241
|
+
summary_blocked_needs_blocker: "blocked needs a blocker: add --blocker <what blocks it>, or pick another --health.",
|
|
242
|
+
};
|
|
243
|
+
const hint = hints[refusal];
|
|
244
|
+
return hint ? ` ${hint}` : ` Read the card with ${home} and try again.`;
|
|
211
245
|
}
|
|
@@ -49,19 +49,29 @@ export async function runMemoryConsolidation(command, door) {
|
|
|
49
49
|
if (door.json)
|
|
50
50
|
return emitAgentDoor(door, answer.body);
|
|
51
51
|
if (runs.length === 0) {
|
|
52
|
-
|
|
53
|
-
? "No consolidation pass has been recorded yet. The pass is a nightly Railway cron; nothing has run, or nothing has changed."
|
|
54
|
-
: "No consolidation runs are visible to this account. The receipts are readable by a super_admin; this is the access rule answering, not an outage.");
|
|
52
|
+
writeNoRunsVisible(door, body.role);
|
|
55
53
|
return 0;
|
|
56
54
|
}
|
|
57
55
|
if (body.night && body.nightLine) {
|
|
58
|
-
|
|
59
|
-
writeLine(door.io.stdout, body.nightLine);
|
|
60
|
-
writeLine(door.io.stdout, nightTable(body.nightScopes ?? []));
|
|
61
|
-
writeLine(door.io.stdout, "");
|
|
62
|
-
writeLine(door.io.stdout, "merged/rewritten/corrected are what an apply run wrote, or what a report run only proposed (mode column). kept is always a proposal. Nothing is ever deleted. `--json` has every run.");
|
|
56
|
+
writeLastNight(door, body.nightLine, body.nightScopes ?? []);
|
|
63
57
|
return 0;
|
|
64
58
|
}
|
|
59
|
+
writeRunList(door, runs);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
function writeNoRunsVisible(door, role) {
|
|
63
|
+
writeLine(door.io.stdout, role === "super_admin"
|
|
64
|
+
? "No consolidation pass has been recorded yet. The pass is a nightly Railway cron; nothing has run, or nothing has changed."
|
|
65
|
+
: "No consolidation runs are visible to this account. The receipts are readable by a super_admin; this is the access rule answering, not an outage.");
|
|
66
|
+
}
|
|
67
|
+
/** Last night first (BLI-13376): one line for the whole night, then a row per scope. */
|
|
68
|
+
function writeLastNight(door, nightLine, scopes) {
|
|
69
|
+
writeLine(door.io.stdout, nightLine);
|
|
70
|
+
writeLine(door.io.stdout, nightTable(scopes));
|
|
71
|
+
writeLine(door.io.stdout, "");
|
|
72
|
+
writeLine(door.io.stdout, "merged/rewritten/corrected are what an apply run wrote, or what a report run only proposed (mode column). kept is always a proposal. Nothing is ever deleted. `--json` has every run.");
|
|
73
|
+
}
|
|
74
|
+
function writeRunList(door, runs) {
|
|
65
75
|
for (const run of runs) {
|
|
66
76
|
writeLine(door.io.stdout, `${run.containerTag ?? "(no tag)"} ${run.mode ?? "?"}`);
|
|
67
77
|
writeLine(door.io.stdout, ` ${run.startedAt ?? "?"} -> ${run.finishedAt ?? "unfinished"} ${run.reason ?? "no reason recorded"}`);
|
|
@@ -74,7 +84,6 @@ export async function runMemoryConsolidation(command, door) {
|
|
|
74
84
|
}
|
|
75
85
|
writeLine(door.io.stdout, "");
|
|
76
86
|
writeLine(door.io.stdout, "report = proposals only, nothing on the shelf changed. apply = the accepted edits were written as supersessions; nothing is ever deleted.");
|
|
77
|
-
return 0;
|
|
78
87
|
}
|
|
79
88
|
/** `merge 2, rewrite 1` out of a verb-count object. Never a sentence. */
|
|
80
89
|
function countsOf(value) {
|
|
@@ -24,6 +24,39 @@ import os from "node:os";
|
|
|
24
24
|
import { MEMORY_DAEMON_MAX_LINE_BYTES, MEMORY_DAEMON_PROTOCOL_VERSION, memoryDaemonEndpoint, } from "@bli-cockpit/telemetry-core";
|
|
25
25
|
/** A probe waits this long in total. A live local socket answers in ~1 ms. */
|
|
26
26
|
export const DAEMON_PROBE_TIMEOUT_MS = 400;
|
|
27
|
+
function socketErrorReason(code) {
|
|
28
|
+
return code === "ENOENT" || code === "ECONNREFUSED"
|
|
29
|
+
? "daemon_absent"
|
|
30
|
+
: `daemon_socket_${(code ?? "error").toLowerCase()}`;
|
|
31
|
+
}
|
|
32
|
+
/** What the daemon's reply says so far; undefined while its one line is still arriving. */
|
|
33
|
+
function judgeDaemonReply(buffered) {
|
|
34
|
+
if (buffered.length > MEMORY_DAEMON_MAX_LINE_BYTES) {
|
|
35
|
+
return { answering: false, reason: "daemon_answer_too_large" };
|
|
36
|
+
}
|
|
37
|
+
const newline = buffered.indexOf("\n");
|
|
38
|
+
if (newline === -1)
|
|
39
|
+
return undefined;
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(buffered.slice(0, newline));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { answering: false, reason: "daemon_answer_unparseable" };
|
|
46
|
+
}
|
|
47
|
+
const record = (parsed ?? {});
|
|
48
|
+
if (record["ok"] !== true) {
|
|
49
|
+
return { answering: false, reason: typeof record["reason"] === "string" ? record["reason"] : "daemon_refused" };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
answering: true,
|
|
53
|
+
reason: "answering",
|
|
54
|
+
extra: {
|
|
55
|
+
pid: typeof record["pid"] === "number" ? record["pid"] : null,
|
|
56
|
+
startedAt: typeof record["startedAt"] === "string" ? record["startedAt"] : null,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
27
60
|
export function probeMemoryDaemon(options = {}) {
|
|
28
61
|
const now = options.now ?? Date.now;
|
|
29
62
|
const started = now();
|
|
@@ -56,40 +89,16 @@ export function probeMemoryDaemon(options = {}) {
|
|
|
56
89
|
timer.unref?.();
|
|
57
90
|
const socket = (options.connectImpl ?? ((address) => net.createConnection({ path: address })))(endpoint.address);
|
|
58
91
|
socket.on("error", (error) => {
|
|
59
|
-
|
|
60
|
-
finish(false, code === "ENOENT" || code === "ECONNREFUSED"
|
|
61
|
-
? "daemon_absent"
|
|
62
|
-
: `daemon_socket_${(code ?? "error").toLowerCase()}`);
|
|
92
|
+
finish(false, socketErrorReason(error.code));
|
|
63
93
|
});
|
|
64
94
|
socket.on("connect", () => {
|
|
65
95
|
socket.write(`${JSON.stringify({ v: MEMORY_DAEMON_PROTOCOL_VERSION, event: "ping" })}\n`);
|
|
66
96
|
});
|
|
67
97
|
socket.on("data", (chunk) => {
|
|
68
98
|
buffered += chunk.toString("utf8");
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
const newline = buffered.indexOf("\n");
|
|
74
|
-
if (newline === -1)
|
|
75
|
-
return;
|
|
76
|
-
let parsed;
|
|
77
|
-
try {
|
|
78
|
-
parsed = JSON.parse(buffered.slice(0, newline));
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
finish(false, "daemon_answer_unparseable");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const record = (parsed ?? {});
|
|
85
|
-
if (record["ok"] !== true) {
|
|
86
|
-
finish(false, typeof record["reason"] === "string" ? record["reason"] : "daemon_refused");
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
finish(true, "answering", {
|
|
90
|
-
pid: typeof record["pid"] === "number" ? record["pid"] : null,
|
|
91
|
-
startedAt: typeof record["startedAt"] === "string" ? record["startedAt"] : null,
|
|
92
|
-
});
|
|
99
|
+
const verdict = judgeDaemonReply(buffered);
|
|
100
|
+
if (verdict)
|
|
101
|
+
finish(verdict.answering, verdict.reason, verdict.extra);
|
|
93
102
|
});
|
|
94
103
|
socket.on("close", () => finish(false, "daemon_closed_without_answer"));
|
|
95
104
|
});
|