@join-pantheon/cli 0.19.1 → 0.20.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,7 +9,7 @@ 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 } from "./config.js";
13
13
  import { validEmail, validUsername } from "./accounts.js";
14
14
  import { install, launcher } from "./install.js";
15
15
  import { Relay } from "./relay.js";
@@ -219,7 +219,37 @@ function sameHost(a, b) {
219
219
  }
220
220
  }
221
221
  function need() {
222
- return load() ?? die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
222
+ const r = resolveFor(process.cwd());
223
+ if (r.cfg && r.cfg.space)
224
+ return r.cfg;
225
+ if (r.reason === "unbound") {
226
+ const names = Object.values(loadAll()?.spaces ?? {}).map((m) => m.name).join(", ");
227
+ 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>`);
228
+ }
229
+ if (r.reason === "stale")
230
+ 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>`);
231
+ if (r.cfg && !r.cfg.space)
232
+ die("signed in, but not on a team yet. Run `pantheon start` (create one) or `pantheon join <invite>` (join one).");
233
+ return die("not configured. Run `pantheon signup` then `pantheon start` (create a team), or `pantheon join <invite>` (join one).");
234
+ }
235
+ function targets() {
236
+ const f = loadAll();
237
+ if (flag("space")) {
238
+ const m = f && findMembership(f, flag("space"));
239
+ if (!f || !m)
240
+ die(`you are not in a team called "${flag("space")}"`);
241
+ return [configFor(f, m, null)];
242
+ }
243
+ const r = resolveFor(process.cwd());
244
+ if (r.cfg && r.cfg.space && r.reason !== "default")
245
+ return [r.cfg];
246
+ if (r.reason === "default" && f)
247
+ return Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null));
248
+ return [need()];
249
+ }
250
+ function needStrict() {
251
+ const r = resolveFor(process.cwd(), { strict: true });
252
+ return r.cfg && r.cfg.space ? r.cfg : null;
223
253
  }
224
254
  function printGranted(granted) {
225
255
  if (!granted.length)
@@ -283,6 +313,13 @@ ${B("getting going — then two commands, run from your project's repo")}
283
313
  --no-worker, --no-hook, --no-notifications, --no-menubar.
284
314
  Already a member, new machine or fresh pull? pantheon install --user does the same.
285
315
 
316
+ ${B("several teams on one machine")}
317
+ pantheon spaces your teams, which repo is bound to which, and where this folder points
318
+ pantheon use <team> --here bind this repo to a team (agents opened here speak in it)
319
+ pantheon repo add | list the repos a team works from; a delivery must come from one of them
320
+ pantheon leave <team> drop one team from this machine (its token and repo bindings)
321
+ pantheon profile set --space <team> … authority that applies in one team only (e.g. --human-required pricing)
322
+
286
323
  ${B("day to day")}
287
324
  pantheon status who is here, what is being worked on, what needs you
288
325
  pantheon brief what needs you, and what your agent already handled
@@ -586,86 +623,94 @@ async function main() {
586
623
  return;
587
624
  }
588
625
  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);
