@join-pantheon/cli 0.30.0 → 0.31.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 +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +408 -34
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +206 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +13 -1
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +48 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
8
8
|
});
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
13
|
import { agentSpawn, CmdLineUnsafe } from "./which.js";
|
|
14
14
|
import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, identity, repoInfo, bindRepo, bindFolder, unbindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
@@ -20,7 +20,7 @@ import { runMcp } from "./mcp.js";
|
|
|
20
20
|
import { Worker } from "./worker.js";
|
|
21
21
|
import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
|
|
22
22
|
import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
23
|
-
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
|
|
23
|
+
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault, wakeMode } from "./profile.js";
|
|
24
24
|
import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
|
|
25
25
|
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
26
26
|
import { doctor, render, shareBlock } from "./doctor.js";
|
|
@@ -258,6 +258,175 @@ function sameHost(a, b) {
|
|
|
258
258
|
return false;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
async function findAndBindRepos(all, m, teamRepos, opts = {}) {
|
|
262
|
+
const want = teamRepos.map((r) => r.fingerprint);
|
|
263
|
+
if (!want.length) {
|
|
264
|
+
console.error(D(` "${m.name}" has no repos registered yet — the first member to connect from a clone registers it`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const name = (fp) => { const r = teamRepos.find((x) => x.fingerprint === fp); return r?.origin ? r.origin.replace(/^.*[:/]([^:/]+)$/, "$1") : fp; };
|
|
268
|
+
const already = new Set(Object.entries(all.bindings ?? {}).filter(([, b]) => b.space === m.id).map(([k]) => k));
|
|
269
|
+
const unbound = new Set(all.unbound ?? []);
|
|
270
|
+
for (const fp of want.filter((f) => unbound.has(f) && !already.has(f)))
|
|
271
|
+
console.error(D(` ${name(fp)}: you unbound it on this machine — left so (pantheon use ${m.name} --here inside it to bind it again)`));
|
|
272
|
+
const todo = want.filter((fp) => !already.has(fp) && !unbound.has(fp));
|
|
273
|
+
if (!todo.length) {
|
|
274
|
+
console.error(`${C("✓")} every repo "${m.name}" works from is bound on this machine: ${want.map(name).join(", ")}`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
let recents = opts.recents;
|
|
278
|
+
const tty = !!process.stdin.isTTY && !!process.stderr.isTTY;
|
|
279
|
+
if (recents === undefined && tty) {
|
|
280
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
281
|
+
const a = ((await questionOrNull(rl, `Also look in the folders your editors and agents have opened? Only their paths are read, and nothing leaves this machine. [Y/n] `)) ?? "").trim().toLowerCase();
|
|
282
|
+
rl.close();
|
|
283
|
+
recents = a === "" || a.startsWith("y");
|
|
284
|
+
}
|
|
285
|
+
const { findTeamClones } = await import("./repo-find.js");
|
|
286
|
+
console.error(D(` looking for ${todo.map(name).join(", ")} on this machine…`));
|
|
287
|
+
let found = findTeamClones(todo, { recents: !!recents, ...(opts.extra ? { extra: opts.extra } : {}) });
|
|
288
|
+
let missing = todo.filter((fp) => !found.some((f) => f.repo.fingerprint === fp));
|
|
289
|
+
if (missing.length && tty) {
|
|
290
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
291
|
+
const a = ((await questionOrNull(rl, ` Not found: ${missing.map(name).join(", ")}. The folder your repos live in (Enter to skip): `)) ?? "").trim();
|
|
292
|
+
rl.close();
|
|
293
|
+
if (a) {
|
|
294
|
+
const more = findTeamClones(missing, { extra: [resolve(a.replace(/^~(?=$|\/)/, homedir()))] });
|
|
295
|
+
found = [...found, ...more];
|
|
296
|
+
missing = missing.filter((fp) => !more.some((f) => f.repo.fingerprint === fp));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const elsewhere = found.filter((f) => { const b = all.bindings?.[f.repo.fingerprint]; return !!b && b.space !== m.id; });
|
|
300
|
+
for (const f of elsewhere)
|
|
301
|
+
console.error(` ${D("–")} ${name(f.repo.fingerprint).padEnd(18)} ${f.repo.root} ${D(`bound to "${findMembership(all, all.bindings[f.repo.fingerprint].space)?.name ?? all.bindings[f.repo.fingerprint].space}" — left there (pantheon use ${m.name} --here inside it to move it)`)}`);
|
|
302
|
+
found = found.filter((f) => !elsewhere.includes(f));
|
|
303
|
+
for (const f of found)
|
|
304
|
+
console.error(` ${C("✓")} ${name(f.repo.fingerprint).padEnd(18)} ${f.repo.root} ${D(`(${f.how})`)}`);
|
|
305
|
+
for (const fp of missing)
|
|
306
|
+
console.error(` ${D("✗")} ${name(fp).padEnd(18)} ${D("not on this machine — clone it, then: pantheon repo find")}`);
|
|
307
|
+
if (!found.length)
|
|
308
|
+
return;
|
|
309
|
+
let go = !!opts.yes || !!opts.auto;
|
|
310
|
+
if (!go && tty) {
|
|
311
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
312
|
+
const a = ((await questionOrNull(rl, `Use ${found.length === 1 ? "it" : `these ${found.length}`} for "${m.name}"? [Y/n] `)) ?? "").trim().toLowerCase();
|
|
313
|
+
rl.close();
|
|
314
|
+
go = a === "" || a.startsWith("y");
|
|
315
|
+
}
|
|
316
|
+
if (!go) {
|
|
317
|
+
console.error(D(` nothing bound${tty ? "" : " — run it again with --yes to bind them"}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const f of found)
|
|
321
|
+
bindRepo(f.repo, m.id);
|
|
322
|
+
const them = found.length === 1 ? "it" : "them";
|
|
323
|
+
console.error(`${B("bound")} ${found.map((f) => name(f.repo.fingerprint)).join(", ")} → "${m.name}" — agents opened in ${them} speak for the team, and your worker (when it is on) serves ${them}`);
|
|
324
|
+
}
|
|
325
|
+
async function bindReposIn(dir, all, m, yes, again) {
|
|
326
|
+
const found = (await import("./repo-find.js")).reposUnder(dir, 3);
|
|
327
|
+
const taken = found.filter((r) => { const b = all.bindings?.[r.fingerprint]; return !!b && b.space !== m.id; });
|
|
328
|
+
for (const r of taken)
|
|
329
|
+
console.error(D(` ${repoLabel(r)} is bound to "${findMembership(all, all.bindings[r.fingerprint].space)?.name ?? all.bindings[r.fingerprint].space}" — left there (pantheon use ${m.name} --here inside it to move it)`));
|
|
330
|
+
const unbound = new Set(all.unbound ?? []);
|
|
331
|
+
for (const r of found.filter((x) => unbound.has(x.fingerprint)))
|
|
332
|
+
console.error(D(` ${repoLabel(r)}: you unbound it on this machine — left so (pantheon use ${m.name} --here inside it to bind it again)`));
|
|
333
|
+
let inside = found.filter((r) => !taken.includes(r) && !unbound.has(r.fingerprint));
|
|
334
|
+
if (inside.length && !yes) {
|
|
335
|
+
console.error(` ${B("repos in it")} ${inside.map(repoLabel).join(", ")}`);
|
|
336
|
+
if (process.stdin.isTTY) {
|
|
337
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
338
|
+
const a = ((await questionOrNull(rl, ` Bind ${inside.length === 1 ? "it" : `these ${inside.length}`} to "${m.name}" and register ${inside.length === 1 ? "it" : "them"} with the team? [Y/n] `)) ?? "").trim().toLowerCase();
|
|
339
|
+
rl.close();
|
|
340
|
+
if (!(a === "" || a.startsWith("y")))
|
|
341
|
+
inside = [];
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
console.error(D(` not bound yet — to bind and register them with "${m.name}": ${again}`));
|
|
345
|
+
inside = [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!inside.length)
|
|
349
|
+
return 0;
|
|
350
|
+
for (const r of inside)
|
|
351
|
+
bindRepo(r, m.id);
|
|
352
|
+
const c = configFor(loadAll() ?? all, m, null);
|
|
353
|
+
const client = new PantheonClient(c, undefined, { role: "cli" });
|
|
354
|
+
const registered = [], failed = [];
|
|
355
|
+
try {
|
|
356
|
+
await client.connect();
|
|
357
|
+
for (const r of inside) {
|
|
358
|
+
if (client.repos.some((x) => x.fingerprint === r.fingerprint)) {
|
|
359
|
+
registered.push(repoLabel(r));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
await client.bindRepo(r.fingerprint, repoShare(r)).then(() => registered.push(repoLabel(r)), (e) => failed.push(`${repoLabel(r)} (${e.message.split("\n")[0]})`));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
failed.push(...inside.map((r) => `${repoLabel(r)} (${e.message.split("\n")[0]})`));
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
client.close();
|
|
370
|
+
}
|
|
371
|
+
console.error(` ${B("repos in it")} ${inside.map(repoLabel).join(", ")} — each bound to "${m.name}"${registered.length ? "" : D(" here")}`);
|
|
372
|
+
if (failed.length)
|
|
373
|
+
console.error(D(` not registered with the team yet: ${failed.join("; ")} — pantheon repo add, from each, when the relay is reachable`));
|
|
374
|
+
return inside.length;
|
|
375
|
+
}
|
|
376
|
+
async function bindReposHere(yes) {
|
|
377
|
+
const cwd = process.cwd();
|
|
378
|
+
if (repoInfo(cwd))
|
|
379
|
+
return;
|
|
380
|
+
const real = (d) => { try {
|
|
381
|
+
return realpathSync.native(d);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return resolve(d);
|
|
385
|
+
} };
|
|
386
|
+
const here = real(cwd);
|
|
387
|
+
if (here === real(homedir()) || dirname(here) === here)
|
|
388
|
+
return;
|
|
389
|
+
if (["Downloads", "Desktop", "Documents", "Library"].some((n) => here === join(real(homedir()), n)))
|
|
390
|
+
return;
|
|
391
|
+
const all = loadAll(), j = load();
|
|
392
|
+
const m = all && j ? findMembership(all, j.spaceId ?? j.space) ?? findMembership(all, j.space) : null;
|
|
393
|
+
if (!all || !m)
|
|
394
|
+
return;
|
|
395
|
+
const n = (await import("./repo-find.js")).reposUnder(cwd, 3).length;
|
|
396
|
+
if (!n)
|
|
397
|
+
return;
|
|
398
|
+
console.error("");
|
|
399
|
+
const many = n > 6 && !process.stdin.isTTY;
|
|
400
|
+
if (many) {
|
|
401
|
+
console.error(D(` this folder holds ${n} repos — to bind and register them all with "${m.name}": pantheon use ${m.name} --here --yes (or pantheon repo find, which binds only the team's own)`));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
await bindReposIn(cwd, all, m, yes, `pantheon use ${m.name} --here --yes`).catch((e) => console.error(D(` could not bind the repos here: ${e.message}`)));
|
|
405
|
+
}
|
|
406
|
+
async function workerAfterBinding(how) {
|
|
407
|
+
if (how === "off")
|
|
408
|
+
return;
|
|
409
|
+
const j = load();
|
|
410
|
+
if (!j?.spaceId || wakeMode(loadProfile(j)) === "no")
|
|
411
|
+
return;
|
|
412
|
+
const { serviceInstalled, servicesUnavailable } = await import("./service.js");
|
|
413
|
+
if (servicesUnavailable(process.platform) || serviceInstalled("worker"))
|
|
414
|
+
return;
|
|
415
|
+
const ws = (await import("./worker-fleet.js")).teamWorkspaces(loadAll(), j.spaceId);
|
|
416
|
+
if (!ws.length)
|
|
417
|
+
return;
|
|
418
|
+
try {
|
|
419
|
+
const { commandsChosen } = await import("./profile.js");
|
|
420
|
+
const commandsOn = ws.some((w) => w.kind === "repo") && commandsChosen() !== false;
|
|
421
|
+
installService("worker", ws[0].path, commandsOn ? ["--allow-bash"] : []);
|
|
422
|
+
console.error(` ${C("✓")} worker on — serves ${ws.map((w) => w.name).join(", ")} while you are away${wakeMode(loadProfile(j)) === "approved" ? "; you are asked the first time each teammate needs it" : ""}${commandsOn ? "; a run may run tests, commit and push to its own branch (edits only: pantheon setup --commands no)" : ""}`);
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
console.error(D(` the worker did not install: ${e.message} — pantheon service install worker`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const repoLabel = (r) => r.origin ?? basename(r.root);
|
|
429
|
+
const repoShare = (r) => (r.origin ? r.origin.replace(/^.*[:/]([^:/]+)$/, "$1") : basename(r.root));
|
|
261
430
|
const teamFlag = (cfg) => (flag("space") ? ` --space ${cfg.space}` : "");
|
|
262
431
|
function need() {
|
|
263
432
|
const r = resolveFor(process.cwd());
|
|
@@ -268,8 +437,18 @@ function need() {
|
|
|
268
437
|
die(`you are not in a team called "${flag("space")}". Yours: ${Object.values(f?.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
269
438
|
return r.cfg && (r.cfg.spaceId === m.id || r.cfg.space === m.name) && r.reason !== "default" ? r.cfg : configFor(f, m, null);
|
|
270
439
|
}
|
|
271
|
-
if (r.cfg && r.cfg.space)
|
|
440
|
+
if (r.cfg && r.cfg.space) {
|
|
441
|
+
if (r.autoBound && r.repo && r.cfg.spaceId) {
|
|
442
|
+
try {
|
|
443
|
+
bindRepo(r.repo, r.cfg.spaceId);
|
|
444
|
+
console.error(D(` ${r.repo.origin ?? r.repo.root} is one of "${r.cfg.space}"'s repos — bound here`));
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
}
|
|
272
448
|
return r.cfg;
|
|
449
|
+
}
|
|
450
|
+
if (r.claimedBy?.length)
|
|
451
|
+
die(`this repo is listed by more than one of your teams (${r.claimedBy.join(", ")}) — pick one: pantheon use <team> --here (or name it: --space <team>)`);
|
|
273
452
|
if (r.repoUnknown)
|
|
274
453
|
die(`git did not answer in this folder, so which team this repo is bound to cannot be told — and a command for the wrong team is worse than none.\n Check that git works here (git status), then run it again — or name the team: --space <team>`);
|
|
275
454
|
if (r.reason === "unbound") {
|
|
@@ -343,6 +522,16 @@ function printGranted(granted) {
|
|
|
343
522
|
console.error(` \u2022 ${g}`);
|
|
344
523
|
console.error(D(` Undo: pantheon hook uninstall · pantheon service uninstall worker`));
|
|
345
524
|
}
|
|
525
|
+
function printRelayNote(relay) {
|
|
526
|
+
let host = relay ?? "the relay";
|
|
527
|
+
try {
|
|
528
|
+
if (relay)
|
|
529
|
+
host = new URL(relay.replace(/^ws/, "http")).host;
|
|
530
|
+
}
|
|
531
|
+
catch { }
|
|
532
|
+
console.error(D(`\n Your team's messages go through ${host}. Whoever runs it can read them (they are not end-to-end encrypted), so treat the team like a shared channel;`));
|
|
533
|
+
console.error(D(` on a relay from 0.31 on, it cannot write or change one in a teammate's name without your machine noticing: every message is signed by its sender. Your own relay: pantheon relay`));
|
|
534
|
+
}
|
|
346
535
|
const HUD_ACTION_MAX_AGE_MS = 30_000;
|
|
347
536
|
function unattendedProject() {
|
|
348
537
|
if (argv.includes("--no-worker"))
|
|
@@ -362,7 +551,23 @@ async function guidedSetup(cfg, project, opts = {}) {
|
|
|
362
551
|
const io = rl ? terminalIO(rl, (l) => console.error(l)) : null;
|
|
363
552
|
console.error(`\n${B("A few choices, then you're done.")}${io ? "" : Object.keys(preset).length ? D(" (applying the answers you passed)") : D(" (no terminal — taking the defaults)")}`);
|
|
364
553
|
try {
|
|
365
|
-
|
|
554
|
+
let serves = null;
|
|
555
|
+
if (!project && cfg.spaceId) {
|
|
556
|
+
try {
|
|
557
|
+
const ws = (await import("./worker-fleet.js")).teamWorkspaces(loadAll(), cfg.spaceId);
|
|
558
|
+
if (ws.length)
|
|
559
|
+
serves = { path: ws[0].path, repos: ws.some((w) => w.kind === "repo") };
|
|
560
|
+
}
|
|
561
|
+
catch { }
|
|
562
|
+
}
|
|
563
|
+
const { agentsNeedingUnsandboxed } = await import("./agents.js");
|
|
564
|
+
const r = await runGuide(io, { cfg, project, optOut: optOutsFrom(argv), serves, ...(opts.founder !== undefined ? { founder: opts.founder } : {}) }, {
|
|
565
|
+
unsandboxedAgents: () => { try {
|
|
566
|
+
return agentsNeedingUnsandboxed();
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return [];
|
|
570
|
+
} },
|
|
366
571
|
publish: async (p) => {
|
|
367
572
|
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
368
573
|
try {
|
|
@@ -632,6 +837,7 @@ ${B("day to day")}
|
|
|
632
837
|
${B("control")}
|
|
633
838
|
pantheon admit <who> let someone in (--may-wake: the team lets them wake workers, and this machine allows them)
|
|
634
839
|
pantheon wake allow <who> let a teammate wake THIS machine; wake deny <who> takes it back; wake list shows who can
|
|
840
|
+
pantheon trust [<who>] the teammates' keys this machine checks their messages against; trust <who> adopts a changed one
|
|
635
841
|
(this machine only — nothing is sent to the team; --space <team> to pick one)
|
|
636
842
|
pantheon admit <who> --revoke remove them
|
|
637
843
|
pantheon worker stay answerable while you are away
|
|
@@ -651,7 +857,7 @@ ${B("control")}
|
|
|
651
857
|
(auto-detects claude / codex / cursor / copilot / kiro on this machine)
|
|
652
858
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
653
859
|
pantheon leave remove what pantheon put on this machine
|
|
654
|
-
pantheon panic
|
|
860
|
+
pantheon panic emergency stop: ends the run in flight and the worker, and switches off wake rights in every team you are in
|
|
655
861
|
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
656
862
|
pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
|
|
657
863
|
pantheon setup three questions — your name, your role, may a woken run execute commands —
|
|
@@ -701,6 +907,7 @@ const COMMAND_FLAGS = {
|
|
|
701
907
|
invite: "--list --revoke <token|all> --may-wake | --no-wake --ttl <minutes>",
|
|
702
908
|
admit: "<member> --may-wake | --no-wake --revoke --forget --yes",
|
|
703
909
|
wake: "allow <member> | deny <member> | list --space <team>",
|
|
910
|
+
trust: "[<member>] --yes --space <team>",
|
|
704
911
|
say: "<ACT> <text> --to <member>[,<member>] --goal <id> --ref <id | seq | last> --summary '<one line>' --domains <a,b> --branch --commit --remote (DELIVERED) --space <team>",
|
|
705
912
|
goal: "list | open \"<title>\" --with <member>[,…] [--acceptance \"…\"] [--deadline <ISO time>] [--list] | close <id> \"<summary>\"",
|
|
706
913
|
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
@@ -713,7 +920,7 @@ const COMMAND_FLAGS = {
|
|
|
713
920
|
doctor: "--share --project <dir>",
|
|
714
921
|
relay: "--port <n> --host <address> --data <dir>",
|
|
715
922
|
use: "<team> --here --yes · pantheon use --unbind (this folder or repo bound to no team again)",
|
|
716
|
-
repo: "add [--base <branch>] | list [--all]",
|
|
923
|
+
repo: "add [--base <branch>] | list [--all] | find [<folder>] [--recents] [--yes]",
|
|
717
924
|
login: "--email <address> --relay <url> --label <device name>",
|
|
718
925
|
signup: "--email <address> --username <name> --relay <url> --label <device name>",
|
|
719
926
|
audit: "--space <team> --since <hours> --json",
|
|
@@ -762,7 +969,7 @@ function slipDistance(a, b) {
|
|
|
762
969
|
const EXTRA_FLAGS = "--agent-cmd --approve-all --ask-before-sending --child --code --data --device --display --force --help --host --idle --keywords --local " +
|
|
763
970
|
"--no-continue --no-guide --no-notify --path --peek --poll --portable --publish --queued --quiet --remind --scratch --session --surface --token --unattended " +
|
|
764
971
|
"--until --user --version --all --json " +
|
|
765
|
-
"--name --title --wake --commands --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities";
|
|
972
|
+
"--name --title --wake --commands --unsandboxed --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities";
|
|
766
973
|
const KNOWN_FLAGS = new Set([...`${Object.values(COMMAND_FLAGS).join(" ")} ${EXTRA_FLAGS}`.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]));
|
|
767
974
|
function refuseUnknownFlags() {
|
|
768
975
|
if (["mcp", "hook", "tray-status", "_menubar"].includes(cmd))
|
|
@@ -895,6 +1102,7 @@ async function main() {
|
|
|
895
1102
|
for (const w of r.warnings)
|
|
896
1103
|
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", r.cfg.relay) : ""));
|
|
897
1104
|
printGranted(r.granted);
|
|
1105
|
+
printRelayNote(r.cfg?.relay);
|
|
898
1106
|
rememberWorkingProxy();
|
|
899
1107
|
if (!r.claimed) {
|
|
900
1108
|
console.error(`\n\x1b[31m No team was created.\x1b[0m ${D("Fix the warning above, then run `pantheon start` again.")}`);
|
|
@@ -914,21 +1122,25 @@ async function main() {
|
|
|
914
1122
|
console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
|
|
915
1123
|
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
916
1124
|
}
|
|
917
|
-
console.error(D(` From
|
|
1125
|
+
console.error(D(` From the folder they work in — a clone of the team's repo, or any folder for docs and plans (no git needed). Single use, expires in ${Math.round(numFlag("ttl", 1440) / 60)}h.`));
|
|
918
1126
|
console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
|
|
919
1127
|
console.error(r.inviteMayWake
|
|
920
1128
|
? ` ${B("It also lets them wake workers:")} ${D("their requests can start an agent run on this machine while you are away (you chose wake \"anyone\"). Take it back: pantheon admit <name> --revoke")}`
|
|
921
|
-
: D(` It lets them join and talk to your agent. It does
|
|
1129
|
+
: D(` It lets them join and talk to your agent. It does not let them wake this machine on its own: the first time their request needs it while you are away, you are asked (Always / Just this once / No). Approve ahead: pantheon wake allow <name>`));
|
|
922
1130
|
console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
|
|
923
|
-
console.error(` ${D("Tell your agent:
|
|
1131
|
+
console.error(` ${D("Tell your agent: read")} ${C("https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
|
|
924
1132
|
}
|
|
925
1133
|
if (!r.reach.shareable && r.reach.hint) {
|
|
926
1134
|
console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
|
|
927
1135
|
console.error(D(` ${r.reach.hint}`));
|
|
928
1136
|
}
|
|
1137
|
+
if (r.claimed) {
|
|
1138
|
+
await bindReposHere(argv.includes("--yes"));
|
|
1139
|
+
await workerAfterBinding(wp.how);
|
|
1140
|
+
}
|
|
929
1141
|
printReady(r.tools);
|
|
930
1142
|
if (r.claimed)
|
|
931
|
-
await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
|
|
1143
|
+
await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes"), founder: true });
|
|
932
1144
|
if (r.relay) {
|
|
933
1145
|
console.error(D(`\nKeeping the relay running here. Ctrl-C stops it (your teammate goes offline too).`));
|
|
934
1146
|
await new Promise(() => { });
|
|
@@ -976,7 +1188,7 @@ async function main() {
|
|
|
976
1188
|
console.error(`\n${D("Or send this — it unfurls in chat with the team's name, and pantheon join accepts it too:")}\n ${C(link)}`);
|
|
977
1189
|
console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
|
|
978
1190
|
console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
|
|
979
|
-
console.error(` ${D("Tell your agent:
|
|
1191
|
+
console.error(` ${D("Tell your agent: read")} ${C("https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? raw}`)}`);
|
|
980
1192
|
client.close();
|
|
981
1193
|
return;
|
|
982
1194
|
}
|
|
@@ -1119,15 +1331,46 @@ async function main() {
|
|
|
1119
1331
|
for (const w of r.warnings)
|
|
1120
1332
|
console.error(`\x1b[33m ! ${w}\x1b[0m` + (/NOT_SIGNED_IN/.test(w) ? signInHint("NOT_SIGNED_IN", parsed.relay) : ""));
|
|
1121
1333
|
printGranted(r.granted);
|
|
1334
|
+
printRelayNote(r.cfg?.relay ?? parsed.relay);
|
|
1122
1335
|
rememberWorkingProxy();
|
|
1123
1336
|
printReady(r.tools);
|
|
1124
1337
|
{
|
|
1125
1338
|
const jc = load();
|
|
1126
|
-
const mode = jc ? loadProfile(jc)
|
|
1339
|
+
const mode = jc ? wakeMode(loadProfile(jc)) : "anyone";
|
|
1127
1340
|
console.error(D(mode === "anyone"
|
|
1128
|
-
? `\n
|
|
1129
|
-
:
|
|
1341
|
+
? `\n Your teammates can hand work to this machine while you're away, so nothing waits on you. To approve each teammate first: pantheon setup --wake approved`
|
|
1342
|
+
: mode === "no"
|
|
1343
|
+
? `\n This machine is set not to be woken: teammates' requests wait for you. Change it: pantheon setup`
|
|
1344
|
+
: `\n The first time a teammate's request needs this machine while you are away, you are asked — "Always / Just this once / No" in the menu bar. Approve someone ahead: pantheon wake allow <name>`));
|
|
1345
|
+
}
|
|
1346
|
+
if (process.stdin.isTTY)
|
|
1347
|
+
await bindReposHere(false);
|
|
1348
|
+
{
|
|
1349
|
+
const j = load();
|
|
1350
|
+
let teamRepos = [];
|
|
1351
|
+
if (j) {
|
|
1352
|
+
const { repo: _r, repoOrigin: _o, ...plain } = j;
|
|
1353
|
+
const c = new PantheonClient(plain, undefined, { role: "cli" });
|
|
1354
|
+
try {
|
|
1355
|
+
await Promise.race([c.connect(), new Promise((_, no) => setTimeout(() => no(new Error("timeout")), 8000))]);
|
|
1356
|
+
await new Promise((res) => setTimeout(res, 150));
|
|
1357
|
+
teamRepos = c.repos.map((r) => ({ fingerprint: r.fingerprint, origin: r.origin ?? null }));
|
|
1358
|
+
}
|
|
1359
|
+
catch { }
|
|
1360
|
+
finally {
|
|
1361
|
+
c.close();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const all = loadAll();
|
|
1365
|
+
const m = all && j ? findMembership(all, j.spaceId ?? j.space) ?? findMembership(all, j.space) : null;
|
|
1366
|
+
if (!teamRepos.length && m?.repos?.length)
|
|
1367
|
+
teamRepos = m.repos.map((r) => ({ fingerprint: r.fp, origin: r.name }));
|
|
1368
|
+
if (all && m && teamRepos.length) {
|
|
1369
|
+
console.error("");
|
|
1370
|
+
await findAndBindRepos(all, m, teamRepos, { auto: !process.stdin.isTTY }).catch(() => { });
|
|
1371
|
+
}
|
|
1130
1372
|
}
|
|
1373
|
+
await workerAfterBinding(wp.how);
|
|
1131
1374
|
{
|
|
1132
1375
|
const joined = load();
|
|
1133
1376
|
if (joined)
|
|
@@ -1324,7 +1567,7 @@ async function main() {
|
|
|
1324
1567
|
for (const p of peers) {
|
|
1325
1568
|
const lag = p.seen_seq === undefined ? null : client.head - p.seen_seq;
|
|
1326
1569
|
const behind = lag === null ? null : lag <= 0 ? 0
|
|
1327
|
-
: recent ? recent.filter((a) => a.seq > p.seen_seq && a.from === cfg.member && a.act !== "JOIN" && a.act !== "LEAVE").length : lag;
|
|
1570
|
+
: recent ? recent.filter((a) => a.seq > p.seen_seq && a.from === cfg.member && a.act !== "JOIN" && a.act !== "LEAVE" && !a.meta?.organization_profile).length : lag;
|
|
1328
1571
|
const reach = behind === null
|
|
1329
1572
|
? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
|
|
1330
1573
|
? "delivered, but no read-receipt — no wake-up hook on their machine"
|
|
@@ -1354,6 +1597,12 @@ async function main() {
|
|
|
1354
1597
|
for (const o of human) {
|
|
1355
1598
|
const who = o.owner.replace(/^human:/, "");
|
|
1356
1599
|
const stale = Date.now() - o.created_at > 3_600_000;
|
|
1600
|
+
const askAct = client.seenAct(o.ref);
|
|
1601
|
+
const wakeAsk = askAct?.from === who ? askAct.meta?.wake_ask : undefined;
|
|
1602
|
+
if (who !== cfg.member && typeof wakeAsk === "string") {
|
|
1603
|
+
console.log(` ${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${D(`is being asked on their machine whether ${wakeAsk === cfg.member ? "you" : wakeAsk} may start work there while they are away`)}`);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1357
1606
|
console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body.split("\n")[0].slice(0, 110)} ${D(o.ref)}`);
|
|
1358
1607
|
if (who === cfg.member)
|
|
1359
1608
|
console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
|
|
@@ -1875,7 +2124,8 @@ async function main() {
|
|
|
1875
2124
|
catch { }
|
|
1876
2125
|
try {
|
|
1877
2126
|
const { reportActivity } = await import("./pending.js");
|
|
1878
|
-
|
|
2127
|
+
if (hookCfg)
|
|
2128
|
+
await reportActivity({ cfg: hookCfg, session: `hook:${session}`, state: "busy" });
|
|
1879
2129
|
}
|
|
1880
2130
|
catch { }
|
|
1881
2131
|
console.log(JSON.stringify({ continue: true }));
|
|
@@ -2351,7 +2601,7 @@ async function main() {
|
|
|
2351
2601
|
}
|
|
2352
2602
|
console.error(` ${D("cannot")} touch your own checkout's working tree, or merge anything`);
|
|
2353
2603
|
console.error(` ${D("limits")} one run at a time, 12 per hour, and only while a worker is running`);
|
|
2354
|
-
console.error(`\n ${D("
|
|
2604
|
+
console.error(`\n ${D("Runs happen only while a worker runs here (pantheon ps shows it). Undo: pantheon wake deny " + who + " (this machine) or pantheon admit " + who + " --no-wake (the team). Emergency: pantheon panic — it also switches off wake rights in every team you are in.")}`);
|
|
2355
2605
|
if (argv.includes("--yes")) {
|
|
2356
2606
|
console.error(` ${D("--yes given: granted without prompting.")}`);
|
|
2357
2607
|
}
|
|
@@ -2823,6 +3073,72 @@ async function main() {
|
|
|
2823
3073
|
client.close();
|
|
2824
3074
|
return;
|
|
2825
3075
|
}
|
|
3076
|
+
case "trust": {
|
|
3077
|
+
const { WOKEN_RUN_ENV } = await import("./worker-policy.js");
|
|
3078
|
+
const inRun = process.env[WOKEN_RUN_ENV] !== undefined || /[\\/]run-homes[\\/]/.test(process.env.PANTHEON_HOME ?? "");
|
|
3079
|
+
if (inRun && argv[1] && !argv[1].startsWith("--"))
|
|
3080
|
+
die("a woken run cannot trust a key — a person has to, on this machine");
|
|
3081
|
+
const cfg = need();
|
|
3082
|
+
const { loadPins, trustPending, updatePins } = await import("./pins.js");
|
|
3083
|
+
const { fingerprint } = await import("./identity.js");
|
|
3084
|
+
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
3085
|
+
await Promise.race([c.connect().catch(() => { }), new Promise((r) => setTimeout(r, 5000))]);
|
|
3086
|
+
const team = c.pinKey();
|
|
3087
|
+
c.close();
|
|
3088
|
+
const p = loadPins();
|
|
3089
|
+
const t = p.teams[team];
|
|
3090
|
+
const who = argv[1] && !argv[1].startsWith("--") ? argv[1] : null;
|
|
3091
|
+
if (!who) {
|
|
3092
|
+
console.log(`${B("Teammates' keys this machine checks messages against")} ${D(`(${cfg.space}; messages ${t?.sigs ? "are" : "are not yet"} signed on this team's relay)`)}\n`);
|
|
3093
|
+
for (const [m, pin] of Object.entries(t?.members ?? {}).sort()) {
|
|
3094
|
+
console.log(` ${m.padEnd(16)} ${pin.fp}${m === cfg.member ? D(" (you)") : ""}${pin.pending ? `\n ${" ".repeat(16)} ${B("changed to")} ${pin.pending.fp} ${D(`— not trusted until you check it with ${m}: pantheon trust ${m}`)}` : ""}`);
|
|
3095
|
+
}
|
|
3096
|
+
if (!t || !Object.keys(t.members).length)
|
|
3097
|
+
console.log(D(" none yet — keys are pinned the first time this machine sees each teammate"));
|
|
3098
|
+
console.log(D(`\n Your own key: ${fingerprint(cfg.publicKey)} — read it to a teammate who asks you to confirm it.`));
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const cur = t?.members[who];
|
|
3102
|
+
if (!cur)
|
|
3103
|
+
die(`no key for "${who}" is known on this machine in "${cfg.space}"`);
|
|
3104
|
+
if (!cur.pending) {
|
|
3105
|
+
console.log(`${C(who)}'s key is ${cur.fp} — nothing is waiting to be confirmed.`);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
console.log(`${B(`${who}'s key changed`)} (a new machine, or someone else):`);
|
|
3109
|
+
console.log(` was ${cur.fp}`);
|
|
3110
|
+
console.log(` now ${cur.pending.fp}`);
|
|
3111
|
+
console.log(D(` Ask ${who} to read theirs out (pantheon trust, last line) and compare. Only then trust it.`));
|
|
3112
|
+
let go = argv.includes("--yes");
|
|
3113
|
+
if (!go && process.stdin.isTTY) {
|
|
3114
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3115
|
+
const a = ((await questionOrNull(rl, `Trust ${who}'s new key ${cur.pending.fp}? [y/N] `)) ?? "").trim().toLowerCase();
|
|
3116
|
+
rl.close();
|
|
3117
|
+
go = a === "y" || a === "yes";
|
|
3118
|
+
}
|
|
3119
|
+
if (!go) {
|
|
3120
|
+
console.log(D(process.stdin.isTTY ? " nothing changed" : ` nothing changed — after checking it with ${who}: pantheon trust ${who} --yes`));
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
let outcome = "ok";
|
|
3124
|
+
updatePins((fresh) => {
|
|
3125
|
+
if (fresh.corrupt) {
|
|
3126
|
+
outcome = "corrupt";
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
if (fresh.teams[team]?.members[who]?.pending?.fp !== cur.pending.fp) {
|
|
3130
|
+
outcome = "moved";
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
return !!trustPending(fresh, team, who);
|
|
3134
|
+
});
|
|
3135
|
+
if (outcome === "corrupt")
|
|
3136
|
+
die("keys.json could not be read, so nothing was changed — fix or remove it (pantheon doctor)");
|
|
3137
|
+
if (outcome === "moved")
|
|
3138
|
+
die(`${who}'s held-back key changed while you were deciding — run pantheon trust ${who} again`);
|
|
3139
|
+
console.log(`${C("trusted")} ${who}'s new key on this machine — their signed messages count again.`);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
2826
3142
|
case "wake": {
|
|
2827
3143
|
const { loadPolicy, setLocalWaker } = await import("./worker-policy.js");
|
|
2828
3144
|
const { wakeMode } = await import("./profile.js");
|
|
@@ -2842,11 +3158,16 @@ async function main() {
|
|
|
2842
3158
|
if (who === cfg.member)
|
|
2843
3159
|
die("your own requests always reach your own worker; there is nothing to allow");
|
|
2844
3160
|
let unchecked = false;
|
|
3161
|
+
let teamBlocks = false;
|
|
2845
3162
|
if (sub === "allow") {
|
|
2846
3163
|
const c = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2847
3164
|
const got = await Promise.race([c.connect().then(() => true, () => false), new Promise((r) => setTimeout(() => r(false), 3_000))]);
|
|
2848
3165
|
if (got) {
|
|
2849
3166
|
const names = c.members.map((m) => m.member);
|
|
3167
|
+
{
|
|
3168
|
+
const e = c.members.find((m) => m.member === who);
|
|
3169
|
+
teamBlocks = e?.admitted === true && !e.revoked_by && e.may_wake === false;
|
|
3170
|
+
}
|
|
2850
3171
|
c.close();
|
|
2851
3172
|
if (!names.includes(who))
|
|
2852
3173
|
die(`"${who}" is not in "${cfg.space}". Its members: ${names.filter((n) => n !== cfg.member).join(", ") || "only you"}`);
|
|
@@ -2874,7 +3195,14 @@ async function main() {
|
|
|
2874
3195
|
console.log(D(` This machine is set to wake for anyone the team lets, so the list is used only if you choose "approved" (pantheon setup).`));
|
|
2875
3196
|
if (mode === "no")
|
|
2876
3197
|
console.log(D(` This machine is set not to be woken at all; the list applies once you choose "approved" (pantheon setup).`));
|
|
2877
|
-
|
|
3198
|
+
if (teamBlocks) {
|
|
3199
|
+
const { markTeamFlagOnYes } = await import("./worker-policy.js");
|
|
3200
|
+
markTeamFlagOnYes(cfg.spaceId ?? cfg.space, who);
|
|
3201
|
+
}
|
|
3202
|
+
if (teamBlocks)
|
|
3203
|
+
console.log(` ${B("The team does not let")} ${C(who)} ${B("wake workers yet.")} ${D(`Your worker turns that on the first time ${who}'s request needs this machine (it is said when it does); or now: pantheon admit ${who} --may-wake${teamFlag(cfg)}`)}`);
|
|
3204
|
+
else if (unchecked)
|
|
3205
|
+
console.log(D(` The team must also let ${who} wake workers at all. If their requests are refused with NOT_ALLOWED_TO_WAKE, a member runs: pantheon admit ${who} --may-wake${teamFlag(cfg)}`));
|
|
2878
3206
|
}
|
|
2879
3207
|
return;
|
|
2880
3208
|
}
|
|
@@ -2922,7 +3250,11 @@ async function main() {
|
|
|
2922
3250
|
savePolicy(p);
|
|
2923
3251
|
const said = cap === "self_upgrade"
|
|
2924
3252
|
? (sub === "grant" ? "— the worker keeps pantheon current with the relay" : "— the worker will ask in your approvals inbox when it falls behind")
|
|
2925
|
-
:
|
|
3253
|
+
: cap === "unsandboxed_runs"
|
|
3254
|
+
? (sub === "grant"
|
|
3255
|
+
? "— Codex, Cursor and Copilot answer teammates' requests while you're away, so nobody waits on you. To have them wait for you instead: pantheon policy revoke unsandboxed-runs"
|
|
3256
|
+
: "— Codex, Cursor and Copilot leave teammates' requests for you to pick up when you're back; Claude Code still answers them. On again: pantheon policy grant unsandboxed-runs")
|
|
3257
|
+
: (sub === "grant" ? `— ${COMMAND_GRANT_SCOPE}` : "— woken runs are back to edits only");
|
|
2926
3258
|
console.log(`${C(sub === "grant" ? "granted" : "revoked")} ${CAPABILITY_ARGS[cap]} ${D(said)}`);
|
|
2927
3259
|
return;
|
|
2928
3260
|
}
|
|
@@ -2937,13 +3269,20 @@ async function main() {
|
|
|
2937
3269
|
const rc = p.grants.run_commands;
|
|
2938
3270
|
const { commandState, repoCommandsAtBase } = await import("./command-state.js");
|
|
2939
3271
|
const cs = commandState({ project: resolve(".") });
|
|
3272
|
+
const { commandCaveat } = await import("./command-state.js");
|
|
3273
|
+
const caveat = cs.level !== "off" ? commandCaveat() : "";
|
|
2940
3274
|
console.log(` run-commands ${rc ? C(rc.mode === "always" ? "granted" : "granted once") : D("not granted")}${rc ? D(` (${rc.by}, ${new Date(rc.at).toLocaleDateString()})`) : cs.allowBash ? D(" — not needed: the worker runs with --allow-bash") : D(" — woken runs edit files only; it asks when a run needed more")}`);
|
|
3275
|
+
const ur = p.grants.unsandboxed_runs;
|
|
3276
|
+
const { unsandboxedAllowed } = await import("./worker-policy.js");
|
|
3277
|
+
console.log(` unsandboxed ${unsandboxedAllowed(p)
|
|
3278
|
+
? C("on") + D(` (${ur ? `${ur.by}, ${new Date(ur.at).toLocaleDateString()}` : "the default"}) — Codex, Cursor and Copilot answer teammates while you're away (off: pantheon policy revoke unsandboxed-runs)`)
|
|
3279
|
+
: D("off (your choice) — Codex, Cursor and Copilot leave requests for when you're back; Claude Code still answers them (on: pantheon policy grant unsandboxed-runs)")}`);
|
|
2941
3280
|
for (const cap of caps)
|
|
2942
3281
|
if (p.asks[cap])
|
|
2943
3282
|
console.log(` ${"\x1b[33mopen request\x1b[0m"} ${CAPABILITY_ARGS[cap]} — answer it here: pantheon approvals, or pantheon policy grant ${CAPABILITY_ARGS[cap]}`);
|
|
2944
3283
|
const pers = loadPersonas(resolve("."), undefined);
|
|
2945
3284
|
console.log(` personas ${pers.length ? pers.map((x) => x.name).join(", ") + D(" (restricted mode — tighten-only)") : D("none — open mode; peers this machine approved to wake it can hand it any matched work")}`);
|
|
2946
|
-
console.log(` commands ${cs.level === "off" ? D(cs.summary) : C(cs.summary)}`);
|
|
3285
|
+
console.log(` commands ${cs.level === "off" ? D(cs.summary) : C(cs.summary)}${caveat ? `\n \x1b[33m${caveat}\x1b[0m` : ""}`);
|
|
2947
3286
|
if (cs.level !== "off")
|
|
2948
3287
|
console.log(` ${D(COMMAND_GRANT_SCOPE)}`);
|
|
2949
3288
|
{
|
|
@@ -2956,7 +3295,7 @@ async function main() {
|
|
|
2956
3295
|
console.log(` ${D(`ignored "${r.line}" — ${r.why}`)}`);
|
|
2957
3296
|
}
|
|
2958
3297
|
console.log(` runs ${D("12/hour, 40/day, one at a time, 2 failed attempts then parked (defaults; personas only lower them)")}`);
|
|
2959
|
-
console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
|
|
3298
|
+
console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands|unsandboxed-runs · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
|
|
2960
3299
|
return;
|
|
2961
3300
|
}
|
|
2962
3301
|
case "explain": {
|
|
@@ -3340,10 +3679,21 @@ async function main() {
|
|
|
3340
3679
|
die("usage: pantheon approvals show <obligation-id>");
|
|
3341
3680
|
const hit = find(id) ?? die(`no decision ${id} is waiting on you`);
|
|
3342
3681
|
console.log(`${B("Decision required")} ${hit.o.id}${conns.length > 1 ? ` ${D("in " + hit.cfg.space)}` : ""}`);
|
|
3682
|
+
const full = hit.client.seenAct(hit.o.ref) ?? (await fetchAct(hit.cfg, hit.o.ref, 4000).catch(() => null))?.act;
|
|
3343
3683
|
console.log(` ${hit.o.body}`);
|
|
3684
|
+
if (full?.body && full.body.trim() !== hit.o.body.trim())
|
|
3685
|
+
console.log(`\n${full.body.split("\n").map((l) => ` ${l}`).join("\n")}`);
|
|
3344
3686
|
if (hit.o.goal)
|
|
3345
3687
|
console.log(` goal ${hit.client.goals.get(hit.o.goal)?.title ?? hit.o.goal}`);
|
|
3346
|
-
|
|
3688
|
+
const { localChoices } = await import("./tray.js");
|
|
3689
|
+
const choices = localChoices(hit.o.ref);
|
|
3690
|
+
if (choices) {
|
|
3691
|
+
console.log(`\nAnswer on this machine, one of:`);
|
|
3692
|
+
for (const c of choices)
|
|
3693
|
+
console.log(` ${c.label.padEnd(16)} ${C(`pantheon approvals answer ${hit.o.id} ${c.answer}`)}`);
|
|
3694
|
+
}
|
|
3695
|
+
else
|
|
3696
|
+
console.log(`\nAnswer: ${C(`pantheon approvals answer ${hit.o.id} 'your decision and rationale'`)}`);
|
|
3347
3697
|
closeAll();
|
|
3348
3698
|
return;
|
|
3349
3699
|
}
|
|
@@ -3351,8 +3701,11 @@ async function main() {
|
|
|
3351
3701
|
die("usage: pantheon approvals [list | show <id> | answer <id> <decision>]");
|
|
3352
3702
|
if (!all.length)
|
|
3353
3703
|
console.log(D(conns.length > 1 ? `no decisions waiting on you in any of your ${conns.length} teams` : "no decisions waiting on you"));
|
|
3354
|
-
|
|
3355
|
-
|
|
3704
|
+
const { localChoices: lc } = await import("./tray.js");
|
|
3705
|
+
for (const { o, cfg, client } of all) {
|
|
3706
|
+
const ch = lc(o.ref);
|
|
3707
|
+
console.log(`${C(o.id)} ${conns.length > 1 ? D(`[${cfg.space}] `) : ""}${client.goals.get(o.goal ?? "")?.title ?? (ch ? "This machine" : "No goal")}\n ${o.body}${ch ? D(`\n answer: ${ch.map((c) => c.answer).join(" / ")} (pantheon approvals show ${o.id})`) : ""}`);
|
|
3708
|
+
}
|
|
3356
3709
|
closeAll();
|
|
3357
3710
|
return;
|
|
3358
3711
|
}
|
|
@@ -3397,11 +3750,11 @@ async function main() {
|
|
|
3397
3750
|
case "worker": {
|
|
3398
3751
|
(await import("./machine.js")).recordWorkerPid();
|
|
3399
3752
|
const allCfg = loadAll();
|
|
3400
|
-
const bindings = Object.
|
|
3753
|
+
const bindings = Object.values(allCfg?.bindings ?? {});
|
|
3401
3754
|
if (!flag("project") && bindings.length) {
|
|
3402
3755
|
const { WorkerFleet } = await import("./worker-fleet.js");
|
|
3403
3756
|
const fleet = new WorkerFleet({
|
|
3404
|
-
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
|
|
3757
|
+
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd"), agentCmdFromCli: true } : { agent: flag("agent", "auto") }),
|
|
3405
3758
|
...(flag("persona") ? { persona: flag("persona") } : {}),
|
|
3406
3759
|
continuations: !argv.includes("--no-continue"),
|
|
3407
3760
|
allowBash: argv.includes("--allow-bash"),
|
|
@@ -3418,7 +3771,7 @@ async function main() {
|
|
|
3418
3771
|
}),
|
|
3419
3772
|
log: (l) => console.error(l),
|
|
3420
3773
|
});
|
|
3421
|
-
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound
|
|
3774
|
+
console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound folder${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
|
|
3422
3775
|
if (argv.includes("--allow-bash")) {
|
|
3423
3776
|
console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
|
|
3424
3777
|
console.error(`\x1b[33m teammate approved to wake this machine (pantheon wake list), since their request is what starts the run.\x1b[0m`);
|
|
@@ -3432,7 +3785,7 @@ async function main() {
|
|
|
3432
3785
|
const worker = new Worker(cfg, {
|
|
3433
3786
|
project,
|
|
3434
3787
|
scratch: resolve(flag("scratch", join(project, ".pantheon", "worktrees"))),
|
|
3435
|
-
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
|
|
3788
|
+
...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd"), agentCmdFromCli: true } : { agent: flag("agent", "auto") }),
|
|
3436
3789
|
...(flag("persona") ? { persona: flag("persona") } : {}),
|
|
3437
3790
|
continuations: !argv.includes("--no-continue"),
|
|
3438
3791
|
allowBash: argv.includes("--allow-bash"),
|
|
@@ -3605,7 +3958,7 @@ async function main() {
|
|
|
3605
3958
|
}
|
|
3606
3959
|
need();
|
|
3607
3960
|
const project = resolve(flag("project", "."));
|
|
3608
|
-
const extra = ["--project", project];
|
|
3961
|
+
const extra = flag("project") ? ["--project", project] : [];
|
|
3609
3962
|
if (argv.includes("--allow-bash"))
|
|
3610
3963
|
extra.push("--allow-bash");
|
|
3611
3964
|
if (argv.includes("--no-continue"))
|
|
@@ -3615,10 +3968,10 @@ async function main() {
|
|
|
3615
3968
|
extra.push(`--${f}`, flag(f));
|
|
3616
3969
|
}
|
|
3617
3970
|
console.error(`${B("installing the pantheon worker as a background service")}`);
|
|
3618
|
-
console.error(D(` project ${project}`));
|
|
3971
|
+
console.error(D(flag("project") ? ` project ${project} (this checkout only)` : ` serves every repo and folder bound to a team on this machine`));
|
|
3619
3972
|
for (const l of installService(kind, project, extra))
|
|
3620
3973
|
console.error(l);
|
|
3621
|
-
console.error(`\n${D("It only acts on requests from teammates approved on this machine
|
|
3974
|
+
console.error(`\n${D("It only acts on requests from teammates approved on this machine — you are asked the first time each one needs it — one run at a time.")}`);
|
|
3622
3975
|
return;
|
|
3623
3976
|
}
|
|
3624
3977
|
case "spaces": {
|
|
@@ -3638,6 +3991,10 @@ async function main() {
|
|
|
3638
3991
|
console.error(` ${D(b.origin === null && !existsSync(join(b.path, ".git")) ? "folder" : "repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
|
|
3639
3992
|
if (!bound.length)
|
|
3640
3993
|
console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
|
|
3994
|
+
const boundFps = new Set(Object.entries(all.bindings ?? {}).filter(([, b]) => b.space === m.id).map(([k]) => k));
|
|
3995
|
+
const missing = (m.repos ?? []).filter((r) => !boundFps.has(r.fp)).map((r) => r.name);
|
|
3996
|
+
if (missing.length)
|
|
3997
|
+
console.error(` ${D(`not on this machine: ${missing.join(", ")} — pantheon repo find, once you have ${missing.length === 1 ? "it" : "them"} cloned`)}`);
|
|
3641
3998
|
}
|
|
3642
3999
|
const r = repoInfo(process.cwd());
|
|
3643
4000
|
console.error(`\n ${D("this folder:")} ${r ? (r.origin ?? r.root) : D("not a git repo")} ${D("→")} ${here.reason === "binding" ? `"${here.cfg.space}"` : here.reason === "single" ? `"${here.cfg.space}" (your only team)` : here.reason === "default" ? `unbound — humans get the default "${here.cfg.space}"; agents refuse` : here.reason === "stale" ? "bound to a team you have left" : "unbound"}`);
|
|
@@ -3679,9 +4036,17 @@ async function main() {
|
|
|
3679
4036
|
if ((here === real(homedir()) || dirname(here) === here) && !argv.includes("--yes")) {
|
|
3680
4037
|
die(`this is ${dirname(here) === here ? "the root of the disk" : "your home folder"}: binding it puts every folder under it in "${m.name}", and an agent opened anywhere there would speak for that team.\n Bind a project folder instead, or if you mean it: pantheon use ${m.name} --here --yes`);
|
|
3681
4038
|
}
|
|
4039
|
+
if ((await import("./repo-find.js")).holdsRepo(process.cwd())) {
|
|
4040
|
+
const n = await bindReposIn(process.cwd(), all, m, argv.includes("--yes"), `pantheon use ${m.name} --here --yes`);
|
|
4041
|
+
if (!n)
|
|
4042
|
+
console.error(D(` nothing bound — the folder itself is not bound, since it holds repos`));
|
|
4043
|
+
else
|
|
4044
|
+
console.error(D(` Agents opened in those repos now speak as you in "${m.name}". Restart any that are open.`));
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
3682
4047
|
const { replaced } = bindFolder(process.cwd(), m.id);
|
|
3683
|
-
console.error(`${B("bound")} this folder
|
|
3684
|
-
console.error(D(` Agents opened here, or below
|
|
4048
|
+
console.error(`${B("bound")} this folder → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
|
|
4049
|
+
console.error(D(` Agents opened here, or in any folder below, now speak as you in "${m.name}". Restart any that are open.`));
|
|
3685
4050
|
return;
|
|
3686
4051
|
}
|
|
3687
4052
|
const { replaced } = bindRepo(r, m.id);
|
|
@@ -3710,6 +4075,15 @@ async function main() {
|
|
|
3710
4075
|
const { repo: _r, repoOrigin: _o, ...plain } = cfg;
|
|
3711
4076
|
const client = new PantheonClient(plain, undefined, { role: "cli" });
|
|
3712
4077
|
await client.connect().catch((e) => die(client.pending ?? connectFailure(e, plain.relay)));
|
|
4078
|
+
if (sub === "find") {
|
|
4079
|
+
await new Promise((res) => setTimeout(res, 150));
|
|
4080
|
+
const all = loadAll() ?? die("not configured");
|
|
4081
|
+
const m = findMembership(all, cfg.spaceId ?? cfg.space) ?? findMembership(all, cfg.space) ?? die(`not in "${cfg.space}" on this machine`);
|
|
4082
|
+
const where = argv[2] && !argv[2].startsWith("--") ? resolve(argv[2]) : null;
|
|
4083
|
+
client.close();
|
|
4084
|
+
await findAndBindRepos(all, m, client.repos, { ...(where ? { extra: [where] } : {}), recents: argv.includes("--recents") ? true : undefined, yes: argv.includes("--yes") });
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
3713
4087
|
if (sub === "add") {
|
|
3714
4088
|
const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
|
|
3715
4089
|
const base = flag("base") ?? null;
|