@join-pantheon/cli 0.19.1 → 0.21.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/dist/cli.js CHANGED
@@ -9,14 +9,16 @@ process.emitWarning = ((warning, ...rest) => {
9
9
  import { randomBytes } from "node:crypto";
10
10
  import { dirname, join, resolve } from "node:path";
11
11
  import { PantheonClient } from "./client.js";
12
- import { fromInvite, load, save, toInvite, CONFIG_PATH, DEFAULT_RELAY } from "./config.js";
12
+ import { fromInvite, load, save, toInvite, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
13
+ import { fmtLimit } from "./plans.js";
13
14
  import { validEmail, validUsername } from "./accounts.js";
14
15
  import { install, launcher } from "./install.js";
15
16
  import { Relay } from "./relay.js";
16
17
  import { runMcp } from "./mcp.js";
17
18
  import { Worker } from "./worker.js";
18
19
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
19
- import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile } from "./profile.js";
20
+ import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
21
+ import { runGuide, terminalIO } from "./setup-guide.js";
20
22
  import { doctor, render } from "./doctor.js";
21
23
  import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
22
24
  import { assess } from "./untrusted.js";
@@ -219,7 +221,37 @@ function sameHost(a, b) {
219
221
  }
220
222
  }
221
223
  function need() {
222
- return load() ?? die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
224
+ const r = resolveFor(process.cwd());
225
+ if (r.cfg && r.cfg.space)
226
+ return r.cfg;
227
+ if (r.reason === "unbound") {
228
+ const names = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name).join(", ");
229
+ die(`this folder is not bound to one of your ${r.memberships} teams (${names}).\n From the team's repo: pantheon use <team> --here\n Or pick a default: pantheon use <team>`);
230
+ }
231
+ if (r.reason === "stale")
232
+ die(`this repo is bound to a team this machine is no longer in (${r.binding?.space}).\n Rebind it: pantheon use <team> --here · or rejoin: pantheon join <invite>`);
233
+ if (r.cfg && !r.cfg.space)
234
+ die("signed in, but not on a team yet. Run `pantheon start` (create one) or `pantheon join <invite>` (join one).");
235
+ return die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
236
+ }
237
+ function targets() {
238
+ const f = loadAll();
239
+ if (flag("space")) {
240
+ const m = f && findMembership(f, flag("space"));
241
+ if (!f || !m)
242
+ die(`you are not in a team called "${flag("space")}"`);
243
+ return [configFor(f, m, null)];
244
+ }
245
+ const r = resolveFor(process.cwd());
246
+ if (r.cfg && r.cfg.space && r.reason !== "default")
247
+ return [r.cfg];
248
+ if (r.reason === "default" && f)
249
+ return Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null));
250
+ return [need()];
251
+ }
252
+ function needStrict() {
253
+ const r = resolveFor(process.cwd(), { strict: true });
254
+ return r.cfg && r.cfg.space ? r.cfg : null;
223
255
  }