626
+ const showStatus = async (cfg) => {
627
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
628
+ await client.connect().catch((e) => {
629
+ if (client.pending) {
630
+ console.error(`${B("not admitted yet")} (${C(cfg.member)}, fingerprint ${C(fingerprint(cfg.publicKey))})\n`);
631
+ console.error(client.pending);
632
+ process.exit(2);
633
+ }
634
+ die(`cannot reach relay at ${cfg.relay} — ${e.message}`);
635
+ });
636
+ await new Promise((r) => setTimeout(r, 350));
637
+ const peers = client.roster.filter((m) => m.id !== cfg.member);
638
+ console.log(`${B(cfg.space)} you are ${C(cfg.member)} ${D(cfg.relay)} ${D(`${client.head} acts`)}\n`);
639
+ console.log(B("members"));
640
+ if (!peers.length)
641
+ console.log(` ${D("(nobody else yet — send them an invite from `pantheon invite`)")}`);
642
+ for (const p of peers) {
643
+ const behind = p.seen_seq === undefined ? null : client.head - p.seen_seq;
644
+ const reach = behind === null
645
+ ? D(p.delivered_seq !== undefined && p.delivered_seq >= client.head
646
+ ? "delivered, but no read-receipt — no wake-up hook on their machine"
647
+ : "no wake-up hook on their machine — nothing surfaces until their human types")
648
+ : behind > 0
649
+ ? `\x1b[33mtheir agent has not seen your last ${behind} act${behind === 1 ? "" : "s"}\x1b[0m`
650
+ : D("their agent is caught up");
651
+ console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
596
652
  }
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`;
653
+ const goals = [...client.goals.values()];
654
+ console.log(`\n${B("goals")}`);
655
+ if (!goals.length)
656
+ console.log(` ${D("(none)")}`);
657
+ for (const g of goals) {
658
+ console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title} ${D(g.id)}`);
659
+ }
660
+ const owed = client.obligations;
661
+ const ageOf = (t) => {
662
+ const m = Math.round((Date.now() - t) / 60000);
663
+ return m < 60 ? `${m}m` : `${Math.round(m / 60)}h`;
664
+ };
665
+ const human = owed.filter((o) => o.kind === "HUMAN");
666
+ const between = owed.filter((o) => o.kind !== "HUMAN");
667
+ console.log(`\n${B("needs a human")}`);
668
+ if (!human.length)
669
+ console.log(` ${D("(nothing waiting on you)")}`);
670
+ for (const o of human) {
671
+ const who = o.owner.replace(/^human:/, "");
672
+ const stale = Date.now() - o.created_at > 3_600_000;
673
+ console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
674
+ if (who === cfg.member)
675
+ console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
676
+ }
677
+ console.log(`\n${B("between delegates")}`);
678
+ if (!between.length)
679
+ console.log(` ${D("(nothing owed)")}`);
680
+ for (const o of between) {
681
+ console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
682
+ }
683
+ client.close();
627
684
  };
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}`);
685
+ const ts = targets();
686
+ for (const [i, c] of ts.entries()) {
687
+ if (ts.length > 1)
688
+ console.log(`${i ? "\n" : ""}${D("━━━ team")} ${B(c.space)} ${D(`(${i + 1} of ${ts.length}) ━━━`)}`);
689
+ await showStatus(c);
645
690
  }
646
- client.close();
647
691
  return;
648
692
  }
649
693
  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();
694
+ for (const cfg of targets()) {
695
+ const profile = loadProfile(cfg);
696
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
697
+ await client.connect().catch((e) => die(client.pending ?? e.message));
698
+ await new Promise((r) => setTimeout(r, 350));
699
+ process.stdout.write(renderBrief({
700
+ space: cfg.space,
701
+ person: {
702
+ member: cfg.member,
703
+ display_name: profile.human.display_name,
704
+ title: profile.human.title,
705
+ responsibilities: profile.human.responsibilities,
706
+ },
707
+ owed: client.myObligations().map((o) => ({
708
+ id: o.id, kind: o.kind, body: o.body, created_at: o.created_at,
709
+ })),
710
+ recentRuns: recentRuns(86_400_000),
711
+ }));
712
+ client.close();
713
+ }
669
714
  return;
670
715
  }
671
716
  case "team": {
@@ -899,11 +944,14 @@ async function main() {
899
944
  }
900
945
  case "hook": {
901
946
  const sub = argv[1] ?? "status";
947
+ const hookCfg = needStrict();
902
948
  const reportIdle = async (session, seen) => {
949
+ if (!hookCfg)
950
+ return true;
903
951
  const { reportActivity } = await import("./pending.js");
904
952
  for (let attempt = 0; attempt < 2; attempt++) {
905
953
  const ok = await reportActivity({
906
- session: `hook:${session}`, state: "idle", ...(seen !== undefined ? { seen } : {}),
954
+ cfg: hookCfg, session: `hook:${session}`, state: "idle", ...(seen !== undefined ? { seen } : {}),
907
955
  }).catch(() => false);
908
956
  if (ok)
909
957
  return true;
@@ -923,8 +971,10 @@ async function main() {
923
971
  }
924
972
  catch { }
925
973
  try {
926
- const { reportActivity } = await import("./pending.js");
927
- await reportActivity({ session: `hook:${session}`, state: "busy" });
974
+ if (hookCfg) {
975
+ const { reportActivity } = await import("./pending.js");
976
+ await reportActivity({ cfg: hookCfg, session: `hook:${session}`, state: "busy" });
977
+ }
928
978
  }
929
979
  catch { }
930
980
  process.exit(0);
@@ -1139,6 +1189,15 @@ async function main() {
1139
1189
  return;
1140
1190
  }
1141
1191
  case "leave": {
1192
+ if (argv[1] && !argv[1].startsWith("--")) {
1193
+ const all = loadAll() ?? die("not configured");
1194
+ 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"}`);
1195
+ const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).map((b) => b.origin ?? b.path);
1196
+ removeMembership(m.id);
1197
+ 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(", ")})` : ""}.`);
1198
+ 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.`));
1199
+ return;
1200
+ }
1142
1201
  const cfg = load();
1143
1202
  const project = resolve(flag("project", "."));
1144
1203
  console.error(`${B("leaving pantheon")}\n`);
@@ -1503,7 +1562,7 @@ async function main() {
1503
1562
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
1504
1563
  try {
1505
1564
  await client.connect();
1506
- await client.publishProfile(organizationProfile(profile));
1565
+ await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space));
1507
1566
  }
1508
1567
  finally {
1509
1568
  client.close();
@@ -1544,6 +1603,39 @@ async function main() {
1544
1603
  if (!surface && !publicChange) {
1545
1604
  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
1605
  }
1606
+ if (flag("space")) {
1607
+ const pf = loadAll() ?? die("not configured");
1608
+ const m = findMembership(pf, flag("space")) ?? die(`you are not in a team called "${flag("space")}"`);
1609
+ const key = m.id.startsWith("legacy:") ? m.name : m.id;
1610
+ const prev = profile.spaces?.[key] ?? profile.spaces?.[m.name] ?? {};
1611
+ const auth = { ...(prev.authority ?? {}) };
1612
+ for (const domain of humanRequired ?? [])
1613
+ auth[domain] = "human_required";
1614
+ for (const domain of agentCanDecide ?? [])
1615
+ auth[domain] = "agent_can_decide";
1616
+ profile = { ...profile, spaces: { ...(profile.spaces ?? {}), [key]: {
1617
+ ...prev,
1618
+ ...(responsibilities !== undefined ? { responsibilities } : {}),
1619
+ ...(humanRequired !== undefined || agentCanDecide !== undefined ? { authority: auth } : {}),
1620
+ ...(keywords !== undefined ? { decision_keywords: keywordFlag(keywords) } : {}),
1621
+ } }, updated_at: Date.now() };
1622
+ saveProfile(profile);
1623
+ console.error(`${C("saved")} — authority for team "${m.name}" only`);
1624
+ const tc = configFor(pf, m, null);
1625
+ const tcl = new PantheonClient(tc, undefined, { role: "cli" });
1626
+ try {
1627
+ await tcl.connect();
1628
+ await tcl.publishProfile(organizationProfile(profile, m.id, m.name));
1629
+ console.error(`${C("published")} into "${m.name}"`);
1630
+ }
1631
+ catch (e) {
1632
+ console.error(`\x1b[33mnot published\x1b[0m — ${e.message}`);
1633
+ }
1634
+ finally {
1635
+ tcl.close();
1636
+ }
1637
+ return;
1638
+ }
1547
1639
  const authority = { ...profile.authority };
1548
1640
  for (const domain of humanRequired ?? [])
1549
1641
  authority[domain] = "human_required";
@@ -1604,7 +1696,7 @@ async function main() {
1604
1696
  }
1605
1697
  profile = { ...profile, delegations: [...profile.delegations, { to, domains, starts_at: startsAt, expires_at: expiresAt }], updated_at: Date.now() };
1606
1698
  saveProfile(profile);
1607
- await client.publishProfile(organizationProfile(profile)).catch((e) => die(e.message));
1699
+ await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space)).catch((e) => die(e.message));
1608
1700
  client.close();
1609
1701
  console.error(`${C("delegated")} — ${domains.join(", ")} routes to ${to} until ${new Date(expiresAt).toISOString()}`);
1610
1702
  }
@@ -1735,6 +1827,50 @@ async function main() {
1735
1827
  return;
1736
1828
  }
1737
1829
  case "approvals": {
1830
+ const sub = argv[1] ?? "list";
1831
+ const id = argv[2];
1832
+ const conns = [];
1833
+ for (const c of targets()) {
1834
+ const cl = new PantheonClient(c, undefined, { role: "cli" });
1835
+ await cl.connect().catch((e) => die(`${c.space}: ${e.message}`));
1836
+ conns.push({ cfg: c, client: cl });
1837
+ }
1838
+ const closeAll = () => { for (const c of conns)
1839
+ c.client.close(); };
1840
+ const all = conns.flatMap(({ cfg, client }) => client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`).map((o) => ({ o, cfg, client })));
1841
+ const find = (want) => all.find((x) => x.o.id === want);
1842
+ if (sub === "answer") {
1843
+ if (!id)
1844
+ die("usage: pantheon approvals answer <obligation-id> <decision>");
1845
+ const hit = find(id) ?? die(`no decision ${id} is waiting on you${conns.length > 1 ? ` in any of your ${conns.length} teams` : ""}`);
1846
+ const answer = argv.slice(3).join(" ").trim() || die("state the decision; it will be written to the audit ledger");
1847
+ const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq });
1848
+ console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
1849
+ closeAll();
1850
+ return;
1851
+ }
1852
+ if (sub === "show") {
1853
+ if (!id)
1854
+ die("usage: pantheon approvals show <obligation-id>");
1855
+ const hit = find(id) ?? die(`no decision ${id} is waiting on you`);
1856
+ console.log(`${B("Decision required")} ${hit.o.id}${conns.length > 1 ? ` ${D("in " + hit.cfg.space)}` : ""}`);
1857
+ console.log(` ${hit.o.body}`);
1858
+ if (hit.o.goal)
1859
+ console.log(` goal ${hit.client.goals.get(hit.o.goal)?.title ?? hit.o.goal}`);
1860
+ console.log(`\nAnswer: ${C(`pantheon approvals answer ${hit.o.id} 'your decision and rationale'`)}`);
1861
+ closeAll();
1862
+ return;
1863
+ }
1864
+ if (sub !== "list")
1865
+ die("usage: pantheon approvals [list | show <id> | answer <id> <decision>]");
1866
+ if (!all.length)
1867
+ console.log(D(conns.length > 1 ? `no decisions waiting on you in any of your ${conns.length} teams` : "no decisions waiting on you"));
1868
+ for (const { o, cfg, client } of all)
1869
+ console.log(`${C(o.id)} ${conns.length > 1 ? D(`[${cfg.space}] `) : ""}${client.goals.get(o.goal ?? "")?.title ?? "No goal"}\n ${o.body}`);
1870
+ closeAll();
1871
+ return;
1872
+ }
1873
+ case "approvals-legacy-unreachable": {
1738
1874
  const cfg = need();
1739
1875
  const sub = argv[1] ?? "list";
1740
1876
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
@@ -1773,6 +1909,41 @@ async function main() {
1773
1909
  return;
1774
1910
  }
1775
1911
  case "worker": {
1912
+ const allCfg = loadAll();
1913
+ const bindings = Object.values(allCfg?.bindings ?? {});
1914
+ if (!flag("project") && bindings.length) {
1915
+ const { WorkerFleet } = await import("./worker-fleet.js");
1916
+ const fleet = new WorkerFleet({
1917
+ ...(flag("agent-cmd") ? { agentCmd: flag("agent-cmd") } : { agent: flag("agent", "auto") }),
1918
+ ...(flag("persona") ? { persona: flag("persona") } : {}),
1919
+ continuations: !argv.includes("--no-continue"),
1920
+ allowBash: argv.includes("--allow-bash"),
1921
+ idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000,
1922
+ maxRunsPerHour: numFlag("max-runs", 12),
1923
+ maxRunsPerDay: numFlag("max-runs-day", 40),
1924
+ graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
1925
+ once: argv.includes("--once"),
1926
+ ...(argv.includes("--no-notify") ? {} : {
1927
+ notify: (n) => {
1928
+ try {
1929
+ const c = load();
1930
+ if (c)
1931
+ sendDesktopNotification(n.title, n.subtitle, n.body, loadProfile(c), "", n.eyebrow);
1932
+ }
1933
+ catch { }
1934
+ },
1935
+ }),
1936
+ log: (l) => console.error(l),
1937
+ });
1938
+ console.error(`${B("pantheon worker")} ${D(`(${bindings.length} bound repo${bindings.length === 1 ? "" : "s"} · ctrl-c to stop)`)}`);
1939
+ if (argv.includes("--allow-bash")) {
1940
+ console.error(`\x1b[33m --allow-bash: a woken run may execute commands. Only do this if you trust every\x1b[0m`);
1941
+ console.error(`\x1b[33m peer you granted --may-wake, since their request is what starts the run.\x1b[0m`);
1942
+ }
1943
+ process.on("SIGINT", () => { fleet.stop(); process.exit(0); });
1944
+ await fleet.run();
1945
+ return;
1946
+ }
1776
1947
  const cfg = need();
1777
1948
  const project = resolve(flag("project", "."));
1778
1949
  const worker = new Worker(cfg, {
@@ -1833,15 +2004,20 @@ async function main() {
1833
2004
  return;
1834
2005
  }
1835
2006
  case "notifier": {
1836
- const cfg = need();
1837
- const notifier = new HumanNotifier(cfg, {
2007
+ const nf = loadAll();
2008
+ const teams = nf ? Object.values(nf.spaces ?? {}).map((m) => configFor(nf, m, null)) : [];
2009
+ const cfgs = teams.length ? teams : [need()];
2010
+ const notifiers = cfgs.map((c) => new HumanNotifier(c, {
1838
2011
  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();
2012
+ ...(cfgs.length > 1 ? { label: c.space, stateFile: join(CONFIG_DIR, `notifier-state-${encodeURIComponent(c.spaceId ?? c.space)}.json`) } : {}),
2013
+ log: (line) => console.error(cfgs.length > 1 ? `[${c.space}] ${line}` : line),
2014
+ }));
2015
+ console.error(`${B("pantheon notifier")} ${D(`(${cfgs.length === 1 ? "" : cfgs.length + " teams · "}ctrl-c to stop)`)}`);
2016
+ const stopAll = () => { for (const n of notifiers)
2017
+ n.stop(); process.exit(0); };
2018
+ process.on("SIGINT", stopAll);
2019
+ process.on("SIGTERM", stopAll);
2020
+ await Promise.all(notifiers.map((n) => n.run()));
1845
2021
  return;
1846
2022
  }
1847
2023
  case "doctor": {
@@ -1927,6 +2103,78 @@ async function main() {
1927
2103
  console.error(`\n${D("It only acts on requests from peers you granted --may-wake, one run at a time.")}`);
1928
2104
  return;
1929
2105
  }
2106
+ case "spaces": {
2107
+ const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
2108
+ const spaces = Object.values(all.spaces ?? {});
2109
+ if (!spaces.length) {
2110
+ console.error(D("no teams yet — pantheon start (create one) or pantheon join <invite>"));
2111
+ return;
2112
+ }
2113
+ const here = resolveFor(process.cwd());
2114
+ console.error(`${B("your teams")} ${D(`(${spaces.length})`)}\n`);
2115
+ for (const m of spaces) {
2116
+ const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id);
2117
+ 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(" ");
2118
+ console.error(` ${B(m.name.padEnd(18))} ${D(new URL(m.relay).host)} ${tags}`);
2119
+ for (const b of bound)
2120
+ console.error(` ${D("repo")} ${b.origin ?? b.path}${b.origin ? D(` (${b.path})`) : ""}`);
2121
+ if (!bound.length)
2122
+ console.error(` ${D("repo none bound — run `pantheon use " + m.name + " --here` from its checkout")}`);
2123
+ }
2124
+ const r = repoInfo(process.cwd());
2125
+ 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"}`);
2126
+ return;
2127
+ }
2128
+ case "use": {
2129
+ const all = loadAll() ?? die("not configured. Run `pantheon signup` first.");
2130
+ const ref = argv[1] ?? die("usage: pantheon use <team> [--here] # --here binds this repo to the team; without it, sets your default");
2131
+ 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"}`);
2132
+ if (argv.includes("--here")) {
2133
+ const r = repoInfo(process.cwd()) ?? die("this folder is not inside a git repo. Run it from the team's checkout.");
2134
+ const { replaced } = bindRepo(r, m.id);
2135
+ console.error(`${B("bound")} ${r.origin ?? r.root} → "${m.name}"${replaced ? D(` (was: ${findMembership(all, replaced.space)?.name ?? replaced.space})`) : ""}`);
2136
+ if (!r.shared)
2137
+ console.error(D(" this repo has no origin remote — deliveries from it cannot be fetched by teammates"));
2138
+ const c = resolveFor(process.cwd()).cfg;
2139
+ if (c && c.space === m.name) {
2140
+ const { repo: _r, repoOrigin: _o, ...plain } = c;
2141
+ const client = new PantheonClient(plain, undefined, { role: "cli" });
2142
+ 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)`)));
2143
+ client.close();
2144
+ }
2145
+ console.error(D(` Agents opened in this repo now speak as you in "${m.name}". Restart any that are open.`));
2146
+ }
2147
+ else {
2148
+ setDefaultSpace(m.id);
2149
+ console.error(`${B("default")} team is now "${m.name}" — used by your own commands when you run them outside a bound repo.`);
2150
+ console.error(D(" Agents never use the default; bind each repo with: pantheon use <team> --here"));
2151
+ }
2152
+ return;
2153
+ }
2154
+ case "repo": {
2155
+ const cfg = need();
2156
+ const sub = argv[1] ?? "list";
2157
+ const { repo: _r, repoOrigin: _o, ...plain } = cfg;
2158
+ const client = new PantheonClient(plain, undefined, { role: "cli" });
2159
+ await client.connect().catch((e) => die(client.pending ?? e.message));
2160
+ if (sub === "add") {
2161
+ const r = repoInfo(flag("path") ?? process.cwd()) ?? die("not inside a git repo");
2162
+ await client.bindRepo(r.fingerprint, r.origin).catch((e) => die(e.message));
2163
+ if (cfg.spaceId)
2164
+ bindRepo(r, cfg.spaceId);
2165
+ console.error(`${B("registered")} ${r.origin ?? r.root} as a repo "${cfg.space}" works from${r.shared ? "" : D(" (no origin remote — deliveries from it cannot be fetched)")}`);
2166
+ }
2167
+ else {
2168
+ await new Promise((res) => setTimeout(res, 150));
2169
+ console.error(`${B(`repos "${cfg.space}" works from`)} ${D(`(${client.repos.length})`)}`);
2170
+ for (const r of client.repos)
2171
+ console.error(` ${(r.origin ?? r.fingerprint).padEnd(48)} ${D(`added by ${r.added_by}`)}`);
2172
+ if (!client.repos.length)
2173
+ console.error(D(" none yet — the first member to connect from a checkout registers it"));
2174
+ }
2175
+ client.close();
2176
+ return;
2177
+ }
1930
2178
  case "mcp":
1931
2179
  await runMcp();
1932
2180
  return;
package/dist/client.js CHANGED
@@ -36,6 +36,8 @@ export class PantheonClient {
36
36
  session;
37
37
  role;
38
38
  enrolledToken = null;
39
+ spaceId = null;
40
+ repos = [];
39
41
  enroll = null;
40
42
  inviteUrl = null;
41
43
  constructor(cfg, onEvent, opts) {
@@ -115,6 +117,7 @@ export class PantheonClient {
115
117
  agent: this.cfg.agent ?? "unknown",
116
118
  pkg: VERSION,
117
119
  from_seq: this.seq,
120
+ ...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: this.cfg.repoOrigin } : {}) } : {}),
118
121
  }));
119
122
  break;
120
123
  case "pending":
@@ -149,6 +152,12 @@ export class PantheonClient {
149
152
  this.enrolledToken = msg.token;
150
153
  this.cfg.token = msg.token;
151
154
  }
155
+ if (msg.space_id) {
156
+ this.spaceId = msg.space_id;
157
+ this.cfg.spaceId = msg.space_id;
158
+ }
159
+ this.repos = msg.repos ?? [];
160
+ this.adoptKnownRepo();
152
161
  this.backoff = 500;
153
162
  this.roster = msg.roster;
154
163
  this.obligations = msg.obligations;
@@ -161,6 +170,10 @@ export class PantheonClient {
161
170
  resolve();
162
171
  }
163
172
  break;
173
+ case "repos":
174
+ this.repos = msg.repos;
175
+ this.adoptKnownRepo();
176
+ break;
164
177
  case "act":
165
178
  if (msg.act.seq > this.seq)
166
179
  this.seq = msg.act.seq;
@@ -308,7 +321,9 @@ export class PantheonClient {
308
321
  });
309
322
  }
310
323
  async send(a) {
311
- const r = await this.rpc({ t: "act", ...a });
324
+ const repo = this.cfg.repo;
325
+ const meta = a.act === "DELIVERED" && repo && !(a.meta && "repo" in a.meta) ? { ...(a.meta ?? {}), repo } : a.meta;
326
+ const r = await this.rpc({ t: "act", ...a, ...(meta ? { meta } : {}), ...(repo ? { repo } : {}) });
312
327
  if (a.ref) {
313
328
  for (const [id, o] of this.claimedByMe)
314
329
  if (o.ref === a.ref)
@@ -350,6 +365,21 @@ export class PantheonClient {
350
365
  publishProfile(profile) {
351
366
  return this.rpc({ t: "publish_profile", profile });
352
367
  }
368
+ adoptKnownRepo() {
369
+ const c = this.cfg;
370
+ if (c.repo || !c.cwdRepo)
371
+ return;
372
+ if (!this.repos.some((r) => r.fingerprint === c.cwdRepo))
373
+ return;
374
+ c.repo = c.cwdRepo;
375
+ if (c.cwdRepoOrigin)
376
+ c.repoOrigin = c.cwdRepoOrigin;
377
+ }
378
+ bindRepo(fingerprint = this.cfg.repo, origin = this.cfg.repoOrigin ?? null) {
379
+ if (!fingerprint)
380
+ return Promise.reject(new Error("not inside a git repo — run this from the team's checkout"));
381
+ return this.rpc({ t: "bind_repo", repo: fingerprint, origin });
382
+ }
353
383
  makeInvite(mayWake = true, ttlMinutes = 60) {
354
384
  return new Promise((res, rej) => {
355
385
  const t = setTimeout(() => rej(new Error("relay did not return an invite in 10s")), 10_000);