@join-pantheon/cli 0.24.4 → 0.25.1
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 +26 -1
- package/dist/admin-stats.js +106 -0
- package/dist/cli.js +77 -10
- package/dist/client.js +2 -0
- package/dist/config.js +28 -0
- package/dist/decision-ui.js +31 -9
- package/dist/doctor.js +118 -62
- package/dist/feedback.js +3 -0
- package/dist/grants.js +38 -17
- package/dist/humanize.js +2 -0
- package/dist/install.js +97 -21
- package/dist/machine.js +12 -5
- 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/relay.js +86 -2
- package/dist/roster-file.js +17 -0
- package/dist/run-failure.js +97 -0
- package/dist/service.js +9 -1
- package/dist/setup-guide.js +30 -2
- package/dist/tools.js +9 -2
- package/dist/tray.js +10 -3
- package/dist/upgrade.js +11 -1
- package/dist/worker-policy.js +3 -1
- package/dist/worker.js +107 -13
- package/helper/macos/main.swift +22 -1
- package/helper/macos/menubar.swift +51 -13
- package/package.json +1 -1
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,15 @@ export class Relay {
|
|
|
263
263
|
: {}),
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
|
+
if (url.pathname === "/join") {
|
|
267
|
+
const token = (url.searchParams.get("token") ?? "").trim();
|
|
268
|
+
if (!/^[A-Za-z0-9_-]{16,64}$/.test(token))
|
|
269
|
+
return this.json(res, 400, { error: "an invite code is required: /join?token=<code>" });
|
|
270
|
+
const team = this.teamOfInvite(token);
|
|
271
|
+
return team
|
|
272
|
+
? this.json(res, 200, { team })
|
|
273
|
+
: this.json(res, 404, { error: "no open invite matches that code — it may have been used already or expired" });
|
|
274
|
+
}
|
|
266
275
|
if (url.pathname === "/act") {
|
|
267
276
|
const space = this.spaces.get(url.searchParams.get("space") ?? "");
|
|
268
277
|
const token = url.searchParams.get("token") ?? "";
|
|
@@ -361,7 +370,7 @@ export class Relay {
|
|
|
361
370
|
onAuthHttp(req, res, url, ip) {
|
|
362
371
|
if (url.pathname === "/")
|
|
363
372
|
return this.landingPage(res);
|
|
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") {
|
|
373
|
+
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") {
|
|
365
374
|
const A0 = this.accounts;
|
|
366
375
|
if (!A0 || !this.adminToken)
|
|
367
376
|
return this.json(res, 404, { error: "not found" });
|
|
@@ -392,6 +401,15 @@ export class Relay {
|
|
|
392
401
|
return void res.end();
|
|
393
402
|
}
|
|
394
403
|
const authed = (fromCookie && tokenOk(fromCookie)) || (fromQuery && tokenOk(fromQuery));
|
|
404
|
+
if (url.pathname === "/admin/report") {
|
|
405
|
+
if (!authed)
|
|
406
|
+
return this.json(res, 401, { error: "admin token required" });
|
|
407
|
+
const rows = this.fleetHealth();
|
|
408
|
+
if (url.searchParams.get("format") === "json")
|
|
409
|
+
return this.json(res, 200, { relay: VERSION, generated: Date.now(), spaces: rows });
|
|
410
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
411
|
+
return void res.end(fleetReport(rows, VERSION) + "\n");
|
|
412
|
+
}
|
|
395
413
|
if (url.pathname === "/admin/export") {
|
|
396
414
|
if (!authed)
|
|
397
415
|
return this.json(res, 403, { error: "sign in at /admin, or append ?token=…" });
|
|
@@ -704,6 +722,71 @@ button:hover{background:#1d4ed8}.err{color:var(--bad);margin-top:12px}
|
|
|
704
722
|
adminCsrf() {
|
|
705
723
|
return createHash("sha256").update("pantheon-admin-csrf " + (this.adminToken ?? "")).digest("hex").slice(0, 32);
|
|
706
724
|
}
|
|
725
|
+
teamOfInvite(token) {
|
|
726
|
+
const exact = (al) => { const inv = al.findInvite(token); return !!inv && inv.token === token; };
|
|
727
|
+
for (const sp of this.spaces.values())
|
|
728
|
+
if (exact(sp.allow))
|
|
729
|
+
return sp.name;
|
|
730
|
+
const safeOf = (name) => name.replace(/[^a-z0-9._-]/gi, "_");
|
|
731
|
+
const live = new Set([...this.spaces.keys()].map(safeOf));
|
|
732
|
+
let files = [];
|
|
733
|
+
try {
|
|
734
|
+
files = readdirSync(this.dataDir);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
for (const f of files) {
|
|
740
|
+
if (!f.endsWith(".members.json"))
|
|
741
|
+
continue;
|
|
742
|
+
const safe = f.slice(0, -".members.json".length);
|
|
743
|
+
if (live.has(safe))
|
|
744
|
+
continue;
|
|
745
|
+
try {
|
|
746
|
+
if (!exact(new Allowlist(this.dataDir, safe)))
|
|
747
|
+
continue;
|
|
748
|
+
const named = this.sleepingRecords(safe).find((r) => r.space)?.space;
|
|
749
|
+
if (named)
|
|
750
|
+
return named;
|
|
751
|
+
if (safeOf(safe) === safe && !/_/.test(safe))
|
|
752
|
+
return safe;
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
catch { }
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
fleetHealth(now = Date.now()) {
|
|
760
|
+
const out = [];
|
|
761
|
+
const seen = new Set();
|
|
762
|
+
const safeOf = (name) => name.replace(/[^a-z0-9._-]/gi, "_");
|
|
763
|
+
for (const sp of this.spaces.values()) {
|
|
764
|
+
seen.add(safeOf(sp.name));
|
|
765
|
+
out.push(spaceHealth({
|
|
766
|
+
name: sp.name, records: sp.ledger.all(), members: sp.allow.all(),
|
|
767
|
+
online: [...sp.members.values()].filter((m) => m.sessions.size > 0).length, relayPkg: VERSION, now,
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
770
|
+
let files = [];
|
|
771
|
+
try {
|
|
772
|
+
files = readdirSync(this.dataDir);
|
|
773
|
+
}
|
|
774
|
+
catch { }
|
|
775
|
+
for (const f of files) {
|
|
776
|
+
if (!f.endsWith(".members.json"))
|
|
777
|
+
continue;
|
|
778
|
+
const safe = f.slice(0, -".members.json".length);
|
|
779
|
+
if (seen.has(safe))
|
|
780
|
+
continue;
|
|
781
|
+
let members = [];
|
|
782
|
+
try {
|
|
783
|
+
members = new Allowlist(this.dataDir, safe).all();
|
|
784
|
+
}
|
|
785
|
+
catch { }
|
|
786
|
+
out.push(spaceHealth({ name: safe, records: this.sleepingRecords(safe), members, online: 0, relayPkg: VERSION, now }));
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
707
790
|
spacesOverview() {
|
|
708
791
|
const now = Date.now();
|
|
709
792
|
const out = new Map();
|
|
@@ -2455,6 +2538,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2455
2538
|
m.agent = agent;
|
|
2456
2539
|
m.last_seen = Date.now();
|
|
2457
2540
|
m.sessions.set(session, { id: session, role, agent, ...(pkg ? { pkg } : {}), ws });
|
|
2541
|
+
sp.allow.touch(id, pkg, agent);
|
|
2458
2542
|
return m;
|
|
2459
2543
|
}
|
|
2460
2544
|
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/service.js
CHANGED
|
@@ -164,14 +164,22 @@ export function uninstallService(kind) {
|
|
|
164
164
|
export function restartService(kind, exec = (c, a) => execFileSync(c, a, { stdio: "ignore" })) {
|
|
165
165
|
if (!serviceInstalled(kind))
|
|
166
166
|
return [` ${kind}: not installed here`];
|
|
167
|
+
const uid = process.getuid?.() ?? 501;
|
|
167
168
|
try {
|
|
168
169
|
if (platform() === "darwin")
|
|
169
|
-
exec("launchctl", ["kickstart", "-k", `gui/${
|
|
170
|
+
exec("launchctl", ["kickstart", "-k", `gui/${uid}/${LABEL(kind)}`]);
|
|
170
171
|
else
|
|
171
172
|
exec("systemctl", ["--user", "restart", `pantheon-${kind}`]);
|
|
172
173
|
return [` ${kind}: restarted on the new code`];
|
|
173
174
|
}
|
|
174
175
|
catch (e) {
|
|
176
|
+
if (platform() === "darwin") {
|
|
177
|
+
try {
|
|
178
|
+
exec("launchctl", ["bootstrap", `gui/${uid}`, plistPath(kind)]);
|
|
179
|
+
return [` ${kind}: was not loaded in launchd — loaded and started on the new code`];
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
175
183
|
return [` ${kind}: could not restart (${e.message.split("\n")[0]}) — pantheon service restart ${kind}`];
|
|
176
184
|
}
|
|
177
185
|
}
|
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, owedPrompt, waitingPrompt, 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");
|
|
@@ -118,6 +118,7 @@ async function trayStatusOne(cfg) {
|
|
|
118
118
|
const decisionsOk = ok;
|
|
119
119
|
const decisions = mineHuman
|
|
120
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;
|
|
121
122
|
const row = (o, kind, who, prompt) => ({
|
|
122
123
|
id: o.id, kind, title: decisionTitle(o.body), headline: decisionTitle(o.body, 200), detail: o.body, prompt, who,
|
|
123
124
|
when: o.created_at ?? 0,
|
|
@@ -127,11 +128,17 @@ async function trayStatusOne(cfg) {
|
|
|
127
128
|
const owed = all.filter((o) => o.owner === cfg.member);
|
|
128
129
|
const waiting = all.filter((o) => o.owner !== cfg.member && o.owner !== `human:${cfg.member}`);
|
|
129
130
|
const items = [
|
|
130
|
-
...mineHuman.map((o) =>
|
|
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
|
+
}),
|
|
131
135
|
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
132
136
|
...waiting.map((o) => {
|
|
133
137
|
const who = o.owner.replace(/^human:/, "");
|
|
134
|
-
|
|
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;
|
|
135
142
|
}),
|
|
136
143
|
];
|
|
137
144
|
const profile = loadProfile(cfg);
|
package/dist/upgrade.js
CHANGED
|
@@ -16,6 +16,16 @@ function pkgVersion(root) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
export const PKG_NAME = "@join-pantheon/cli";
|
|
19
|
+
export function rewireVia(root, fallback, run = (file, args) => execFileSync(process.execPath, [file, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 120_000 })) {
|
|
20
|
+
try {
|
|
21
|
+
const out = JSON.parse(run(join(root, "dist", "cli.js"), ["rewire", "--json"]));
|
|
22
|
+
if (Array.isArray(out) && out.every((o) => o && typeof o.tool === "string" && typeof o.status === "string")) {
|
|
23
|
+
return out.map((o) => ({ tool: o.tool, status: o.status }));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
return fallback();
|
|
28
|
+
}
|
|
19
29
|
export function defaultDeps() {
|
|
20
30
|
const git = (root, ...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
21
31
|
return {
|
|
@@ -29,7 +39,7 @@ export function defaultDeps() {
|
|
|
29
39
|
pull: (root) => git(root, "pull", "--ff-only"),
|
|
30
40
|
build: (root) => execFileSync("npm", ["install", "--no-audit", "--no-fund"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }),
|
|
31
41
|
version: pkgVersion,
|
|
32
|
-
rewire: () => installForUser().map((t) => ({ tool: t.tool, status: t.status })),
|
|
42
|
+
rewire: () => rewireVia(repoRoot(), () => installForUser().map((t) => ({ tool: t.tool, status: t.status }))),
|
|
33
43
|
running: () => {
|
|
34
44
|
const up = [];
|
|
35
45
|
try {
|
package/dist/worker-policy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CONFIG_PATH } from "./config.js";
|
|
4
4
|
export const ASK_COOLDOWN_MS = 24 * 60 * 60 * 1_000;
|
|
5
|
+
export const ASK_EXPIRY_MS = 14 * 24 * 60 * 60 * 1_000;
|
|
5
6
|
export function policyHome(home) {
|
|
6
7
|
return home ?? process.env.PANTHEON_HOME ?? dirname(CONFIG_PATH);
|
|
7
8
|
}
|
|
@@ -41,7 +42,8 @@ export function revoke(p, cap) {
|
|
|
41
42
|
export function mayAsk(p, cap, now = Date.now()) {
|
|
42
43
|
if (p.grants[cap])
|
|
43
44
|
return false;
|
|
44
|
-
|
|
45
|
+
const open = p.asks[cap];
|
|
46
|
+
if (open && now - open.at < ASK_EXPIRY_MS)
|
|
45
47
|
return false;
|
|
46
48
|
const deniedAt = p.denied[cap];
|
|
47
49
|
return deniedAt === undefined || now - deniedAt >= ASK_COOLDOWN_MS;
|