224
256
  function printGranted(granted) {
225
257
  if (!granted.length)
@@ -236,6 +268,39 @@ function unattendedProject() {
236
268
  return { project: resolve(flag("project", ".")), how: "explicit" };
237
269
  return { project: resolve("."), how: "default" };
238
270
  }
271
+ async function guidedSetup(cfg, project, opts = {}) {
272
+ if (argv.includes("--no-guide"))
273
+ return;
274
+ const interactive = !!process.stdin.isTTY && !argv.includes("--yes");
275
+ if (!interactive && !opts.force)
276
+ return;
277
+ const rl = interactive ? createInterface({ input: process.stdin, output: process.stderr }) : null;
278
+ const io = rl ? terminalIO(rl, (l) => console.error(l)) : null;
279
+ console.error(`\n${B("A few choices, then you're done.")}${io ? "" : D(" (no terminal — taking the defaults)")}`);
280
+ try {
281
+ const r = await runGuide(io, { cfg, project }, {
282
+ publish: async (p) => {
283
+ const c = new PantheonClient(cfg, undefined, { role: "cli" });
284
+ try {
285
+ await c.connect();
286
+ await c.publishProfile(organizationProfile(p, cfg.spaceId, cfg.space));
287
+ }
288
+ finally {
289
+ c.close();
290
+ }
291
+ },
292
+ });
293
+ console.error("");
294
+ for (const l of r.applied)
295
+ console.error(` ${C("✓")} ${l}`);
296
+ for (const w of r.warnings)
297
+ console.error(`\x1b[33m ! ${w}\x1b[0m`);
298
+ console.error(D(`\n Change any of this later: pantheon setup`));
299
+ }
300
+ finally {
301
+ rl?.close();
302
+ }
303
+ }
239
304
  function printReady(tools) {
240
305
  const workerOn = tools.some((t) => t.tool === "worker" && t.status.startsWith("on"));
241
306
  console.error(`\n${B("You're set.")} Restart your agent once (quit and reopen) and it is connected.`);
@@ -283,6 +348,15 @@ ${B("getting going — then two commands, run from your project's repo")}
283
348
  --no-worker, --no-hook, --no-notifications, --no-menubar.
284
349
  Already a member, new machine or fresh pull? pantheon install --user does the same.
285
350
 
351
+ ${B("several teams on one machine")}
352
+ pantheon spaces your teams, which repo is bound to which, and where this folder points
353
+ pantheon use <team> --here bind this repo to a team (agents opened here speak in it)
354
+ pantheon repo add | list the repos a team works from; a delivery must come from one of them
355
+ (repo add --base <branch>: the branch workers cut from, else origin's default)
356
+ pantheon leave <team> drop one team from this machine (its token and repo bindings)
357
+ pantheon space delete <team> --yes dissolve a team you founded: disconnects everyone, frees a plan slot
358
+ pantheon profile set --space <team> … authority that applies in one team only (e.g. --human-required pricing)
359
+
286
360
  ${B("day to day")}
287
361
  pantheon status who is here, what is being worked on, what needs you
288
362
  pantheon brief what needs you, and what your agent already handled
@@ -313,6 +387,8 @@ ${B("control")}
313
387
  pantheon leave remove what pantheon put on this machine
314
388
  pantheon panic cut off all unattended work on this machine, now
315
389
  pantheon doctor check this machine end to end
390
+ pantheon setup the guided choices: who may wake this machine, what a run may execute,
391
+ what you personally decide, which alerts you want — re-run any time
316
392
  pantheon upgrade pull the latest pantheon, rebuild, and re-wire your agents
317
393
 
318
394
  ${D("more: pantheon help --all")}
@@ -391,6 +467,8 @@ async function main() {
391
467
  console.error(D(` ${r.reach.hint}`));
392
468
  }
393
469
  printReady(r.tools);
470
+ if (r.claimed)
471
+ await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null);
394
472
  if (r.relay) {
395
473
  console.error(D(`\nKeeping the relay running here. Ctrl-C stops it (your teammate goes offline too).`));
396
474
  await new Promise(() => { });
@@ -403,7 +481,8 @@ async function main() {
403
481
  await client.connect().catch((e) => die(client.pending ?? e.message));
404
482
  await new Promise((r) => setTimeout(r, 250));
405
483
  const ttl = Number(flag("ttl", "1440"));
406
- const inv = await client.makeInvite(!argv.includes("--no-wake"), ttl).catch((e) => die(e.message));
484
+ const mayWake = argv.includes("--may-wake") ? true : argv.includes("--no-wake") ? false : wakeDefault(loadProfile(cfg));
485
+ const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
407
486
  console.log(`pantheon join ${toInvite({ ...cfg, token: inv.url })}`);
408
487
  console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
409
488
  client.close();
@@ -521,6 +600,11 @@ async function main() {
521
600
  console.error(`\x1b[33m ! ${w}\x1b[0m`);
522
601
  printGranted(r.granted);
523
602
  printReady(r.tools);
603
+ {
604
+ const joined = load();
605
+ if (joined)
606
+ await guidedSetup(joined, repoInfo(process.cwd())?.root ?? null);
607
+ }
524
608
  console.error(`\n${D('Then ask your agent: "check pantheon_status and say hello to the team".')}`);
525
609
  return;
526
610
  }
@@ -581,91 +665,106 @@ async function main() {
581
665
  }
582
666
  return;
583
667
  }
668
+ case "setup": {
669
+ const cfg = need();
670
+ await guidedSetup(cfg, repoInfo(process.cwd())?.root ?? null, { force: true });
671
+ return;
672
+ }
584
673
  case "version": {
585
674
  console.log(VERSION);
586
675
  return;
587
676
  }
588
677
  case "status": {
589
- const cfg = need();
590
- const client = new PantheonClient(cfg, undefined, { role: "cli" });
591
- await client.connect().catch((e) => {
592
- if (client.pending) {
593
- console.error(`${B("not admitted yet")} (${C(cfg.member)}, fingerprint ${C(fingerprint(cfg.publicKey))})\n`);
594
- console.error(client.pending);
595
- process.exit(2);
678
+ const showStatus = async (cfg) => {
679
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
680
+ await client.connect().catch((e) => {
681
+ if (client.pending) {
682
+ console.error(`${B("not admitted yet")} (${C(cfg.member)}, fingerprint ${C(fingerprint(cfg.publicKey))})\n`);
683
+ console.error(client.pending);
684
+ process.exit(2);
685
+ }
686
+ die(`cannot reach relay at ${cfg.relay} — ${e.message}`);
687
+ });
688
+ await new Promise((r) => setTimeout(r, 350));
689
+ const peers = client.roster.filter((m) => m.id !== cfg.member);
690
+ console.log(`${B(cfg.space)} you are ${C(cfg.member)} ${D(cfg.relay)} ${D(`${client.head} acts`)}\n`);
691
+ if (client.plan)
692
+ console.log(D(` ${client.plan.name} plan · ${client.plan.usage.spaces} of ${fmtLimit(client.plan.limits.spaces)} spaces created · ${client.plan.usage.members} of ${fmtLimit(client.plan.limits.members)} people here${client.plan.limits.wake ? "" : " · no unattended workers"}`) + "\n");
693
+ console.log(B("members"));
694
+ if (!peers.length)
695
+ console.log(` ${D("(nobody else yet — send them an invite from `pantheon invite`)")}`);
696
+ for (const p of peers) {
697
+ const behind = p.seen_seq === undefined ? null : client.head - p.seen_seq;
698
+ const reach = behind === null
699
+ ? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
700
+ ? "delivered, but no read-receipt — no wake-up hook on their machine"
701
+ : "no wake-up hook on their machine — nothing surfaces until their human types")
702
+ : behind > 0
703
+ ? `\x1b[33mtheir agent has not seen your last ${behind} act${behind === 1 ? "" : "s"}\x1b[0m`
704
+ : D("their agent is caught up");
705
+ console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
596
706
  }
597
- die(`cannot reach relay at ${cfg.relay} — ${e.message}`);
598
- });
599
- await new Promise((r) => setTimeout(r, 350));
600
- const peers = client.roster.filter((m) => m.id !== cfg.member);
601
- console.log(`${B(cfg.space)} you are ${C(cfg.member)} ${D(cfg.relay)} ${D(`${client.head} acts`)}\n`);
602
- console.log(B("members"));
603
- if (!peers.length)
604
- console.log(` ${D("(nobody else yet — send them an invite from `pantheon invite`)")}`);
605
- for (const p of peers) {
606
- const behind = p.seen_seq === undefined ? null : client.head - p.seen_seq;
607
- const reach = behind === null
608
- ? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
609
- ? "delivered, but no read-receipt — no wake-up hook on their machine"
610
- : "no wake-up hook on their machine — nothing surfaces until their human types")
611
- : behind > 0
612
- ? `\x1b[33mtheir agent has not seen your last ${behind} act${behind === 1 ? "" : "s"}\x1b[0m`
613
- : D("their agent is caught up");
614
- console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
615
- }
616
- const goals = [...client.goals.values()];
617
- console.log(`\n${B("goals")}`);
618
- if (!goals.length)
619
- console.log(` ${D("(none)")}`);
620
- for (const g of goals) {
621
- console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title} ${D(g.id)}`);
622
- }
623
- const owed = client.obligations;
624
- const ageOf = (t) => {
625
- const m = Math.round((Date.now() - t) / 60000);
626
- return m < 60 ? `${m}m` : `${Math.round(m / 60)}h`;
707
+ const goals = [...client.goals.values()];
708
+ console.log(`\n${B("goals")}`);
709
+ if (!goals.length)
710
+ console.log(` ${D("(none)")}`);
711
+ for (const g of goals) {
712
+ console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title} ${D(g.id)}`);
713
+ }
714
+ const owed = client.obligations;
715
+ const ageOf = (t) => {
716
+ const m = Math.round((Date.now() - t) / 60000);
717
+ return m < 60 ? `${m}m` : `${Math.round(m / 60)}h`;
718
+ };
719
+ const human = owed.filter((o) => o.kind === "HUMAN");
720
+ const between = owed.filter((o) => o.kind !== "HUMAN");
721
+ console.log(`\n${B("needs a human")}`);
722
+ if (!human.length)
723
+ console.log(` ${D("(nothing waiting on you)")}`);
724
+ for (const o of human) {
725
+ const who = o.owner.replace(/^human:/, "");
726
+ const stale = Date.now() - o.created_at > 3_600_000;
727
+ console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
728
+ if (who === cfg.member)
729
+ console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
730
+ }
731
+ console.log(`\n${B("between delegates")}`);
732
+ if (!between.length)
733
+ console.log(` ${D("(nothing owed)")}`);
734
+ for (const o of between) {
735
+ console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
736
+ }
737
+ client.close();
627
738
  };
628
- const human = owed.filter((o) => o.kind === "HUMAN");
629
- const between = owed.filter((o) => o.kind !== "HUMAN");
630
- console.log(`\n${B("needs a human")}`);
631
- if (!human.length)
632
- console.log(` ${D("(nothing waiting on you)")}`);
633
- for (const o of human) {
634
- const who = o.owner.replace(/^human:/, "");
635
- const stale = Date.now() - o.created_at > 3_600_000;
636
- console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
637
- if (who === cfg.member)
638
- console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
639
- }
640
- console.log(`\n${B("between delegates")}`);
641
- if (!between.length)
642
- console.log(` ${D("(nothing owed)")}`);
643
- for (const o of between) {
644
- console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
739
+ const ts = targets();
740
+ for (const [i, c] of ts.entries()) {
741
+ if (ts.length > 1)
742
+ console.log(`${i ? "\n" : ""}${D("━━━ team")} ${B(c.space)} ${D(`(${i + 1} of ${ts.length}) ━━━`)}`);
743
+ await showStatus(c);
645
744
  }
646
- client.close();
647
745
  return;
648
746
  }
649
747
  case "brief": {
650
- const cfg = need();
651
- const profile = loadProfile(cfg);
652
- const client = new PantheonClient(cfg, undefined, { role: "cli" });
653
- await client.connect().catch((e) => die(client.pending ?? e.message));
654
- await new Promise((r) => setTimeout(r, 350));
655
- process.stdout.write(renderBrief({
656
- space: cfg.space,
657
- person: {
658
- member: cfg.member,
659
- display_name: profile.human.display_name,
660
- title: profile.human.title,
661
- responsibilities: profile.human.responsibilities,
662
- },
663
- owed: client.myObligations().map((o) => ({
664
- id: o.id, kind: o.kind, body: o.body, created_at: o.created_at,
665
- })),
666
- recentRuns: recentRuns(86_400_000),
667
- }));
668
- client.close();
748
+ for (const cfg of targets()) {
749
+ const profile = loadProfile(cfg);
750
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
751
+ await client.connect().catch((e) => die(client.pending ?? e.message));
752
+ await new Promise((r) => setTimeout(r, 350));
753
+ process.stdout.write(renderBrief({
754
+ space: cfg.space,
755
+ person: {
756
+ member: cfg.member,
757
+ display_name: profile.human.display_name,
758
+ title: profile.human.title,
759
+ responsibilities: profile.human.responsibilities,
760
+ },
761
+ owed: client.myObligations().map((o) => ({
762
+ id: o.id, kind: o.kind, body: o.body, created_at: o.created_at,
763
+ })),
764
+ recentRuns: recentRuns(86_400_000),
765
+ }));
766
+ client.close();
767
+ }
669
768
  return;
670
769
  }
671
770
  case "team": {
@@ -899,11 +998,14 @@ async function main() {
899
998
  }
900
999
  case "hook": {
901
1000
  const sub = argv[1] ?? "status";
1001
+ const hookCfg = resolveForHook();
902
1002
  const reportIdle = async (session, seen) => {
1003
+ if (!hookCfg)
1004
+ return true;
903
1005
  const { reportActivity } = await import("./pending.js");
904
1006
  for (let attempt = 0; attempt < 2; attempt++) {
905
1007
  const ok = await reportActivity({
906
- session: `hook:${session}`, state: "idle", ...(seen !== undefined ? { seen } : {}),
1008
+ cfg: hookCfg, session: `hook:${session}`, state: "idle", ...(seen !== undefined ? { seen } : {}),
907
1009
  }).catch(() => false);
908
1010
  if (ok)
909
1011
  return true;
@@ -923,8 +1025,10 @@ async function main() {
923
1025
  }
924
1026
  catch { }
925
1027
  try {
926
- const { reportActivity } = await import("./pending.js");
927
- await reportActivity({ session: `hook:${session}`, state: "busy" });
1028
+ if (hookCfg) {
1029
+ const { reportActivity } = await import("./pending.js");
1030
+ await reportActivity({ cfg: hookCfg, session: `hook:${session}`, state: "busy" });
1031
+ }
928
1032
  }
929
1033
  catch { }
930
1034
  process.exit(0);
@@ -997,7 +1101,7 @@ async function main() {
997
1101
  const { pending: pendingFn } = await import("./pending.js");
998
1102
  const { load: loadCfg } = await import("./config.js");
999
1103
  const d = await decideStop(JSON.stringify({ session_id: session }), {
1000
- pending: async (s) => pendingFn({ session: s }),
1104
+ pending: async (s) => (hookCfg ? pendingFn({ session: s, cfg: hookCfg }) : Promise.resolve(null)),
1001
1105
  ...(loadCfg()?.member ? { me: loadCfg().member } : {}),
1002
1106
  });
1003
1107
  if (d.exit === 2 && d.message) {
@@ -1030,7 +1134,7 @@ async function main() {
1030
1134
  const { pending: pendingFn } = await import("./pending.js");
1031
1135
  const { load: loadCfg } = await import("./config.js");
1032
1136
  const d = await decideStop(raw, {
1033
- pending: async (session) => pendingFn({ session }),
1137
+ pending: async (session) => (hookCfg ? pendingFn({ session, cfg: hookCfg }) : Promise.resolve(null)),
1034
1138
  ...(loadCfg()?.member ? { me: loadCfg().member } : {}),
1035
1139
  });
1036
1140
  if (d.exit === 2 && d.message) {
@@ -1068,7 +1172,9 @@ async function main() {
1068
1172
  let surfaced;
1069
1173
  const d = await decideStop(raw, {
1070
1174
  pending: async (session) => {
1071
- const r = await pendingFn({ session });
1175
+ if (!hookCfg)
1176
+ return null;
1177
+ const r = await pendingFn({ session, cfg: hookCfg });
1072
1178
  if (r)
1073
1179
  surfaced = r.seq;
1074
1180
  return r;
@@ -1138,7 +1244,34 @@ async function main() {
1138
1244
  }
1139
1245
  return;
1140
1246
  }
1247
+ case "space": {
1248
+ const sub = argv[1];
1249
+ const name = argv[2];
1250
+ if (sub !== "delete" || !name || name.startsWith("--"))
1251
+ die("usage: pantheon space delete <team> --yes");
1252
+ const all = loadAll() ?? die("not configured");
1253
+ const m = findMembership(all, name) ?? die(`you are not in a team called "${name}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
1254
+ if (!argv.includes("--yes"))
1255
+ die(`this deletes "${m.name}" for everyone in it — its history, goals and invites — and cannot be undone.\n If you mean it: pantheon space delete ${m.name} --yes`);
1256
+ const client = new PantheonClient(configFor(all, m, null), undefined, { role: "cli" });
1257
+ await client.connect().catch((e) => die(client.pending ?? e.message));
1258
+ await client.deleteSpace(m.name).catch((e) => { client.close(); die(e.message); });
1259
+ client.close();
1260
+ const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).length;
1261
+ removeMembership(m.id);
1262
+ console.error(`${B("deleted")} "${m.name}" — everyone in it was disconnected; its token${bound ? ` and ${bound} repo binding${bound === 1 ? "" : "s"}` : ""} left this machine`);
1263
+ return;
1264
+ }
1141
1265
  case "leave": {
1266
+ if (argv[1] && !argv[1].startsWith("--")) {
1267
+ const all = loadAll() ?? die("not configured");
1268
+ const m = findMembership(all, argv[1]) ?? die(`you are not in a team called "${argv[1]}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
1269
+ const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).map((b) => b.origin ?? b.path);
1270
+ removeMembership(m.id);
1271
+ console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length} repo binding${bound.length === 1 ? "" : "s"} with it (${bound.join(", ")})` : ""}.`);
1272
+ console.error(D(`Your teammates still list you in "${m.name}". Ask one of them to run: pantheon admit ${m.name === all.member ? "you" : all.member} --revoke if you are not coming back.`));
1273
+ return;
1274
+ }
1142
1275
  const cfg = load();
1143
1276
  const project = resolve(flag("project", "."));
1144
1277
  console.error(`${B("leaving pantheon")}\n`);
@@ -1503,7 +1636,7 @@ async function main() {
1503
1636
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1504
1637
  try {
1505
1638
  await client.connect();
1506
- await client.publishProfile(organizationProfile(profile));
1639
+ await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space));
1507
1640
  }
1508
1641
  finally {
1509
1642
  client.close();
@@ -1544,6 +1677,39 @@ async function main() {
1544
1677
  if (!surface && !publicChange) {
1545
1678
  die("usage: pantheon profile set [--surface codex|claude-code|cursor] [--name name] [--title title] [--responsibilities a,b] [--human-required a,b] [--agent-can-decide a,b] [--keywords 'domain=word|word;domain=word']");
1546
1679
  }
1680
+ if (flag("space")) {
1681
+ const pf = loadAll() ?? die("not configured");
1682
+ const m = findMembership(pf, flag("space")) ?? die(`you are not in a team called "${flag("space")}"`);
1683
+ const key = m.id.startsWith("legacy:") ? m.name : m.id;
1684
+ const prev = profile.spaces?.[key] ?? profile.spaces?.[m.name] ?? {};
1685
+ const auth = { ...(prev.authority ?? {}) };
1686
+ for (const domain of humanRequired ?? [])
1687
+ auth[domain] = "human_required";
1688
+ for (const domain of agentCanDecide ?? [])
1689
+ auth[domain] = "agent_can_decide";
1690
+ profile = { ...profile, spaces: { ...(profile.spaces ?? {}), [key]: {
1691
+ ...prev,
1692
+ ...(responsibilities !== undefined ? { responsibilities } : {}),
1693
+ ...(humanRequired !== undefined || agentCanDecide !== undefined ? { authority: auth } : {}),
1694
+ ...(keywords !== undefined ? { decision_keywords: keywordFlag(keywords) } : {}),
1695
+ } }, updated_at: Date.now() };
1696
+ saveProfile(profile);
1697
+ console.error(`${C("saved")} — authority for team "${m.name}" only`);
1698
+ const tc = configFor(pf, m, null);
1699
+ const tcl = new PantheonClient(tc, undefined, { role: "cli" });
1700
+ try {
1701
+ await tcl.connect();
1702
+ await tcl.publishProfile(organizationProfile(profile, m.id, m.name));
1703
+ console.error(`${C("published")} into "${m.name}"`);
1704
+ }
1705
+ catch (e) {
1706
+ console.error(`\x1b[33mnot published\x1b[0m — ${e.message}`);
1707
+ }
1708
+ finally {
1709
+ tcl.close();
1710
+ }
1711
+ return;
1712
+ }
1547
1713
  const authority = { ...profile.authority };
1548
1714
  for (const domain of humanRequired ?? [])
1549
1715
  authority[domain] = "human_required";
@@ -1604,7 +1770,7 @@ async function main() {
1604
1770
  }
1605
1771
  profile = { ...profile, delegations: [...profile.delegations, { to, domains, starts_at: startsAt, expires_at: expiresAt }], updated_at: Date.now() };
1606
1772
  saveProfile(profile);
1607
- await client.publishProfile(organizationProfile(profile)).catch((e) => die(e.message));
1773
+ await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space)).catch((e) => die(e.message));
1608
1774
  client.close();
1609
1775
  console.error(`${C("delegated")} — ${domains.join(", ")} routes to ${to} until ${new Date(expiresAt).toISOString()}`);
1610
1776
  }
@@ -1735,6 +1901,50 @@ async function main() {
1735
1901
  return;
1736
1902
  }
1737
1903
  case "approvals": {
1904
+ const sub = argv[1] ?? "list";
1905
+ const id = argv[2];
1906
+ const conns = [];
1907
+ for (const c of targets()) {
1908
+ const cl = new PantheonClient(c, undefined, { role: "cli" });
1909
+ await cl.connect().catch((e) => die(`${c.space}: ${e.message}`));
1910
+ conns.push({ cfg: c, client: cl });
1911
+ }
1912
+ const closeAll = () => { for (const c of conns)
1913
+ c.client.close(); };
1914
+ const all = conns.flatMap(({ cfg, client }) => client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`).map((o) => ({ o, cfg, client })));
1915
+ const find = (want) => all.find((x) => x.o.id === want);
1916
+ if (sub === "answer") {
1917
+ if (!id)
1918
+ die("usage: pantheon approvals answer <obligation-id> <decision>");
1919
+ const hit = find(id) ?? die(`no decision ${id} is waiting on you${conns.length > 1 ? ` in any of your ${conns.length} teams` : ""}`);
1920
+ const answer = argv.slice(3).join(" ").trim() || die("state the decision; it will be written to the audit ledger");
1921
+ const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq });
1922
+ console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
1923
+ closeAll();
1924
+ return;
1925
+ }
1926
+ if (sub === "show") {
1927
+ if (!id)
1928
+ die("usage: pantheon approvals show <obligation-id>");
1929
+ const hit = find(id) ?? die(`no decision ${id} is waiting on you`);
1930
+ console.log(`${B("Decision required")} ${hit.o.id}${conns.length > 1 ? ` ${D("in " + hit.cfg.space)}` : ""}`);
1931
+ console.log(` ${hit.o.body}`);
1932
+ if (hit.o.goal)
1933
+ console.log(` goal ${hit.client.goals.get(hit.o.goal)?.title ?? hit.o.goal}`);
1934
+ console.log(`\nAnswer: ${C(`pantheon approvals answer ${hit.o.id} 'your decision and rationale'`)}`);
1935
+ closeAll();
1936
+ return;
1937
+ }
1938
+ if (sub !== "list")
1939
+ die("usage: pantheon approvals [list | show <id> | answer <id> <decision>]");
1940
+ if (!all.length)
1941
+ console.log(D(conns.length > 1 ? `no decisions waiting on you in any of your ${conns.length} teams` : "no decisions waiting on you"));
1942
+ for (const { o, cfg, client } of all)
1943
+ console.log(`${C(o.id)} ${conns.length > 1 ? D(`[${cfg.space}] `) : ""}${client.goals.get(o.goal ?? "")?.title ?? "No goal"}\n ${o.body}`);
1944
+ closeAll();
1945
+ return;
1946
+ }
1947
+ case "approvals-legacy-unreachable": {
1738
1948
  const cfg = need();
1739
1949
  const sub = argv[1] ?? "list";
1740
1950
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
@@ -1773,6 +1983,41 @@ async function main() {
1773
1983
  return;
1774
1984
  }
1775
1985
  case "worker": {
1986
+ const allCfg = loadAll();
1987
+ const bindings = Object.values(allCfg?.bindings ?? {});
1988
+ if (!flag("project") && bindings.length) {
1989
+ const { WorkerFleet } = await import("./worker-fleet.js");
1990
+ const fleet = new WorkerFleet({
1991
+ ...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
1992
+ ...(flag("persona") ? { persona: flag("persona") } : {}),
1993
+ continuations: !argv.includes("--no-continue"),
1994
+ allowBash: argv.includes("--allow-bash"),
1995
+ idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000,
1996
+ maxRunsPerHour: numFlag("max-runs", 12),
1997
+ maxRunsPerDay: numFlag("max-runs-day", 40),
1998
+ graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
1999
+ once: argv.includes("--once"),
2000
+ ...(argv.includes("--no-notify") ? {} : {
2001
+ notify: (n) => {
2002
+ try {
2003
+ const c = load();
2004
+ if (c)
2005
+ sendDesktopNotification(n.title, n.subtitle, n.body, loadProfile(c), "", n.eyebrow);
2006
+ }
2007
+ catch { }
2008
+ },
2009
+ }),
2010
+ log: (l) => console.error(l),
2011
+ });
2012
+ console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound repo${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
2013
+ if (argv.includes("--allow-bash")) {
2014
+ console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
2015
+ console.error(`\x1b[33m peer you granted --may-wake, since their request is what starts the run.\x1b[0m`);
2016
+ }
2017
+ process.on("SIGINT", () => { fleet.stop(); process.exit(0); });
2018
+ await fleet.run();
2019
+ return;
2020
+ }
1776
2021
  const cfg = need();
1777
2022
  const project = resolve(flag("project", "."));
1778
2023
  const worker = new Worker(cfg, {
@@ -1833,15 +2078,20 @@ async function main() {
1833
2078
  return;
1834
2079
  }
1835
2080
  case "notifier": {
1836
- const cfg = need();
1837
- const notifier = new HumanNotifier(cfg, {
2081
+ const nf = loadAll();
2082
+ const teams = nf ? Object.values(nf.spaces ?? {}).map((m) => configFor(nf, m, null)) : [];
2083
+ const cfgs = teams.length ? teams : [need()];
2084
+ const notifiers = cfgs.map((c) => new HumanNotifier(c, {
1838
2085
  reminderMs: Math.max(1, numFlag("remind", DEFAULT_REMINDER_MS / 60_000)) * 60_000,
1839
- log: (line) => console.error(line),
1840
- });
1841
- console.error(`${B("pantheon notifier")} ${D("(ctrl-c to stop)")}`);
1842
- process.on("SIGINT", () => { notifier.stop(); process.exit(0); });
1843
- process.on("SIGTERM", () => { notifier.stop(); process.exit(0); });
1844
- await notifier.run();
2086
+ ...(cfgs.length > 1 ? { label: c.space, stateFile: join(CONFIG_DIR, `notifier-state-${encodeURIComponent(c.spaceId ?? c.space)}.json`) } : {}),
2087
+ log: (line) => console.error(cfgs.length > 1 ? `[${c.space}] ${line}` : line),
2088
+ }));
2089
+ console.error(`${B("pantheon notifier")} ${D(`(${cfgs.length === 1 ? "" : cfgs.length + " teams · "}ctrl-c to stop)`)}`);
2090
+ const stopAll = () => { for (const n of notifiers)
2091
+ n.stop(); process.exit(0); };
2092
+ process.on("SIGINT", stopAll);
2093
+ process.on("SIGTERM", stopAll);
2094
+ await Promise.all(notifiers.map((n) => n.run()));
1845
2095
  return;
1846
2096
  }
1847
2097
  case "doctor": {
@@ -1927,6 +2177,79 @@ async function main() {
1927
2177
  console.error(`\n${D("It only acts on requests from peers you granted --may-wake, one run at a time.")}`);
1928
2178
  return;
1929
2179
  }
2180
+ case "spaces": {
2181
+ const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
2182
+ const spaces = Object.values(all.spaces ?? {});
2183
+ if (!spaces.length) {
2184
+ console.error(D("no teams yet — pantheon start (create one) or pantheon join <invite>"));
2185
+ return;
2186
+ }
2187
+ const here = resolveFor(process.cwd());
2188
+ console.error(`${B("your teams")} ${D(`(${spaces.length})`)}\n`);
2189
+ for (const m of spaces) {
2190
+ const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id);
2191
+ const tags = [m.id === all.default_space ? C("default") : "", here.cfg?.spaceId === m.id || (here.cfg?.space === m.name && here.reason !== "none") ? C("← here") : ""].filter(Boolean).join(" ");
2192
+ console.error(` ${B(m.name.padEnd(18))} ${D(new URL(m.relay).host)} ${tags}`);
2193
+ for (const b of bound)
2194
+ console.error(` ${D("repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
2195
+ if (!bound.length)
2196
+ console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
2197
+ }
2198
+ const r = repoInfo(process.cwd());
2199
+ 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"}`);
2200
+ return;
2201
+ }
2202
+ case "use": {
2203
+ const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
2204
+ const ref = argv[1] ?? die("usage: pantheon use <team> [--here] # --here binds this repo to the team; without it, sets your default");
2205
+ const m = findMembership(all, ref) ?? die(`you are not in a team called "${ref}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
2206
+ if (argv.includes("--here")) {
2207
+ const r = repoInfo(process.cwd()) ?? die("this folder is not inside a git repo. Run it from the team's checkout.");
2208
+ const { replaced } = bindRepo(r, m.id);
2209
+ console.error(`${B("bound")} ${r.origin ?? r.root} → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
2210
+ if (!r.shared)
2211
+ console.error(D(" this repo has no origin remote — deliveries from it cannot be fetched by teammates"));
2212
+ const c = resolveFor(process.cwd()).cfg;
2213
+ if (c && c.space === m.name) {
2214
+ const { repo: _r, repoOrigin: _o, ...plain } = c;
2215
+ const client = new PantheonClient(plain, undefined, { role: "cli" });
2216
+ await client.connect().then(() => client.bindRepo(r.fingerprint, r.origin)).catch((e) => console.error(D(` (could not register the repo with the relay: ${e.message} — run \`pantheon repo add\` when it is reachable)`)));
2217
+ client.close();
2218
+ }
2219
+ console.error(D(` Agents opened in this repo now speak as you in "${m.name}". Restart any that are open.`));
2220
+ }
2221
+ else {
2222
+ setDefaultSpace(m.id);
2223
+ console.error(`${B("default")} team is now "${m.name}" — used by your own commands when you run them outside a bound repo.`);
2224
+ console.error(D(" Agents never use the default; bind each repo with: pantheon use <team> --here"));
2225
+ }
2226
+ return;
2227
+ }
2228
+ case "repo": {
2229
+ const cfg = need();
2230
+ const sub = argv[1] ?? "list";
2231
+ const { repo: _r, repoOrigin: _o, ...plain } = cfg;
2232
+ const client = new PantheonClient(plain, undefined, { role: "cli" });
2233
+ await client.connect().catch((e) => die(client.pending ?? e.message));
2234
+ if (sub === "add") {
2235
+ const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
2236
+ const base = flag("base") ?? null;
2237
+ await client.bindRepo(r.fingerprint, r.origin, base).catch((e) => die(e.message));
2238
+ if (cfg.spaceId)
2239
+ bindRepo(r, cfg.spaceId);
2240
+ console.error(`${B("registered")} ${r.origin ?? r.root} as a repo "${cfg.space}" works from${base ? ` — workers cut from ${C(base)}` : ""}${r.shared ? "" : D(" (no origin remote — deliveries from it cannot be fetched)")}`);
2241
+ }
2242
+ else {
2243
+ await new Promise((res) => setTimeout(res, 150));
2244
+ console.error(`${B(`repos "${cfg.space}" works from`)} ${D(`(${client.repos.length})`)}`);
2245
+ for (const r of client.repos)
2246
+ console.error(` ${(r.origin ?? r.fingerprint).padEnd(48)} ${D(`added by ${r.added_by}`)}${r.base ? ` ${D("base")} ${C(r.base)}` : ""}`);
2247
+ if (!client.repos.length)
2248
+ console.error(D(" none yet — the first member to connect from a checkout registers it"));
2249
+ }
2250
+ client.close();
2251
+ return;
2252
+ }
1930
2253
  case "mcp":
1931
2254
  await runMcp();
1932
2255
  return;