@join-pantheon/cli 0.24.1 → 0.25.0
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/README.md +22 -1
- package/dist/admin-stats.js +97 -0
- package/dist/cli.js +136 -7
- package/dist/client.js +2 -0
- package/dist/decision-ui.js +40 -5
- package/dist/doctor.js +101 -60
- package/dist/feedback.js +3 -0
- package/dist/grants.js +38 -17
- package/dist/humanize.js +225 -0
- package/dist/install.js +97 -21
- package/dist/mcp-instructions.js +85 -0
- package/dist/mcp-supervisor.js +2 -1
- package/dist/mcp.js +47 -82
- package/dist/measure.js +142 -0
- package/dist/notice.js +7 -2
- package/dist/notifier.js +19 -5
- package/dist/relay.js +54 -2
- package/dist/roster-file.js +17 -0
- package/dist/run-failure.js +97 -0
- package/dist/setup-guide.js +30 -2
- package/dist/tools.js +9 -2
- package/dist/tray.js +36 -4
- package/dist/worker-policy.js +3 -1
- package/dist/worker.js +107 -13
- package/helper/macos/main.swift +52 -1
- package/helper/macos/menubar.swift +607 -17
- package/package.json +1 -1
package/dist/measure.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve, dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { claudeGranted, cursorGranted, claudeAllow, cursorAllow } from "./grants.js";
|
|
6
|
+
import { codexApprovalState } from "./install.js";
|
|
7
|
+
import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
|
|
8
|
+
const ROOT = process.env.PANTHEON_MCP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
export const HOST_STARTUP_ALLOWANCE_MS = 10_000;
|
|
10
|
+
export const STARTUP_WARN_MS = 1_500;
|
|
11
|
+
export function measureMcp(opts = {}) {
|
|
12
|
+
const entry = opts.entry ?? join(ROOT, "dist", "cli.js");
|
|
13
|
+
const timeoutMs = opts.timeoutMs ?? HOST_STARTUP_ALLOWANCE_MS;
|
|
14
|
+
if (!existsSync(entry))
|
|
15
|
+
return Promise.resolve({ error: `${entry} is missing` });
|
|
16
|
+
return new Promise((done) => {
|
|
17
|
+
let settled = false;
|
|
18
|
+
const finish = (m) => {
|
|
19
|
+
if (settled)
|
|
20
|
+
return;
|
|
21
|
+
settled = true;
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
try {
|
|
24
|
+
proc.kill("SIGKILL");
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
done(m);
|
|
28
|
+
};
|
|
29
|
+
const t0 = Date.now();
|
|
30
|
+
const proc = spawn(process.execPath, [entry, "mcp", "--child"], {
|
|
31
|
+
cwd: opts.cwd, stdio: ["pipe", "pipe", "pipe"], env: opts.env ?? process.env,
|
|
32
|
+
});
|
|
33
|
+
const timer = setTimeout(() => finish({ error: `no answer to initialize within ${timeoutMs}ms` }), timeoutMs);
|
|
34
|
+
proc.on("error", (e) => finish({ error: `could not start: ${e.message}` }));
|
|
35
|
+
proc.on("exit", (code) => finish({ error: `exited with ${code ?? "a signal"} before answering` }));
|
|
36
|
+
proc.stdin.on("error", () => { });
|
|
37
|
+
const send = (m) => { try {
|
|
38
|
+
proc.stdin.write(JSON.stringify(m) + "\n");
|
|
39
|
+
}
|
|
40
|
+
catch { } };
|
|
41
|
+
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "pantheon doctor", version: "0" } } });
|
|
42
|
+
let initializeMs = -1;
|
|
43
|
+
let pending = "";
|
|
44
|
+
proc.stdout.on("data", (d) => {
|
|
45
|
+
pending += d.toString("utf8");
|
|
46
|
+
let nl;
|
|
47
|
+
while ((nl = pending.indexOf("\n")) >= 0) {
|
|
48
|
+
const line = pending.slice(0, nl).trim();
|
|
49
|
+
pending = pending.slice(nl + 1);
|
|
50
|
+
if (!line)
|
|
51
|
+
continue;
|
|
52
|
+
let msg;
|
|
53
|
+
try {
|
|
54
|
+
msg = JSON.parse(line);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (msg.error)
|
|
60
|
+
return finish({ error: msg.error.message ?? "the server refused the handshake" });
|
|
61
|
+
if (msg.id === 1) {
|
|
62
|
+
initializeMs = Date.now() - t0;
|
|
63
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
64
|
+
send({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
65
|
+
}
|
|
66
|
+
else if (msg.id === 2) {
|
|
67
|
+
const tools = (msg.result?.tools ?? []).map((t) => t.name).sort();
|
|
68
|
+
finish({ initializeMs, toolsMs: Date.now() - t0, tools });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function agentLine(name, seen, wanted, strip) {
|
|
75
|
+
if (seen.state === "absent")
|
|
76
|
+
return { text: `${name} not set up here`, drift: false };
|
|
77
|
+
if (seen.state === "invalid")
|
|
78
|
+
return { text: `${name} settings unreadable`, drift: true };
|
|
79
|
+
const have = seen.tools.map(strip);
|
|
80
|
+
const want = wanted.map(strip);
|
|
81
|
+
const missing = want.filter((t) => !have.includes(t));
|
|
82
|
+
const extra = have.filter((t) => !want.includes(t));
|
|
83
|
+
if (!missing.length && !extra.length)
|
|
84
|
+
return { text: `${name} allows ${have.length}`, drift: false };
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (missing.length)
|
|
87
|
+
parts.push(`${missing.length} would prompt on every call (${missing.map((t) => t.replace(/^pantheon_/, "")).join(", ")})`);
|
|
88
|
+
if (extra.length)
|
|
89
|
+
parts.push(`${extra.length} allowed against your choice (${extra.map((t) => t.replace(/^pantheon_/, "")).join(", ")})`);
|
|
90
|
+
return { text: `${name} allows ${have.length} — ${parts.join("; ")}`, drift: true };
|
|
91
|
+
}
|
|
92
|
+
export function toolsReport(registered, seen, approval) {
|
|
93
|
+
const problems = [];
|
|
94
|
+
const expected = [...PANTHEON_TOOL_NAMES].sort();
|
|
95
|
+
let head;
|
|
96
|
+
if (registered === null) {
|
|
97
|
+
head = `${expected.length} tools`;
|
|
98
|
+
}
|
|
99
|
+
else if (JSON.stringify(registered) !== JSON.stringify(expected)) {
|
|
100
|
+
head = `${registered.length} registered, this build expects ${expected.length}`;
|
|
101
|
+
problems.push("the running server and this install disagree about the tool list — a stale window or a half-finished upgrade");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
head = `${registered.length} registered`;
|
|
105
|
+
}
|
|
106
|
+
const stripClaude = (t) => t.replace(/^mcp__pantheon__/, "");
|
|
107
|
+
const stripCursor = (t) => t.replace(/^Mcp\(pantheon:(.*)\)$/, "$1");
|
|
108
|
+
const claude = agentLine("Claude Code", seen.claude, claudeAllow(approval), stripClaude);
|
|
109
|
+
const cursor = agentLine("Cursor", seen.cursor, cursorAllow(approval), stripCursor);
|
|
110
|
+
if (claude.drift)
|
|
111
|
+
problems.push("Claude Code's allow-list does not match");
|
|
112
|
+
if (cursor.drift)
|
|
113
|
+
problems.push("Cursor's allow-list does not match");
|
|
114
|
+
let codex;
|
|
115
|
+
if (seen.codex === null)
|
|
116
|
+
codex = "Codex not set up here";
|
|
117
|
+
else if (!seen.codex.present)
|
|
118
|
+
codex = "Codex has no pantheon entry";
|
|
119
|
+
else if (!seen.codex.serverApprove) {
|
|
120
|
+
codex = "Codex entry without its approval line";
|
|
121
|
+
problems.push("Codex would prompt on every tool");
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const wantPrompt = approval === "ask-before-sending" ? [...WRITE_TOOL_NAMES].sort() : [];
|
|
125
|
+
const havePrompt = [...seen.codex.prompted].sort();
|
|
126
|
+
if (JSON.stringify(wantPrompt) === JSON.stringify(havePrompt)) {
|
|
127
|
+
codex = approval === "ask-before-sending" ? "Codex per-server, send/open_goal/claim prompt" : "Codex per-server";
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
codex = `Codex per-server, but ${havePrompt.length} tool${havePrompt.length === 1 ? "" : "s"} set to prompt (chose ${approval === "ask-before-sending" ? "3" : "0"})`;
|
|
131
|
+
problems.push("Codex's per-tool overrides do not match your choice");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const detail = `${head}; ${claude.text}, ${cursor.text}, ${codex}`;
|
|
135
|
+
if (!problems.length)
|
|
136
|
+
return { level: "pass", detail };
|
|
137
|
+
return { level: "warn", detail, fix: `${problems.join("; ")} — pantheon install --user rewrites every grant to match this build and your approval choice` };
|
|
138
|
+
}
|
|
139
|
+
export function grantsSeen() {
|
|
140
|
+
return { claude: claudeGranted(), cursor: cursorGranted(), codex: codexApprovalState() };
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=measure.js.map
|
package/dist/notice.js
CHANGED
|
@@ -135,7 +135,9 @@ export function composeWorker(run, ctx) {
|
|
|
135
135
|
const asked = plain(run.obligationBody);
|
|
136
136
|
let body;
|
|
137
137
|
if (!run.ok) {
|
|
138
|
-
body =
|
|
138
|
+
body = run.diagnosis
|
|
139
|
+
? `${run.diagnosis.says} ${peer}'s request is still open: ${asked}`
|
|
140
|
+
: `Could not finish ${peer}'s request: ${asked}. It needs a live session or your decision.`;
|
|
139
141
|
}
|
|
140
142
|
else if (run.reply) {
|
|
141
143
|
const verb = VERB[run.reply.act] ?? "Replied to";
|
|
@@ -150,7 +152,7 @@ export function composeWorker(run, ctx) {
|
|
|
150
152
|
const where = `${run.goal ? ` in "${run.goal.title}"` : ""}${run.obligationRef ? ` (act ${run.obligationRef})` : ""}`;
|
|
151
153
|
const prompt = run.ok
|
|
152
154
|
? `My Pantheon worker just handled ${peer}'s request${where}${run.reply ? `; it replied with act ${run.reply.id}` : ""}. Read that reply and the thread it belongs to, tell me in plain words what it did and whether anything is still owed by me, then give me numbered next steps (reply, review the branch, follow up with ${peer}, or nothing). Do not act until I choose.`
|
|
153
|
-
: `A Pantheon run on this machine could not finish ${peer}'s request${where}. Read the thread, tell me what is still owed and why the run stopped, then give me numbered next steps. Do not act until I choose.`;
|
|
155
|
+
: `A Pantheon run on this machine could not finish ${peer}'s request${where}. The worker's own diagnosis: ${run.diagnosis ? `${run.diagnosis.says} ${run.diagnosis.fix}` : "none recorded"}. Read the thread, tell me what is still owed and why the run stopped, then give me numbered next steps. Do not act until I choose.`;
|
|
154
156
|
return {
|
|
155
157
|
kind: run.ok ? "worker" : "worker_failed",
|
|
156
158
|
who: fit(`Your worker · for ${peer}`, BUDGET.who),
|
|
@@ -161,6 +163,9 @@ export function composeWorker(run, ctx) {
|
|
|
161
163
|
sound: !run.ok,
|
|
162
164
|
primary: primaryFor(ctx.tools, ctx.preferred, prompt),
|
|
163
165
|
details: { ...(run.reply ? { act: run.reply.id } : run.obligationRef ? { act: run.obligationRef } : {}) },
|
|
166
|
+
...(run.diagnosis?.dir && run.diagnosis.lines?.length
|
|
167
|
+
? { fix: { label: "Fix it here", dir: run.diagnosis.dir, lines: run.diagnosis.lines, why: run.diagnosis.fix } }
|
|
168
|
+
: {}),
|
|
164
169
|
};
|
|
165
170
|
}
|
|
166
171
|
//# sourceMappingURL=notice.js.map
|
package/dist/notifier.js
CHANGED
|
@@ -9,26 +9,37 @@ import { CONFIG_DIR } from "./config.js";
|
|
|
9
9
|
import { loadProfile } from "./profile.js";
|
|
10
10
|
import { APP_TOOLS, composeDecision, composeDelivery, TOOL_LABELS } from "./notice.js";
|
|
11
11
|
import { cliOnPath, detectAvailable } from "./agents.js";
|
|
12
|
+
import { whichPantheon } from "./service.js";
|
|
12
13
|
export const DEFAULT_REMINDER_MS = 4 * 60 * 60 * 1_000;
|
|
14
|
+
export const MAX_REMINDERS = 4;
|
|
15
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
13
16
|
export function humanObligations(obligations, member) {
|
|
14
17
|
return obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${member}`);
|
|
15
18
|
}
|
|
16
19
|
export function dueHumanObligations(obligations, member, state, now, reminderMs) {
|
|
17
20
|
return humanObligations(obligations, member).filter((o) => {
|
|
18
21
|
const last = state.notified_at[o.id];
|
|
19
|
-
|
|
22
|
+
if (last === undefined)
|
|
23
|
+
return true;
|
|
24
|
+
const n = state.reminded?.[o.id] ?? 0;
|
|
25
|
+
if (n >= MAX_REMINDERS)
|
|
26
|
+
return false;
|
|
27
|
+
const gap = Math.min(reminderMs * 2 ** n, Math.max(reminderMs, DAY_MS));
|
|
28
|
+
return now - last >= gap;
|
|
20
29
|
});
|
|
21
30
|
}
|
|
22
31
|
export function pruneNotificationState(state, obligations, member, now = Date.now()) {
|
|
23
32
|
const active = new Set(humanObligations(obligations, member).map((o) => o.id));
|
|
33
|
+
const reminded = Object.fromEntries(Object.entries(state.reminded ?? {}).filter(([id]) => active.has(id)));
|
|
24
34
|
return {
|
|
25
35
|
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id, at]) => active.has(id) || (id.startsWith("act:") && now - at < 7 * 86_400_000))),
|
|
36
|
+
...(Object.keys(reminded).length ? { reminded } : {}),
|
|
26
37
|
};
|
|
27
38
|
}
|
|
28
39
|
function readState(file) {
|
|
29
40
|
try {
|
|
30
41
|
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
31
|
-
return { notified_at: parsed.notified_at ?? {} };
|
|
42
|
+
return { notified_at: parsed.notified_at ?? {}, ...(parsed.reminded ? { reminded: parsed.reminded } : {}) };
|
|
32
43
|
}
|
|
33
44
|
catch {
|
|
34
45
|
return { notified_at: {} };
|
|
@@ -111,7 +122,7 @@ export function sendDesktopNotification(notice, home = CONFIG_DIR) {
|
|
|
111
122
|
if (!existsSync(binary))
|
|
112
123
|
return false;
|
|
113
124
|
try {
|
|
114
|
-
const child = spawn(binary, ["post", "--json", JSON.stringify({ ...notice, bin_dir: join(home, "bin") })], { detached: true, stdio: "ignore" });
|
|
125
|
+
const child = spawn(binary, ["post", "--json", JSON.stringify({ ...notice, bin_dir: join(home, "bin"), pantheon: whichPantheon() ?? "pantheon" })], { detached: true, stdio: "ignore" });
|
|
115
126
|
child.unref();
|
|
116
127
|
return true;
|
|
117
128
|
}
|
|
@@ -139,7 +150,7 @@ export class HumanNotifier {
|
|
|
139
150
|
const pollMs = this.opts.pollMs ?? 5_000;
|
|
140
151
|
let state = readState(stateFile);
|
|
141
152
|
this.opts.log(`notifier online as ${this.cfg.member}`);
|
|
142
|
-
this.opts.log(` reminders
|
|
153
|
+
this.opts.log(` reminders after ${Math.round(this.opts.reminderMs / 60_000)} minutes, then doubling to daily, ${MAX_REMINDERS} at most — after that a decision waits on pantheon brief and the menu bar`);
|
|
143
154
|
while (!this.stopped) {
|
|
144
155
|
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member, now());
|
|
145
156
|
let dirty = JSON.stringify(pruned) !== JSON.stringify(state);
|
|
@@ -153,13 +164,16 @@ export class HumanNotifier {
|
|
|
153
164
|
const peerOf = (from) => from ? { member: from, agent: client.roster.find((m) => m.id === from)?.agent } : undefined;
|
|
154
165
|
const withLabel = (n) => this.opts.label ? { ...n, context: n.context ? `${this.opts.label} · ${n.context}` : this.opts.label } : n;
|
|
155
166
|
for (const obligation of due) {
|
|
167
|
+
const isReminder = state.notified_at[obligation.id] !== undefined;
|
|
156
168
|
const act = client.seenAct(obligation.ref);
|
|
157
169
|
const notice = withLabel(composeDecision(obligation, act, obligation.goal ? client.goals.get(obligation.goal) : undefined, peerOf(act?.from), { tools, preferred: preferredTool(profile, tools), now: now() }));
|
|
158
170
|
const ok = send(notice);
|
|
159
171
|
if (ok) {
|
|
160
172
|
state.notified_at[obligation.id] = now();
|
|
173
|
+
if (isReminder)
|
|
174
|
+
state.reminded = { ...(state.reminded ?? {}), [obligation.id]: (state.reminded?.[obligation.id] ?? 0) + 1 };
|
|
161
175
|
dirty = true;
|
|
162
|
-
this.opts.log(`> notified for ${obligation.id} (${oneLine(obligation.body, 100)})`);
|
|
176
|
+
this.opts.log(`> ${isReminder ? `reminder ${state.reminded[obligation.id]}/${MAX_REMINDERS}` : "notified"} for ${obligation.id} (${oneLine(obligation.body, 100)})`);
|
|
163
177
|
}
|
|
164
178
|
else {
|
|
165
179
|
this.opts.log(`! could not show a desktop notification for ${obligation.id}`);
|
package/dist/relay.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Accounts, normalizeUsername, validEmail, validUsername } from "./accoun
|
|
|
12
12
|
import { emailSenderFromEnv } from "./email.js";
|
|
13
13
|
import { Allowlist } from "./roster-file.js";
|
|
14
14
|
import { StateStore } from "./store.js";
|
|
15
|
-
import { activity, mergeActivity, feed, noteRefusal, refusalsSince, countActs, ESCALATIONS, MESSAGE_ACTS, ACT_TONE, DAY } from "./admin-stats.js";
|
|
15
|
+
import { activity, mergeActivity, feed, noteRefusal, refusalsSince, countActs, spaceHealth, fleetReport, ESCALATIONS, MESSAGE_ACTS, ACT_TONE, DAY } from "./admin-stats.js";
|
|
16
16
|
import { fmtLimit, parseLimits, PLAN_NAME } from "./plans.js";
|
|
17
17
|
import { routeDecision } from "./profile.js";
|
|
18
18
|
function workState(m, now = Date.now(), busyTtlMs = LIMITS.activityBusyTtlMs, pruneMs = LIMITS.activityPruneMs) {
|
|
@@ -263,6 +263,17 @@ export class Relay {
|
|
|
263
263
|
: {}),
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
|
+
if (url.pathname === "/act") {
|
|
267
|
+
const space = this.spaces.get(url.searchParams.get("space") ?? "");
|
|
268
|
+
const token = url.searchParams.get("token") ?? "";
|
|
269
|
+
if (!space || space.token !== token)
|
|
270
|
+
return this.json(res, 404, { error: "no such space" });
|
|
271
|
+
const act = space.ledger.find(url.searchParams.get("id") ?? "");
|
|
272
|
+
if (!act)
|
|
273
|
+
return this.json(res, 404, { error: "no such act" });
|
|
274
|
+
const goal = act.goal ? space.goals.get(act.goal) : undefined;
|
|
275
|
+
return this.json(res, 200, { act, ...(goal ? { goal: { id: goal.id, title: goal.title, state: goal.state } } : {}) });
|
|
276
|
+
}
|
|
266
277
|
if (url.pathname === "/history") {
|
|
267
278
|
const space = this.spaces.get(url.searchParams.get("space") ?? "");
|
|
268
279
|
const token = url.searchParams.get("token") ?? "";
|
|
@@ -350,7 +361,7 @@ export class Relay {
|
|
|
350
361
|
onAuthHttp(req, res, url, ip) {
|
|
351
362
|
if (url.pathname === "/")
|
|
352
363
|
return this.landingPage(res);
|
|
353
|
-
if (url.pathname === "/admin" || url.pathname === "/admin/space" || url.pathname === "/admin/export" || url.pathname === "/admin/backup" || url.pathname === "/admin/action" || url.pathname === "/admin/login" || url.pathname === "/admin/logout") {
|
|
364
|
+
if (url.pathname === "/admin" || url.pathname === "/admin/space" || url.pathname === "/admin/export" || url.pathname === "/admin/backup" || url.pathname === "/admin/action" || url.pathname === "/admin/login" || url.pathname === "/admin/logout" || url.pathname === "/admin/report") {
|
|
354
365
|
const A0 = this.accounts;
|
|
355
366
|
if (!A0 || !this.adminToken)
|
|
356
367
|
return this.json(res, 404, { error: "not found" });
|
|
@@ -381,6 +392,15 @@ export class Relay {
|
|
|
381
392
|
return void res.end();
|
|
382
393
|
}
|
|
383
394
|
const authed = (fromCookie && tokenOk(fromCookie)) || (fromQuery && tokenOk(fromQuery));
|
|
395
|
+
if (url.pathname === "/admin/report") {
|
|
396
|
+
if (!authed)
|
|
397
|
+
return this.json(res, 401, { error: "admin token required" });
|
|
398
|
+
const rows = this.fleetHealth();
|
|
399
|
+
if (url.searchParams.get("format") === "json")
|
|
400
|
+
return this.json(res, 200, { relay: VERSION, generated: Date.now(), spaces: rows });
|
|
401
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
402
|
+
return void res.end(fleetReport(rows, VERSION) + "\n");
|
|
403
|
+
}
|
|
384
404
|
if (url.pathname === "/admin/export") {
|
|
385
405
|
if (!authed)
|
|
386
406
|
return this.json(res, 403, { error: "sign in at /admin, or append ?token=…" });
|
|
@@ -693,6 +713,37 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
693
713
|
adminCsrf() {
|
|
694
714
|
return createHash("sha256").update("pantheon-admin-csrf " + (this.adminToken ?? "")).digest("hex").slice(0, 32);
|
|
695
715
|
}
|
|
716
|
+
fleetHealth(now = Date.now()) {
|
|
717
|
+
const out = [];
|
|
718
|
+
const seen = new Set();
|
|
719
|
+
const safeOf = (name) => name.replace(/[^a-z0-9._-]/gi, "_");
|
|
720
|
+
for (const sp of this.spaces.values()) {
|
|
721
|
+
seen.add(safeOf(sp.name));
|
|
722
|
+
out.push(spaceHealth({
|
|
723
|
+
name: sp.name, records: sp.ledger.all(), members: sp.allow.all(),
|
|
724
|
+
online: [...sp.members.values()].filter((m) => m.sessions.size > 0).length, relayPkg: VERSION, now,
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
let files = [];
|
|
728
|
+
try {
|
|
729
|
+
files = readdirSync(this.dataDir);
|
|
730
|
+
}
|
|
731
|
+
catch { }
|
|
732
|
+
for (const f of files) {
|
|
733
|
+
if (!f.endsWith(".members.json"))
|
|
734
|
+
continue;
|
|
735
|
+
const safe = f.slice(0, -".members.json".length);
|
|
736
|
+
if (seen.has(safe))
|
|
737
|
+
continue;
|
|
738
|
+
let members = [];
|
|
739
|
+
try {
|
|
740
|
+
members = new Allowlist(this.dataDir, safe).all();
|
|
741
|
+
}
|
|
742
|
+
catch { }
|
|
743
|
+
out.push(spaceHealth({ name: safe, records: this.sleepingRecords(safe), members, online: 0, relayPkg: VERSION, now }));
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
696
747
|
spacesOverview() {
|
|
697
748
|
const now = Date.now();
|
|
698
749
|
const out = new Map();
|
|
@@ -2444,6 +2495,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2444
2495
|
m.agent = agent;
|
|
2445
2496
|
m.last_seen = Date.now();
|
|
2446
2497
|
m.sessions.set(session, { id: session, role, agent, ...(pkg ? { pkg } : {}), ws });
|
|
2498
|
+
sp.allow.touch(id, pkg, agent);
|
|
2447
2499
|
return m;
|
|
2448
2500
|
}
|
|
2449
2501
|
work(m) {
|
package/dist/roster-file.js
CHANGED
|
@@ -146,6 +146,23 @@ export class Allowlist {
|
|
|
146
146
|
this.flush();
|
|
147
147
|
return true;
|
|
148
148
|
}
|
|
149
|
+
static TOUCH_EVERY_MS = 10 * 60_000;
|
|
150
|
+
touch(member, pkg, agent, now = Date.now()) {
|
|
151
|
+
const e = this.entries.get(member);
|
|
152
|
+
if (!e)
|
|
153
|
+
return false;
|
|
154
|
+
const known = agent && agent !== "unknown" ? agent : undefined;
|
|
155
|
+
const changed = (pkg !== undefined && pkg !== e.last_pkg) || (known !== undefined && known !== e.last_agent);
|
|
156
|
+
if (!changed && e.last_seen !== undefined && now - e.last_seen < Allowlist.TOUCH_EVERY_MS)
|
|
157
|
+
return false;
|
|
158
|
+
e.last_seen = now;
|
|
159
|
+
if (pkg !== undefined)
|
|
160
|
+
e.last_pkg = pkg;
|
|
161
|
+
if (known !== undefined)
|
|
162
|
+
e.last_agent = known;
|
|
163
|
+
this.flush();
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
149
166
|
admittedCount() { return [...this.entries.values()].filter((e) => e.admitted).length; }
|
|
150
167
|
founder() {
|
|
151
168
|
const f = [...this.entries.values()].filter((e) => e.admitted && (e.admitted_by === e.member || e.admitted_by === undefined)).sort((a, b) => a.first_seen - b.first_seen)[0];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const sh = (dir, ...args) => execFileSync("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 }).trim();
|
|
3
|
+
export const gitProbe = {
|
|
4
|
+
isRepo: (dir) => { try {
|
|
5
|
+
return sh(dir, "rev-parse", "--is-inside-work-tree") === "true";
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
} },
|
|
10
|
+
hasRemote: (dir) => { try {
|
|
11
|
+
return sh(dir, "remote").split("\n").some((r) => r.trim());
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
} },
|
|
16
|
+
dirty: (dir) => { try {
|
|
17
|
+
return sh(dir, "status", "--porcelain").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
} },
|
|
22
|
+
branchOf: (dir) => { try {
|
|
23
|
+
return sh(dir, "rev-parse", "--abbrev-ref", "HEAD");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
} },
|
|
28
|
+
};
|
|
29
|
+
export function exitLine(detail) {
|
|
30
|
+
const lines = detail.split("\n").map((l) => l.trim());
|
|
31
|
+
const first = lines[0] ?? "";
|
|
32
|
+
if (!/^exit\s+-?\d+$/.test(first))
|
|
33
|
+
return first.slice(0, 120);
|
|
34
|
+
const said = lines.slice(1).find((l) => l && !/^\s*$/.test(l));
|
|
35
|
+
return (said ? `${first}: ${said}` : first).slice(0, 120);
|
|
36
|
+
}
|
|
37
|
+
const list = (paths, max = 3) => paths.length <= max ? paths.join(", ") : `${paths.slice(0, max).join(", ")} and ${paths.length - max} more`;
|
|
38
|
+
const ask = (fn, fallback) => { try {
|
|
39
|
+
return fn();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return fallback;
|
|
43
|
+
} };
|
|
44
|
+
export function diagnoseRun(f, probe = gitProbe) {
|
|
45
|
+
if (!f.exited) {
|
|
46
|
+
return {
|
|
47
|
+
cause: "crashed",
|
|
48
|
+
says: `The agent did not finish — ${exitLine(f.detail)}.`,
|
|
49
|
+
fix: `Read the transcript at ${f.runLog}. If the agent CLI is broken or missing, pantheon doctor names it.`,
|
|
50
|
+
dir: f.worktree,
|
|
51
|
+
lines: [`cat ${f.runLog}`],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!f.branch) {
|
|
55
|
+
return {
|
|
56
|
+
cause: "not_a_repo",
|
|
57
|
+
says: "There is no git repo here, so the run had no branch and nothing it could deliver.",
|
|
58
|
+
fix: `It can still answer and escalate. To let it deliver work, point the worker at a checkout: pantheon service install worker from inside the repo.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!f.allowBash) {
|
|
62
|
+
return {
|
|
63
|
+
cause: "no_commands",
|
|
64
|
+
says: "This worker may edit files but not run commands, so it could not run tests, commit or push.",
|
|
65
|
+
fix: "Allow commands if you want unattended runs to deliver work: pantheon setup --commands yes. Until then a woken run can answer and draft, never deliver.",
|
|
66
|
+
capability: "run_commands",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!ask(() => probe.hasRemote(f.project), true)) {
|
|
70
|
+
return {
|
|
71
|
+
cause: "no_remote",
|
|
72
|
+
says: "This repo has no remote, so nothing the run builds can be fetched by the teammate who asked.",
|
|
73
|
+
fix: "Add a remote your teammate can reach, then the run can push a branch and DELIVER it.",
|
|
74
|
+
dir: f.project,
|
|
75
|
+
lines: ["git remote add origin <url>", "git push -u origin HEAD"],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const dirty = ask(() => probe.dirty(f.project), []);
|
|
79
|
+
if (dirty.length) {
|
|
80
|
+
const on = ask(() => probe.branchOf(f.project), "");
|
|
81
|
+
return {
|
|
82
|
+
cause: "human_has_it",
|
|
83
|
+
says: `The work is uncommitted in your own checkout${on ? ` on ${on}` : ""} — a run never sees that, it starts from what origin has.`,
|
|
84
|
+
fix: "Commit and push it yourself, then the run (or your teammate) can see it. A worker deliberately never touches your working tree.",
|
|
85
|
+
dir: f.project,
|
|
86
|
+
lines: [`git status --short # ${list(dirty)}`, "git add -A", `git commit -m "…"`, `git push${on ? ` -u origin ${on}` : ""}`],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
cause: "silent",
|
|
91
|
+
says: "The run finished cleanly but sent nothing back, so the request is still open.",
|
|
92
|
+
fix: `Read what it did at ${f.runLog}. If it was right to stay silent, answer the teammate yourself; if not, the transcript says where it stopped.`,
|
|
93
|
+
dir: f.worktree,
|
|
94
|
+
lines: [`cat ${f.runLog}`],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=run-failure.js.map
|
package/dist/setup-guide.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { approvalSummary, installForUser } from "./install.js";
|
|
2
3
|
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -33,6 +34,7 @@ export function guideDefaults(profile, cfg) {
|
|
|
33
34
|
return {
|
|
34
35
|
wake: profile.agent.wake_default ?? "anyone",
|
|
35
36
|
commands: profile.agent.allow_bash ?? false,
|
|
37
|
+
approval: profile.agent.approval ?? "approve",
|
|
36
38
|
name: profile.human.display_name || cfg.display || cfg.member,
|
|
37
39
|
title: profile.human.title || "",
|
|
38
40
|
decides: Object.entries(profile.authority).filter(([, m]) => m === "human_required").map(([d]) => d),
|
|
@@ -64,7 +66,7 @@ export function questionnaire() {
|
|
|
64
66
|
"",
|
|
65
67
|
"Only 3 and 4 need the person every time. Ask 2 whenever there is a repo (it is the one default people are",
|
|
66
68
|
"surprised by). The rest have defaults you may take and say out loud:",
|
|
67
|
-
" --wake anyone --decides none --alerts decisions --playbook yes --base <what origin calls its default>",
|
|
69
|
+
" --wake anyone --decides none --alerts decisions --playbook yes --base <what origin calls its default> --approve-all",
|
|
68
70
|
"Without a repo, --wake, --commands, --playbook and --base do nothing yet: skip them and say so.",
|
|
69
71
|
"",
|
|
70
72
|
"1. While they're away, may teammates hand work to this machine? (their agent runs here on its own,",
|
|
@@ -86,6 +88,13 @@ export function questionnaire() {
|
|
|
86
88
|
"8. Which branch should workers start from, and deliveries be checked against? Usually the branch this",
|
|
87
89
|
" project deploys or releases from (often the default branch). → --base <branch>",
|
|
88
90
|
"",
|
|
91
|
+
"9. Should their own agent ask them before each message it sends to a teammate in their name? Default no:",
|
|
92
|
+
" the agent coordinates for them without stopping. Reads and waiting never prompt either way.",
|
|
93
|
+
opt("approve-all", "", "No — send, open goals and claim work without asking (recommended; the default)"),
|
|
94
|
+
opt("ask-before-sending", "", "Yes — show each send, goal and claim for approval first"),
|
|
95
|
+
" These are switches: pass --approve-all or --ask-before-sending with no value. Either way the install",
|
|
96
|
+
" output prints the exact line written to ~/.codex/config.toml, and pantheon leave removes it.",
|
|
97
|
+
"",
|
|
89
98
|
"Example: pantheon setup --yes --wake anyone --commands yes --name \"Praneet Sinha\" --title Founder \\",
|
|
90
99
|
" --decides deployment --alerts decisions --playbook yes --base main",
|
|
91
100
|
].join("\n");
|
|
@@ -133,6 +142,12 @@ export function parseSetupFlags(argv) {
|
|
|
133
142
|
const playbook = yn("playbook");
|
|
134
143
|
if (playbook !== undefined)
|
|
135
144
|
preset.persona = playbook;
|
|
145
|
+
if (argv.includes("--ask-before-sending") && argv.includes("--approve-all"))
|
|
146
|
+
errors.push("--ask-before-sending and --approve-all contradict each other; pass one");
|
|
147
|
+
else if (argv.includes("--ask-before-sending"))
|
|
148
|
+
preset.approval = "ask-before-sending";
|
|
149
|
+
else if (argv.includes("--approve-all"))
|
|
150
|
+
preset.approval = "approve";
|
|
136
151
|
const base = val("base");
|
|
137
152
|
if (base !== undefined && base.trim())
|
|
138
153
|
preset.base = base.trim();
|
|
@@ -179,6 +194,9 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
179
194
|
: a.wake === "anyone" ? "teammates may hand work to this machine while you're away; invites you send grant it, and you can revoke anyone (pantheon admit <name> --revoke)"
|
|
180
195
|
: "only people you approve by hand (pantheon admit <name> --may-wake)"}`);
|
|
181
196
|
io.say(` • Alerts: ${shortAlert(a.notifications).toLowerCase()}`);
|
|
197
|
+
io.say(` • Approval: ${a.approval === "approve"
|
|
198
|
+
? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor and Codex)"
|
|
199
|
+
: "your agent asks you before each send, goal and claim; reading and waiting never prompt"}`);
|
|
182
200
|
io.say(` • Decisions: ${a.decides.length ? `you personally decide ${a.decides.join(", ")}` : "none reserved for you — your agent may decide; add an area when one comes up"}`);
|
|
183
201
|
if (a.wake !== "no" && ctx.project)
|
|
184
202
|
io.say(` • Playbook: ${a.persona ? "a short starter playbook for your worker, in this repo" : "none"}`);
|
|
@@ -212,6 +230,8 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
212
230
|
const b = await io.choose("Which branch should your worker start from, and deliveries be checked against? Usually the branch this project deploys or releases from.", [`${suggestedBase}${suggestedBase === detected ? " (what origin calls its default)" : ""}`, "A different branch"], 0);
|
|
213
231
|
a.base = b === 0 ? suggestedBase : ((await io.ask("Branch name", suggestedBase)).trim() || suggestedBase);
|
|
214
232
|
}
|
|
233
|
+
const ap = await io.choose("Should your agent ask you before each message it sends to a teammate in your name? Reading the board and waiting for peers never prompt either way.", ["No — coordinate for me without stopping to ask (recommended)", "Yes — show me each send, goal and claim first"], a.approval === "approve" ? 0 : 1);
|
|
234
|
+
a.approval = ap === 0 ? "approve" : "ask-before-sending";
|
|
215
235
|
}
|
|
216
236
|
}
|
|
217
237
|
let next = a.title ? applyProfileInit(profile, { name: a.name, title: a.title }) : { ...profile, human: { ...profile.human, display_name: a.name } };
|
|
@@ -227,7 +247,7 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
227
247
|
next = {
|
|
228
248
|
...next,
|
|
229
249
|
authority,
|
|
230
|
-
agent: { ...next.agent, wake_enabled: a.wake !== "no", wake_default: a.wake, allow_bash: a.commands },
|
|
250
|
+
agent: { ...next.agent, wake_enabled: a.wake !== "no", wake_default: a.wake, allow_bash: a.commands, approval: a.approval },
|
|
231
251
|
notifications: {
|
|
232
252
|
decisions: a.notifications !== "none",
|
|
233
253
|
deliveries: a.notifications === "deliveries" || a.notifications === "worker",
|
|
@@ -246,6 +266,14 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
246
266
|
warnings.push(`profile saved but not published (${e.message}) — run \`pantheon profile publish\` when the relay is reachable`);
|
|
247
267
|
}
|
|
248
268
|
}
|
|
269
|
+
const wiring = deps.applyAgentWiring ?? ((ap) => installForUser(ap).map((t) => ({ tool: t.tool, status: t.status })));
|
|
270
|
+
try {
|
|
271
|
+
const codex = wiring(a.approval).find((t) => t.tool === "codex");
|
|
272
|
+
applied.push(`approval: ${approvalSummary(a.approval)}${codex ? ` — codex: ${codex.status}` : ""}`);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
warnings.push(`agent wiring not re-applied (${e.message}) — run pantheon install --user`);
|
|
276
|
+
}
|
|
249
277
|
if (a.wake === "no") {
|
|
250
278
|
if (installed("worker")) {
|
|
251
279
|
try {
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export const PANTHEON_TOOL_NAMES = [
|
|
2
|
-
"pantheon_status", "pantheon_brief", "
|
|
3
|
-
"
|
|
2
|
+
"pantheon_status", "pantheon_brief", "pantheon_directory", "pantheon_route_decision",
|
|
3
|
+
"pantheon_wait", "pantheon_history",
|
|
4
|
+
"pantheon_send", "pantheon_open_goal", "pantheon_claim",
|
|
4
5
|
];
|
|
6
|
+
export const WRITE_TOOL_NAMES = ["pantheon_send", "pantheon_open_goal", "pantheon_claim"];
|
|
7
|
+
export const READ_TOOL_NAMES = PANTHEON_TOOL_NAMES.filter((t) => !WRITE_TOOL_NAMES.includes(t));
|
|
5
8
|
export const PANTHEON_TOOLS = PANTHEON_TOOL_NAMES.map((t) => `mcp__pantheon__${t}`);
|
|
9
|
+
export const APPROVAL_MODES = ["approve", "ask-before-sending"];
|
|
10
|
+
export function toolsAllowed(approval) {
|
|
11
|
+
return approval === "ask-before-sending" ? READ_TOOL_NAMES : PANTHEON_TOOL_NAMES;
|
|
12
|
+
}
|
|
6
13
|
//# sourceMappingURL=tools.js.map
|
package/dist/tray.js
CHANGED
|
@@ -11,7 +11,7 @@ import { requireDeveloperTools } from "./notifier.js";
|
|
|
11
11
|
import { plistPath, serviceInstalled, whichPantheon } from "./service.js";
|
|
12
12
|
import { VERSION } from "./version.js";
|
|
13
13
|
import { LIMITS } from "./protocol.js";
|
|
14
|
-
import { allDecisionsPrompt, decisionPrompt, decisionTitle, workerSummary } from "./decision-ui.js";
|
|
14
|
+
import { allDecisionsPrompt, decisionPrompt, decisionTitle, owedPrompt, stripCeremony, waitingPrompt, workerSummary } from "./decision-ui.js";
|
|
15
15
|
import { loadProfile, SURFACE_BUNDLE_IDS } from "./profile.js";
|
|
16
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
export const trayBinaryPath = (home = CONFIG_DIR) => join(home, "bin", "pantheon-tray");
|
|
@@ -93,6 +93,9 @@ export async function trayStatus(cfg = load(), f = loadAll()) {
|
|
|
93
93
|
members: ok.reduce((n, p) => n + (p.members ?? 0), 0),
|
|
94
94
|
decisions: ok.flatMap((p) => (p.decisions ?? []).map((d) => ({ ...d, body: `[${p.space}] ${d.body}` }))),
|
|
95
95
|
decisions_prompt: allDecisionsPrompt(ok.reduce((n, p) => n + (p.decisions?.length ?? 0), 0)),
|
|
96
|
+
items: ok.flatMap((p) => (p.items ?? []).map((i) => ({ ...i, space: p.space }))),
|
|
97
|
+
owed_count: ok.reduce((n, p) => n + (p.owed_count ?? 0), 0),
|
|
98
|
+
waiting_count: ok.reduce((n, p) => n + (p.waiting_count ?? 0), 0),
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
return trayStatusOne(cfg);
|
|
@@ -104,14 +107,40 @@ async function trayStatusOne(cfg) {
|
|
|
104
107
|
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
105
108
|
const [health, obligations] = await Promise.all([
|
|
106
109
|
getJson(`${base}/health?space=${encodeURIComponent(cfg.space)}`),
|
|
107
|
-
getJson(`${base}/obligations?${q}
|
|
110
|
+
getJson(`${base}/obligations?${q}`),
|
|
108
111
|
]);
|
|
109
112
|
const space = health
|
|
110
113
|
?.spaces?.find((s) => s.name === cfg.space);
|
|
111
114
|
const pending = obligations?.obligations;
|
|
112
|
-
const
|
|
113
|
-
const
|
|
115
|
+
const ok = Array.isArray(pending) && pending.every((o) => o && typeof o.id === "string" && typeof o.body === "string" && typeof o.owner === "string");
|
|
116
|
+
const all = (ok ? pending : []);
|
|
117
|
+
const mineHuman = all.filter((o) => o.owner === `human:${cfg.member}`);
|
|
118
|
+
const decisionsOk = ok;
|
|
119
|
+
const decisions = mineHuman
|
|
114
120
|
.map((o) => ({ id: o.id, body: decisionTitle(o.body), detail: o.body, prompt: decisionPrompt(o.id) }));
|
|
121
|
+
const senderOf = (o) => stripCeremony(o.body).from;
|
|
122
|
+
const row = (o, kind, who, prompt) => ({
|
|
123
|
+
id: o.id, kind, title: decisionTitle(o.body), headline: decisionTitle(o.body, 200), detail: o.body, prompt, who,
|
|
124
|
+
when: o.created_at ?? 0,
|
|
125
|
+
...(o.goal ? { goal: o.goal } : {}),
|
|
126
|
+
...(o.ref ? { ref: o.ref } : {}),
|
|
127
|
+
});
|
|
128
|
+
const owed = all.filter((o) => o.owner === cfg.member);
|
|
129
|
+
const waiting = all.filter((o) => o.owner !== cfg.member && o.owner !== `human:${cfg.member}`);
|
|
130
|
+
const items = [
|
|
131
|
+
...mineHuman.map((o) => {
|
|
132
|
+
const from = senderOf(o);
|
|
133
|
+
return row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id));
|
|
134
|
+
}),
|
|
135
|
+
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
136
|
+
...waiting.map((o) => {
|
|
137
|
+
const who = o.owner.replace(/^human:/, "");
|
|
138
|
+
const r = row(o, "waiting", who, waitingPrompt(who, decisionTitle(o.body), o.goal));
|
|
139
|
+
return o.owner.startsWith("human:")
|
|
140
|
+
? { ...r, title: `${who} decides: ${r.title}`, headline: `${who} decides: ${r.headline}` }
|
|
141
|
+
: r;
|
|
142
|
+
}),
|
|
143
|
+
];
|
|
115
144
|
const profile = loadProfile(cfg);
|
|
116
145
|
const surface = profile.agent.primary_surface;
|
|
117
146
|
const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : "Codex";
|
|
@@ -137,6 +166,9 @@ async function trayStatusOne(cfg) {
|
|
|
137
166
|
surface_bundle_id: SURFACE_BUNDLE_IDS[surface],
|
|
138
167
|
decisions_prompt: allDecisionsPrompt(decisions.length),
|
|
139
168
|
decisions,
|
|
169
|
+
items,
|
|
170
|
+
owed_count: owed.length,
|
|
171
|
+
waiting_count: waiting.length,
|
|
140
172
|
};
|
|
141
173
|
}
|
|
142
174
|
//# sourceMappingURL=tray.js.map
|