@join-pantheon/cli 0.30.0 → 0.31.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/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";
@@ -18,9 +18,9 @@ import { install, launcher, installForUser } from "./install.js";
18
18
  import { Relay } from "./relay.js";
19
19
  import { runMcp } from "./mcp.js";
20
20
  import { Worker } from "./worker.js";
21
- import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
21
+ import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, runNotifiers, 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
- const r = await runGuide(io, { cfg, project, optOut: optOutsFrom(argv) }, {
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 cut off all unattended work on this machine, now
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 their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(numFlag("ttl", 1440) / 60)}h.`));
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 NOT let them wake this machine: approve a person on this machine with pantheon wake allow <name>.`));
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: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
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: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? raw}`)}`);
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).agent.wake_default ?? "approved" : "approved";
1339
+ const mode = jc ? wakeMode(loadProfile(jc)) : "anyone";
1127
1340
  console.error(D(mode === "anyone"
1128
- ? `\n You chose wake "anyone": teammates this team lets wake workers can start an agent run on this machine. Change it: pantheon setup`
1129
- : `\n Nobody on the team can wake this machine until you approve them, here: pantheon wake allow <name>${mode === "no" ? "" : " (or choose \"anyone\" below)"}.`));
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>`));
1130
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
+ }
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
- await reportActivity({ session: `hook:${session}`, state: "busy" });
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("Nothing happens at all unless you run `pantheon worker`. Undo instantly: pantheon panic")}`);
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
- 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)}`));
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
- : (sub === "grant" ? `— ${COMMAND_GRANT_SCOPE}` : "— woken runs are back to edits only");
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": {
@@ -3134,8 +3473,8 @@ async function main() {
3134
3473
  return;
3135
3474
  }
3136
3475
  const open = o.goal && c2.goals.get(o.goal)?.state !== "CLOSED" ? o.goal : null;
3137
- await c2.send({ act: "ANSWER", ref: o.ref, goal: open, after_seq: c2.readSeq,
3138
- body: "Marked done from the alert — handled outside this thread." }).catch((e) => die(e.message));
3476
+ const doneBody = "Marked done from the alert — handled outside this thread.";
3477
+ await c2.send({ act: "ANSWER", ref: o.ref, goal: open, after_seq: c2.readSeq, body: doneBody }).catch((e) => die(e.message));
3139
3478
  c2.close();
3140
3479
  console.error(D(`done — ${o.id} is no longer waiting on you`));
3141
3480
  return;
@@ -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
- console.log(`\nAnswer: ${C(`pantheon approvals answer ${hit.o.id} 'your decision and rationale'`)}`);
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
- for (const { o, cfg, client } of all)
3355
- console.log(`${C(o.id)} ${conns.length > 1 ? D(`[${cfg.space}] `) : ""}${client.goals.get(o.goal ?? "")?.title ?? "No goal"}\n ${o.body}`);
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.entries(allCfg?.bindings ?? {}).filter(([k]) => !k.startsWith("d_")).map(([, b]) => b);
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 repo${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
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"),
@@ -3523,7 +3876,9 @@ async function main() {
3523
3876
  n.stop(); process.exit(0); };
3524
3877
  process.on("SIGINT", stopAll);
3525
3878
  process.on("SIGTERM", stopAll);
3526
- await Promise.all(notifiers.map((n) => n.run()));
3879
+ const ok = await runNotifiers(notifiers.map((n, i) => ({ label: cfgs[i].space, run: () => n.run(), stopped: () => n.isStopped })), { log: (line) => console.error(line) });
3880
+ if (!ok)
3881
+ process.exit(1);
3527
3882
  return;
3528
3883
  }
3529
3884
  case "doctor": {
@@ -3605,7 +3960,7 @@ async function main() {
3605
3960
  }
3606
3961
  need();
3607
3962
  const project = resolve(flag("project", "."));
3608
- const extra = ["--project", project];
3963
+ const extra = flag("project") ? ["--project", project] : [];
3609
3964
  if (argv.includes("--allow-bash"))
3610
3965
  extra.push("--allow-bash");
3611
3966
  if (argv.includes("--no-continue"))
@@ -3615,10 +3970,10 @@ async function main() {
3615
3970
  extra.push(`--${f}`, flag(f));
3616
3971
  }
3617
3972
  console.error(`${B("installing the pantheon worker as a background service")}`);
3618
- console.error(D(` project ${project}`));
3973
+ console.error(D(flag("project") ? ` project ${project} (this checkout only)` : ` serves every repo and folder bound to a team on this machine`));
3619
3974
  for (const l of installService(kind, project, extra))
3620
3975
  console.error(l);
3621
- console.error(`\n${D("It only acts on requests from teammates approved on this machine (pantheon wake allow <name>), one run at a time.")}`);
3976
+ 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
3977
  return;
3623
3978
  }
3624
3979
  case "spaces": {
@@ -3638,6 +3993,10 @@ async function main() {
3638
3993
  console.error(` ${D(b.origin === null && !existsSync(join(b.path, ".git")) ? "folder" : "repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
3639
3994
  if (!bound.length)
3640
3995
  console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
3996
+ const boundFps = new Set(Object.entries(all.bindings ?? {}).filter(([, b]) => b.space === m.id).map(([k]) => k));
3997
+ const missing = (m.repos ?? []).filter((r) => !boundFps.has(r.fp)).map((r) => r.name);
3998
+ if (missing.length)
3999
+ console.error(` ${D(`not on this machine: ${missing.join(", ")} — pantheon repo find, once you have ${missing.length === 1 ? "it" : "them"} cloned`)}`);
3641
4000
  }
3642
4001
  const r = repoInfo(process.cwd());
3643
4002
  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 +4038,17 @@ async function main() {
3679
4038
  if ((here === real(homedir()) || dirname(here) === here) && !argv.includes("--yes")) {
3680
4039
  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
4040
  }
4041
+ if ((await import("./repo-find.js")).holdsRepo(process.cwd())) {
4042
+ const n = await bindReposIn(process.cwd(), all, m, argv.includes("--yes"), `pantheon use ${m.name} --here --yes`);
4043
+ if (!n)
4044
+ console.error(D(` nothing bound — the folder itself is not bound, since it holds repos`));
4045
+ else
4046
+ console.error(D(` Agents opened in those repos now speak as you in "${m.name}". Restart any that are open.`));
4047
+ return;
4048
+ }
3682
4049
  const { replaced } = bindFolder(process.cwd(), m.id);
3683
- console.error(`${B("bound")} this folder (not a git repo) → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
3684
- console.error(D(` Agents opened here, or below here, now speak as you in "${m.name}". Delivering code needs a git repo. Restart any that are open.`));
4050
+ console.error(`${B("bound")} this folder → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
4051
+ console.error(D(` Agents opened here, or in any folder below, now speak as you in "${m.name}". Restart any that are open.`));
3685
4052
  return;
3686
4053
  }
3687
4054
  const { replaced } = bindRepo(r, m.id);
@@ -3710,6 +4077,15 @@ async function main() {
3710
4077
  const { repo: _r, repoOrigin: _o, ...plain } = cfg;
3711
4078
  const client = new PantheonClient(plain, undefined, { role: "cli" });
3712
4079
  await client.connect().catch((e) => die(client.pending ?? connectFailure(e, plain.relay)));
4080
+ if (sub === "find") {
4081
+ await new Promise((res) => setTimeout(res, 150));
4082
+ const all = loadAll() ?? die("not configured");
4083
+ const m = findMembership(all, cfg.spaceId ?? cfg.space) ?? findMembership(all, cfg.space) ?? die(`not in "${cfg.space}" on this machine`);
4084
+ const where = argv[2] && !argv[2].startsWith("--") ? resolve(argv[2]) : null;
4085
+ client.close();
4086
+ await findAndBindRepos(all, m, client.repos, { ...(where ? { extra: [where] } : {}), recents: argv.includes("--recents") ? true : undefined, yes: argv.includes("--yes") });
4087
+ return;
4088
+ }
3713
4089
  if (sub === "add") {
3714
4090
  const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
3715
4091
  const base = flag("base") ?? null;