@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/README.md
CHANGED
|
@@ -87,6 +87,15 @@ asks you in chat, and passes your answers back as flags — to `pantheon setup -
|
|
|
87
87
|
`pantheon start` / `pantheon join`, which apply them the moment the team exists — so nobody has to
|
|
88
88
|
open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
|
|
89
89
|
|
|
90
|
+
One thing the setup grants that you should know about: your own agent runs the Pantheon tools
|
|
91
|
+
without a per-call prompt, in Claude Code, Cursor and Codex, so it can coordinate for you without
|
|
92
|
+
stopping to ask. For Codex that is one line, `default_tools_approval_mode = "approve"`, written under
|
|
93
|
+
`[mcp_servers.pantheon]` in `~/.codex/config.toml` — it covers Pantheon's tools only, never the shell or
|
|
94
|
+
any other server. The install output prints that exact line, the defaults screen states the grant,
|
|
95
|
+
`pantheon doctor` shows it, and `pantheon leave` removes it. If you would rather approve each message
|
|
96
|
+
your agent sends in your name, `pantheon setup --ask-before-sending` (or the same flag on start/join)
|
|
97
|
+
makes reads silent and prompts for send, open goal and claim; `--approve-all` switches back.
|
|
98
|
+
|
|
90
99
|
Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
|
|
91
100
|
`pantheon install --user` (from your project's repo if you have one).
|
|
92
101
|
|
|
@@ -166,6 +175,18 @@ Checks node, your keypair, the MCP entry, whether the repo has a remote (`DELIVE
|
|
|
166
175
|
whether the relay is reachable **from this machine**, whether a plain `ws://` is crossing the public
|
|
167
176
|
internet, and whether you were actually admitted. Non-zero exit means don't start yet.
|
|
168
177
|
|
|
178
|
+
It also measures what Pantheon costs your agent, on this machine rather than by estimate: it starts
|
|
179
|
+
the MCP server the way your agent does and times its answer (`mcp startup`, typically ~0.2s), reports
|
|
180
|
+
the instruction text every turn carries (`mcp context`), and lists how many tools the server
|
|
181
|
+
registers against how many each agent on this machine has been told to allow (`mcp tools`). If an
|
|
182
|
+
allow-list has drifted — a tool that would prompt on every call, or one allowed against your
|
|
183
|
+
`--ask-before-sending` choice — the line names the tool, and `pantheon install --user` fixes it.
|
|
184
|
+
|
|
185
|
+
If something is wrong and you want to tell us, `pantheon doctor --share` prints the same run with
|
|
186
|
+
nothing identifying in it — no member or team names, no teammates, no paths, no relay address, no
|
|
187
|
+
fingerprint — ready to paste into an email to sid@joinpantheon.network. Nothing is ever sent by the
|
|
188
|
+
product itself; you read it, and you decide.
|
|
189
|
+
|
|
169
190
|
<details><summary>The long way round, if you want to control each piece</summary>
|
|
170
191
|
|
|
171
192
|
`pantheon relay` · `pantheon init` · `pantheon join` · `pantheon members` · `pantheon admit <who> --may-wake`
|
|
@@ -462,7 +483,7 @@ pantheon join <line> join a teammate's team from their line
|
|
|
462
483
|
pantheon invite another single-use line (third person, second machine)
|
|
463
484
|
pantheon status · brief · team who is here, what needs you, who is in the space
|
|
464
485
|
pantheon watch live tail of everything the delegates say to each other
|
|
465
|
-
pantheon doctor
|
|
486
|
+
pantheon doctor [--share] preflight: node, keys, MCP entry, relay, membership, what pantheon costs your agent; --share = paste-safe
|
|
466
487
|
pantheon worker / service stay answerable while you are away (install as a service)
|
|
467
488
|
pantheon persona list / init / show — what a woken worker may do
|
|
468
489
|
pantheon spaces / use / repo several teams on one machine — what is bound where; bind this repo; a team's repos
|
package/dist/admin-stats.js
CHANGED
|
@@ -113,4 +113,101 @@ export function feed(records, f = {}) {
|
|
|
113
113
|
const rows = more ? hits.slice(0, limit) : hits;
|
|
114
114
|
return { rows, next: more ? rows[rows.length - 1].seq : null };
|
|
115
115
|
}
|
|
116
|
+
export const VERDICT_ORDER = ["went quiet", "never started", "new", "active", "idle"];
|
|
117
|
+
export function versionBehind(pkg, relay) {
|
|
118
|
+
const a = pkg.split(".").map(Number), b = relay.split(".").map(Number);
|
|
119
|
+
for (let i = 0; i < 3; i++) {
|
|
120
|
+
const x = a[i] ?? 0, y = b[i] ?? 0;
|
|
121
|
+
if (Number.isNaN(x) || Number.isNaN(y))
|
|
122
|
+
return false;
|
|
123
|
+
if (x !== y)
|
|
124
|
+
return x < y;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const daysAgo = (ts, now) => Math.max(0, Math.floor((now - ts) / DAY));
|
|
129
|
+
export function spaceHealth(input) {
|
|
130
|
+
const now = input.now ?? Date.now();
|
|
131
|
+
const admitted = input.members.filter((m) => m.admitted);
|
|
132
|
+
const messages = input.records.filter((r) => r.act !== "JOIN" && r.act !== "LEAVE");
|
|
133
|
+
let messages_7d = 0, messages_prev_21d = 0, last_message = null;
|
|
134
|
+
const days = new Set();
|
|
135
|
+
for (const r of messages) {
|
|
136
|
+
const d = daysAgo(r.ts, now);
|
|
137
|
+
if (d < 7)
|
|
138
|
+
messages_7d++;
|
|
139
|
+
else if (d < 28) {
|
|
140
|
+
messages_prev_21d++;
|
|
141
|
+
days.add(d);
|
|
142
|
+
}
|
|
143
|
+
if (last_message === null || r.ts > last_message)
|
|
144
|
+
last_message = r.ts;
|
|
145
|
+
}
|
|
146
|
+
const seenAt = (m) => m.last_seen ?? m.first_seen;
|
|
147
|
+
const last_seen = admitted.length ? Math.max(...admitted.map(seenAt)) : null;
|
|
148
|
+
const born = input.members.length ? Math.min(...input.members.map((m) => m.first_seen)) : now;
|
|
149
|
+
const age = daysAgo(born, now);
|
|
150
|
+
const versions = {};
|
|
151
|
+
const behind = [];
|
|
152
|
+
for (const m of admitted) {
|
|
153
|
+
if (!m.last_pkg)
|
|
154
|
+
continue;
|
|
155
|
+
versions[m.last_pkg] = (versions[m.last_pkg] ?? 0) + 1;
|
|
156
|
+
if (versionBehind(m.last_pkg, input.relayPkg))
|
|
157
|
+
behind.push({ member: m.member, pkg: m.last_pkg, days: daysAgo(seenAt(m), now) });
|
|
158
|
+
}
|
|
159
|
+
behind.sort((a, b) => a.days - b.days);
|
|
160
|
+
const newest = admitted.filter((m) => m.last_pkg).sort((a, b) => seenAt(b) - seenAt(a))[0];
|
|
161
|
+
const seenSays = last_seen === null ? "" : `; last seen ${daysAgo(last_seen, now)}d ago${newest ? ` on ${newest.last_pkg}` : ""}`;
|
|
162
|
+
let verdict, says;
|
|
163
|
+
if (input.online > 0 || messages_7d > 0) {
|
|
164
|
+
verdict = "active";
|
|
165
|
+
says = `${messages_7d} message${messages_7d === 1 ? "" : "s"} this week${input.online ? `, ${input.online} online now` : ""}`;
|
|
166
|
+
}
|
|
167
|
+
else if (days.size >= 3) {
|
|
168
|
+
verdict = "went quiet";
|
|
169
|
+
says = `${messages_prev_21d} messages on ${days.size} days in the three weeks before, none this week${seenSays}`;
|
|
170
|
+
}
|
|
171
|
+
else if (age < 7) {
|
|
172
|
+
verdict = "new";
|
|
173
|
+
says = `set up ${age}d ago${messages.length ? "" : ", no message yet"}`;
|
|
174
|
+
}
|
|
175
|
+
else if (messages.length === 0 && age < 28) {
|
|
176
|
+
verdict = "never started";
|
|
177
|
+
says = `set up ${age}d ago, not one message${seenSays}`;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
verdict = "idle";
|
|
181
|
+
const since = last_message ?? last_seen;
|
|
182
|
+
says = since === null ? "nothing on record" : `quiet for ${daysAgo(since, now)}d${seenSays}`;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
name: input.name, verdict, says, members: admitted.length, online: input.online,
|
|
186
|
+
messages_7d, messages_prev_21d, active_days_prev_21d: days.size, last_message, last_seen, versions, behind,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function fleetReport(rows, relayPkg, now = Date.now()) {
|
|
190
|
+
const date = new Date(now).toISOString().slice(0, 10);
|
|
191
|
+
const counts = VERDICT_ORDER.map((v) => [v, rows.filter((r) => r.verdict === v).length]).filter(([, n]) => n > 0);
|
|
192
|
+
const lines = [
|
|
193
|
+
`pantheon relay ${relayPkg} — the week to ${date}`,
|
|
194
|
+
`${rows.length} space${rows.length === 1 ? "" : "s"}${counts.map(([v, n]) => ` · ${n} ${v}`).join("")}`,
|
|
195
|
+
];
|
|
196
|
+
const sorted = [...rows].sort((a, b) => VERDICT_ORDER.indexOf(a.verdict) - VERDICT_ORDER.indexOf(b.verdict) || b.messages_7d - a.messages_7d || a.name.localeCompare(b.name));
|
|
197
|
+
const width = Math.max(4, ...sorted.map((r) => r.name.length));
|
|
198
|
+
for (const r of sorted)
|
|
199
|
+
lines.push(`${r.verdict.padEnd(13)} ${r.name.padEnd(width)} ${r.says}`);
|
|
200
|
+
const builds = {};
|
|
201
|
+
const behind = [];
|
|
202
|
+
for (const r of rows) {
|
|
203
|
+
for (const [v, n] of Object.entries(r.versions))
|
|
204
|
+
builds[v] = (builds[v] ?? 0) + n;
|
|
205
|
+
for (const b of r.behind)
|
|
206
|
+
behind.push(`${b.member} in ${r.name} on ${b.pkg}, ${b.days}d ago`);
|
|
207
|
+
}
|
|
208
|
+
const spread = Object.keys(builds).sort((a, b) => (versionBehind(a, b) ? 1 : versionBehind(b, a) ? -1 : 0)).map((v) => `${v} ×${builds[v]}`).join(" · ");
|
|
209
|
+
if (spread)
|
|
210
|
+
lines.push(`builds in use: ${spread}${behind.length ? ` (behind the relay: ${behind.join("; ")})` : ""}`);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
116
213
|
//# sourceMappingURL=admin-stats.js.map
|
package/dist/cli.js
CHANGED
|
@@ -7,13 +7,13 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
7
7
|
return _emitWarning(warning, ...rest);
|
|
8
8
|
});
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
13
|
import { fromInvite, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
14
|
import { fmtLimit } from "./plans.js";
|
|
15
15
|
import { validEmail, validUsername } from "./accounts.js";
|
|
16
|
-
import { install, launcher } from "./install.js";
|
|
16
|
+
import { install, launcher, removeCodexUserScope } from "./install.js";
|
|
17
17
|
import { Relay } from "./relay.js";
|
|
18
18
|
import { runMcp } from "./mcp.js";
|
|
19
19
|
import { Worker } from "./worker.js";
|
|
@@ -22,7 +22,8 @@ import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
|
22
22
|
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
|
|
23
23
|
import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
|
|
24
24
|
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
25
|
-
import { doctor, render } from "./doctor.js";
|
|
25
|
+
import { doctor, render, shareBlock } from "./doctor.js";
|
|
26
|
+
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
26
27
|
import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
|
|
27
28
|
import { assess } from "./untrusted.js";
|
|
28
29
|
import { report as spendReport, recentRuns } from "./spend.js";
|
|
@@ -331,6 +332,42 @@ async function fetchHistory(cfg, since = 0) {
|
|
|
331
332
|
throw new Error(`relay answered ${res.status} for the ledger`);
|
|
332
333
|
return (await res.json()).acts ?? [];
|
|
333
334
|
}
|
|
335
|
+
async function fetchAct(cfg, id) {
|
|
336
|
+
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
337
|
+
const q = `space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}`;
|
|
338
|
+
try {
|
|
339
|
+
const res = await fetch(`${base}/act?${q}&id=${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(8000) });
|
|
340
|
+
if (res.ok) {
|
|
341
|
+
const body = await res.json();
|
|
342
|
+
if (body.act)
|
|
343
|
+
return { act: body.act, ...(body.goal?.title ? { goalTitle: body.goal.title } : {}) };
|
|
344
|
+
}
|
|
345
|
+
if (res.status === 404) {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch { }
|
|
349
|
+
let head = 0;
|
|
350
|
+
try {
|
|
351
|
+
const h = await (await fetch(`${base}/health?space=${encodeURIComponent(cfg.space)}`, { signal: AbortSignal.timeout(8000) })).json();
|
|
352
|
+
head = h.spaces?.[0]?.seq ?? 0;
|
|
353
|
+
}
|
|
354
|
+
catch { }
|
|
355
|
+
const windows = head > 0 ? [60, 200, 600].map((n) => Math.max(0, head - n)) : [0];
|
|
356
|
+
if (windows[windows.length - 1] !== 0)
|
|
357
|
+
windows.push(0);
|
|
358
|
+
for (const since of windows) {
|
|
359
|
+
try {
|
|
360
|
+
const acts = await fetchHistory(cfg, since);
|
|
361
|
+
const act = acts.find((a) => a.id === id || String(a.seq) === id);
|
|
362
|
+
if (act)
|
|
363
|
+
return { act };
|
|
364
|
+
if (since === 0)
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
catch { }
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
334
371
|
function workerMidRun() {
|
|
335
372
|
try {
|
|
336
373
|
const s = JSON.parse(readFileSync(join(CONFIG_DIR, "worker-state.json"), "utf8"));
|
|
@@ -464,7 +501,7 @@ ${B("control")}
|
|
|
464
501
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
465
502
|
pantheon leave remove what pantheon put on this machine
|
|
466
503
|
pantheon panic cut off all unattended work on this machine, now
|
|
467
|
-
pantheon doctor
|
|
504
|
+
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
468
505
|
pantheon setup three questions — your name, your role, may a woken run execute commands —
|
|
469
506
|
then one screen of defaults to keep or change: who may wake this machine,
|
|
470
507
|
alerts, what you personally decide, playbook, base branch. Re-run any time.
|
|
@@ -1567,6 +1604,11 @@ async function main() {
|
|
|
1567
1604
|
const { revokeToolPermissions } = await import("./grants.js");
|
|
1568
1605
|
for (const l of revokeToolPermissions())
|
|
1569
1606
|
console.error(` perms:${l}`);
|
|
1607
|
+
{
|
|
1608
|
+
const l = removeCodexUserScope();
|
|
1609
|
+
if (l)
|
|
1610
|
+
console.error(` perms:${l}`);
|
|
1611
|
+
}
|
|
1570
1612
|
const { unlinkSync, existsSync: exists, rmSync } = await import("node:fs");
|
|
1571
1613
|
if (cfg && exists(CONFIG_PATH)) {
|
|
1572
1614
|
if (argv.includes("--keep-identity")) {
|
|
@@ -1584,12 +1626,13 @@ async function main() {
|
|
|
1584
1626
|
else {
|
|
1585
1627
|
console.error(` config: none to remove`);
|
|
1586
1628
|
}
|
|
1587
|
-
console.error(`\n ${D("Not touched: the MCP
|
|
1588
|
-
console.error(` ${D("the relay's ledger on disk, and anything you pushed to git.")}`);
|
|
1629
|
+
console.error(`\n ${D("Not touched: the MCP entries in Claude Code and Cursor (pantheon install wrote them; remove them there),")}`);
|
|
1630
|
+
console.error(` ${D("the relay's ledger on disk, and anything you pushed to git. Codex's entry and approval line were removed above.")}`);
|
|
1589
1631
|
if (cfg) {
|
|
1590
1632
|
console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
|
|
1591
1633
|
console.error(` ${D(`pantheon admit ${cfg.member} --revoke`)}`);
|
|
1592
1634
|
}
|
|
1635
|
+
console.error(`\n ${D(`If Pantheon let you down, one line on why to ${FEEDBACK_ADDRESS} would help more than you might think.`)}`);
|
|
1593
1636
|
return;
|
|
1594
1637
|
}
|
|
1595
1638
|
case "spend": {
|
|
@@ -2165,6 +2208,61 @@ async function main() {
|
|
|
2165
2208
|
hud = null;
|
|
2166
2209
|
}
|
|
2167
2210
|
}
|
|
2211
|
+
if (hud?.action === "fix") {
|
|
2212
|
+
const dir = hud.dir && existsSync(hud.dir) ? hud.dir : process.cwd();
|
|
2213
|
+
const steps = hud.lines ?? [];
|
|
2214
|
+
console.error(`${B("What stopped it")}\n ${hud.why ?? "(not recorded)"}\n`);
|
|
2215
|
+
console.error(`${B("Where")} ${dir}\n`);
|
|
2216
|
+
if (!steps.length) {
|
|
2217
|
+
console.error(D("Nothing to run from here."));
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
console.error(B("Steps"));
|
|
2221
|
+
for (const l of steps)
|
|
2222
|
+
console.error(` ${C(l)}`);
|
|
2223
|
+
if (!process.stdin.isTTY)
|
|
2224
|
+
return;
|
|
2225
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2226
|
+
try {
|
|
2227
|
+
const answer = (await new Promise((res) => rl.question(`\n${D("run these here now? [y/N] ")}`, res))).trim().toLowerCase();
|
|
2228
|
+
if (answer !== "y" && answer !== "yes") {
|
|
2229
|
+
console.error(D("left for you — the commands are above"));
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const { spawnSync } = await import("node:child_process");
|
|
2233
|
+
for (const line of steps) {
|
|
2234
|
+
const cmd = line.replace(/\s+#.*$/, "").trim();
|
|
2235
|
+
if (!cmd)
|
|
2236
|
+
continue;
|
|
2237
|
+
console.error(D(`\n$ ${cmd}`));
|
|
2238
|
+
const r = spawnSync("sh", ["-lc", cmd], { cwd: dir, stdio: "inherit" });
|
|
2239
|
+
if (r.status !== 0) {
|
|
2240
|
+
console.error(`\n${B("stopped")} — "${cmd}" exited ${r.status}. The rest is yours.`);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
console.error(`\n${B("done")} — if that was the blocker, the next run can finish.`);
|
|
2245
|
+
}
|
|
2246
|
+
finally {
|
|
2247
|
+
rl.close();
|
|
2248
|
+
}
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
if (hud?.action === "done") {
|
|
2252
|
+
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2253
|
+
await c2.connect().catch((e) => die(e.message));
|
|
2254
|
+
const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
|
|
2255
|
+
if (!o) {
|
|
2256
|
+
c2.close();
|
|
2257
|
+
console.error(D("that decision is no longer waiting on you"));
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
await c2.send({ act: "ANSWER", ref: o.ref, goal: o.goal, after_seq: c2.readSeq,
|
|
2261
|
+
body: "Marked done from the alert — handled outside this thread." }).catch((e) => die(e.message));
|
|
2262
|
+
c2.close();
|
|
2263
|
+
console.error(D(`done — ${o.id} is no longer waiting on you`));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2168
2266
|
if (hud?.action === "details") {
|
|
2169
2267
|
const acts = await fetchHistory(cfg).catch(() => []);
|
|
2170
2268
|
const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
|
|
@@ -2460,6 +2558,31 @@ async function main() {
|
|
|
2460
2558
|
console.log(JSON.stringify(await trayStatus()));
|
|
2461
2559
|
return;
|
|
2462
2560
|
}
|
|
2561
|
+
case "tray-detail": {
|
|
2562
|
+
const key = argv[1] ?? die("usage: pantheon tray-detail <act id>");
|
|
2563
|
+
const cfgNow = load() ?? die("not joined to a team");
|
|
2564
|
+
const { humanize } = await import("./humanize.js");
|
|
2565
|
+
const found = await fetchAct(cfgNow, key);
|
|
2566
|
+
if (!found) {
|
|
2567
|
+
console.log(JSON.stringify({ ok: false }));
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
const a = found.act;
|
|
2571
|
+
let goalTitle = found.goalTitle;
|
|
2572
|
+
if (!goalTitle && a.goal) {
|
|
2573
|
+
const c2 = new PantheonClient(cfgNow, undefined, { role: "cli" });
|
|
2574
|
+
goalTitle = await c2.connect().then(() => c2.goals.get(a.goal)?.title).catch(() => undefined);
|
|
2575
|
+
c2.close();
|
|
2576
|
+
}
|
|
2577
|
+
const { headline, blocks } = humanize(a.body, { ...(goalTitle ? { goalTitle } : {}), from: a.from });
|
|
2578
|
+
console.log(JSON.stringify({
|
|
2579
|
+
ok: true, id: a.id, act: a.act, from: a.from, ts: a.ts,
|
|
2580
|
+
...(a.summary ? { summary: a.summary } : {}),
|
|
2581
|
+
...(goalTitle ? { goal_title: goalTitle } : {}),
|
|
2582
|
+
headline, blocks, raw: a.body,
|
|
2583
|
+
}));
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2463
2586
|
case "menubar": {
|
|
2464
2587
|
need();
|
|
2465
2588
|
const { ensureTrayBinary, writeTrayCommands } = await import("./tray.js");
|
|
@@ -2500,8 +2623,14 @@ async function main() {
|
|
|
2500
2623
|
}
|
|
2501
2624
|
case "doctor": {
|
|
2502
2625
|
const dir = resolve(flag("project", "."));
|
|
2503
|
-
|
|
2626
|
+
const share = argv.includes("--share");
|
|
2627
|
+
if (!share)
|
|
2628
|
+
console.log(`${B("pantheon doctor")} ${D(dir)}\n`);
|
|
2504
2629
|
const checks = await doctor(dir);
|
|
2630
|
+
if (share) {
|
|
2631
|
+
console.log(shareBlock(checks));
|
|
2632
|
+
process.exit(checks.some((c) => c.level === "fail") ? 1 : 0);
|
|
2633
|
+
}
|
|
2505
2634
|
const { text, failed } = render(checks);
|
|
2506
2635
|
console.log(text);
|
|
2507
2636
|
process.exit(failed ? 1 : 0);
|
package/dist/client.js
CHANGED
package/dist/decision-ui.js
CHANGED
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
const oneLine = (value) => value.replace(/\s+/g, " ").trim();
|
|
2
|
+
const LAYERS = [
|
|
3
|
+
/^([a-z0-9._-]+)\s+escalated\s*:\s*/i,
|
|
4
|
+
/^([a-z0-9._-]+)\s+needs? a decision\s*:\s*/i,
|
|
5
|
+
/^([a-z0-9._-]+)\s+(?:asked|asks|said|says|wrote|reported)\s*:\s*/i,
|
|
6
|
+
/^([a-z0-9._-]+)\s+is\s+(?:asking|saying|reporting)(?:\s+that)?\s*:\s*/i,
|
|
7
|
+
/^decision (?:for|required for|needed from)\s+(?:you|[a-z0-9._-]+)\s*(?:\([^)]*\))?\s*[:—-]\s*/i,
|
|
8
|
+
/^(?:action|input|answer|decision)\s+(?:required|needed)\s*(?:\([^)]*\))?\s*(?:from|for)?\s*(?:you|[a-z0-9._-]+)?\s*[:—-]\s*/i,
|
|
9
|
+
/^needs (?:you|your input|a human)[,:]?\s*([a-z0-9._-]*)\s*[:—-]\s*/i,
|
|
10
|
+
/^human (?:action|input) required\s*[—:-]?\s*/i,
|
|
11
|
+
];
|
|
12
|
+
export function stripCeremony(body) {
|
|
13
|
+
let text = oneLine(body);
|
|
14
|
+
let from;
|
|
15
|
+
for (let round = 0; round < 6; round++) {
|
|
16
|
+
const before = text;
|
|
17
|
+
for (const re of LAYERS) {
|
|
18
|
+
const m = re.exec(text);
|
|
19
|
+
if (!m)
|
|
20
|
+
continue;
|
|
21
|
+
const name = (m[1] ?? "").trim();
|
|
22
|
+
if (!from && name && !/^(you|your|a)$/i.test(name))
|
|
23
|
+
from = name.toLowerCase();
|
|
24
|
+
text = text.slice(m[0].length);
|
|
25
|
+
}
|
|
26
|
+
if (text === before)
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return { text: text.trim(), ...(from ? { from } : {}) };
|
|
30
|
+
}
|
|
2
31
|
export function decisionTitle(body, max = 96) {
|
|
3
|
-
let text =
|
|
4
|
-
.replace(
|
|
5
|
-
.replace(
|
|
6
|
-
.replace(
|
|
7
|
-
.
|
|
32
|
+
let text = stripCeremony(body).text
|
|
33
|
+
.replace(/\(\s*goal\s+g_[a-z0-9]+\s*,\s*/gi, "(")
|
|
34
|
+
.replace(/\s*\b(?:of|in|for|on|from)?\s*\(?\bgoal\s+g_[a-z0-9]+\)?/gi, "")
|
|
35
|
+
.replace(/\s{2,}/g, " ")
|
|
36
|
+
.trim();
|
|
8
37
|
const blockedAsk = text.match(/^cannot answer (.+?)\s+ask\s*:/i);
|
|
9
38
|
if (blockedAsk) {
|
|
10
39
|
text = `Decide whether to share the information requested for ${blockedAsk[1]}`;
|
|
@@ -25,6 +54,12 @@ export function decisionPrompt(id) {
|
|
|
25
54
|
export function allDecisionsPrompt(count) {
|
|
26
55
|
return `Check my ${count} Pantheon decision${count === 1 ? "" : "s"}. Explain each one in plain language: what happened, why I am needed, my options, and your recommendation. Prioritize what needs attention first. Do not decide or act for me; wait for my answers.`;
|
|
27
56
|
}
|
|
57
|
+
export function owedPrompt(title, goal) {
|
|
58
|
+
return `Pantheon: my side owes this — "${title}". Read the thread (pantheon history${goal ? ` --goal ${goal}` : ""}), tell me in plain language what is owed, what is already done, and exactly what you would send to discharge it. Draft the act, then wait for me before sending.`;
|
|
59
|
+
}
|
|
60
|
+
export function waitingPrompt(who, title, goal) {
|
|
61
|
+
return `Pantheon: I am waiting on ${who} for "${title}". Read the thread (pantheon history${goal ? ` --goal ${goal}` : ""}), tell me whether it is genuinely blocked on them, how long it has been waiting, and whether anything on my side is holding it up. If a nudge is warranted, draft a short one and wait for me.`;
|
|
62
|
+
}
|
|
28
63
|
export function workerSummary(raw) {
|
|
29
64
|
const state = oneLine(raw);
|
|
30
65
|
if (/deferring.*turn is in flight|holding.*live session/i.test(state))
|