@join-pantheon/cli 0.31.1 → 0.31.2-rc.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
@@ -7,6 +7,7 @@ process.emitWarning = ((warning, ...rest) => {
7
7
  return _emitWarning(warning, ...rest);
8
8
  });
9
9
  import { randomBytes } from "node:crypto";
10
+ import { gitConfigValue, suggestUsername } from "./onboard-defaults.js";
10
11
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
11
12
  import { basename, dirname, join, resolve } from "node:path";
12
13
  import { PantheonClient } from "./client.js";
@@ -14,13 +15,13 @@ import { agentSpawn, CmdLineUnsafe } from "./which.js";
14
15
  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";
15
16
  import { fmtLimit } from "./plans.js";
16
17
  import { validEmail, validUsername } from "./accounts.js";
17
- import { install, launcher, installForUser } from "./install.js";
18
+ import { install, launcher, installForUser, windowsRepoOverrides } from "./install.js";
18
19
  import { Relay } from "./relay.js";
19
20
  import { runMcp } from "./mcp.js";
20
21
  import { Worker } from "./worker.js";
21
22
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, runNotifiers, sendDesktopNotification } from "./notifier.js";
22
23
  import { desktopUnavailable, hostEnv } from "./host-env.js";
23
- import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault, wakeMode } from "./profile.js";
24
+ import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, teamProfile, wakeDefault, wakeMode } from "./profile.js";
24
25
  import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
25
26
  import { selectActs, formatAct, resolveRef } from "./history-view.js";
26
27
  import { doctor, render, shareBlock } from "./doctor.js";
@@ -69,7 +70,7 @@ async function signedFetch(cfg, method, path, params = {}) {
69
70
  }
70
71
  return res;
71
72
  }
72
- function requireSignInOnHostedRelay(relayFlag) {
73
+ async function requireSignInOnHostedRelay(relayFlag) {
73
74
  const cfg = identity();
74
75
  if (cfg?.account)
75
76
  return;
@@ -77,6 +78,13 @@ function requireSignInOnHostedRelay(relayFlag) {
77
78
  const hosted = relay === DEFAULT_RELAY && !relayFlag && !process.env.PANTHEON_RELAY;
78
79
  if (!hosted)
79
80
  return;
81
+ if (process.stdin.isTTY) {
82
+ if (await interactiveOnboard(relay, { starting: true }) && identity()?.account) {
83
+ console.error("");
84
+ return;
85
+ }
86
+ die("not signed in yet.\n When the code arrives: pantheon verify <code>\n Then run pantheon start again.");
87
+ }
80
88
  die(`sign in first — this relay verifies email.\n New here: pantheon signup --email you@work.com --username you\n Have an account: pantheon login --email you@work.com\n Then run pantheon start again.`);
81
89
  }
82
90
  async function requireSignInOnAccountsRelay(relayFlag) {
@@ -91,17 +99,24 @@ async function requireSignInOnAccountsRelay(relayFlag) {
91
99
  catch {
92
100
  return;
93
101
  }
102
+ if (hasAccounts && process.stdin.isTTY) {
103
+ if (await interactiveOnboard(relayFlag, { starting: true }) && identity()?.account) {
104
+ console.error("");
105
+ return;
106
+ }
107
+ die("not signed in yet.\n When the code arrives: pantheon verify <code>\n Then run this same pantheon start again.");
108
+ }
94
109
  if (hasAccounts)
95
110
  die(`sign in first — ${relayFlag} verifies email.${signInHint("NOT_SIGNED_IN", relayFlag)}\n Then run this same pantheon start again.`);
96
111
  }
97
- async function interactiveOnboard(relayArg) {
112
+ async function interactiveOnboard(relayArg, opts = {}) {
98
113
  const prior = identity();
99
114
  const relay = relayArg ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
100
115
  const base = relay.replace(/^ws/, "http").replace(/\/+$/, "");
101
116
  if (!process.stdin.isTTY) {
102
117
  console.error(`${B("pantheon")} — sign in to get started:`);
103
118
  console.error(` ${C("pantheon signup --email you@work.com --username you")} then ${C("pantheon verify <code>")}`);
104
- return;
119
+ return false;
105
120
  }
106
121
  const rl = createInterface({ input: process.stdin, output: process.stderr });
107
122
  const ask = async (q) => {
@@ -117,10 +132,16 @@ async function interactiveOnboard(relayArg) {
117
132
  const postJson = async (path, body) => fetch(`${base}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
118
133
  .then(async (x) => ({ status: x.status, j: await x.json() }));
119
134
  try {
120
- console.error(`${B("Welcome to pantheon.")} Let's get this machine signed in.\n`);
135
+ console.error(opts.joining
136
+ ? `${B("Welcome to Pantheon.")} You've been invited to "${opts.joining}". First, sign this machine in — your email and a 6-digit code, nothing else.\n`
137
+ : opts.starting
138
+ ? `${B("Welcome to Pantheon.")} First, sign this machine in — your email and a 6-digit code, nothing else. Then your team is created.\n`
139
+ : `${B("Welcome to pantheon.")} Let's get this machine signed in.\n`);
140
+ const gitEmail = gitConfigValue("user.email");
141
+ const emailGuess = validEmail(gitEmail) ? gitEmail : "";
121
142
  let email = "";
122
143
  while (!email) {
123
- const e = await ask(" Your work email: ");
144
+ const e = (await ask(` Your work email${emailGuess ? ` [${emailGuess}]` : ""}: `)) || emailGuess;
124
145
  if (validEmail(e))
125
146
  email = e;
126
147
  else
@@ -129,8 +150,9 @@ async function interactiveOnboard(relayArg) {
129
150
  let started = null;
130
151
  let mode = "signup";
131
152
  let signupUsername = "";
153
+ let userGuess = suggestUsername(email);
132
154
  while (!started || started.status !== 200) {
133
- const u = await ask(" Pick a username: ");
155
+ const u = (await ask(` Pick a username${userGuess ? ` [${userGuess}]` : ""}: `)) || userGuess;
134
156
  if (!validUsername(u.toLowerCase())) {
135
157
  console.error(D(" 2–32 chars: letters, digits, . _ - (not at the ends)"));
136
158
  continue;
@@ -138,25 +160,28 @@ async function interactiveOnboard(relayArg) {
138
160
  signupUsername = u;
139
161
  started = await postJson("/auth/signup", { email, username: u, pubkey: kp.publicKey, label }).catch((e) => { console.error(D(` could not reach the relay: ${e.message}`)); return null; });
140
162
  if (!started)
141
- return;
163
+ return false;
142
164
  if (started.status === 404) {
143
165
  console.error(`\n ${D("This relay is invite-based (no email accounts). Get started with:")}`);
144
166
  console.error(` ${C("pantheon start")} ${D("(create a team)")} or ${C("pantheon join <invite>")}`);
145
- return;
167
+ return false;
146
168
  }
147
169
  if (started.status === 200)
148
170
  break;
149
171
  if (started.j.reason === "email_taken") {
150
172
  console.error(D(` that email already has an account — signing this device in instead…`));
151
- started = await postJson("/auth/login", { email, pubkey: kp.publicKey, label });
173
+ started = await postJson("/auth/login", { email, pubkey: kp.publicKey, label }).catch((e) => { console.error(D(` could not reach the relay: ${e.message}`)); return null; });
174
+ if (!started)
175
+ return false;
152
176
  mode = "login";
153
177
  break;
154
178
  }
155
179
  console.error(D(` ${started.j.error ?? "try a different username"}`));
180
+ userGuess = suggestUsername(email, Math.floor(10 + Math.random() * 90));
156
181
  }
157
182
  if (!started || started.status !== 200) {
158
183
  console.error(D(` ${started?.j.error ?? "sign-up failed"}`));
159
- return;
184
+ return false;
160
185
  }
161
186
  const authUser = mode === "signup" ? signupUsername : undefined;
162
187
  save({ ...(prior ?? {}), relay, member: signupUsername || prior?.member || "me",
@@ -182,7 +207,7 @@ async function interactiveOnboard(relayArg) {
182
207
  console.error(D(` could not resend: ${rs?.j.error ?? "the relay did not send an email"} — try again in a minute.`));
183
208
  continue;
184
209
  }
185
- const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey });
210
+ const v = await postJson("/auth/verify", { email, code, pubkey: kp.publicKey }).catch((e) => ({ status: 0, j: { error: `could not reach the relay (${e.message}) — try the same code again` } }));
186
211
  if (v.status === 200) {
187
212
  const acct = v.j.account;
188
213
  const cfg = { ...(prior ?? {}), relay, member: acct.username, space: prior?.space ?? "", token: prior?.token ?? "",
@@ -191,14 +216,17 @@ async function interactiveOnboard(relayArg) {
191
216
  delete cfg.pendingAuth;
192
217
  save(cfg);
193
218
  console.error(`\n ${C("You're in")} — signed in as ${B(acct.username)} <${acct.email}> on this machine.\n`);
219
+ if (opts.joining || opts.starting)
220
+ return true;
194
221
  console.error(` ${B("Next:")} ${C("pantheon start")} ${D("(create a team, get a link to send)")}`);
195
222
  console.error(` ${C("pantheon join <invite-link>")} ${D("(join a teammate's team)")}`);
196
- return;
223
+ return true;
197
224
  }
198
225
  const spent = v.j.reason === "expired" || v.j.reason === "too_many_attempts" || v.j.tries_left === 0;
199
226
  console.error(D(` ${v.j.error ?? "that code is not right"}${spent ? " — type 'resend' for a new code" : " — try again"}`));
200
227
  }
201
228
  console.error(D("\n Too many tries. Run `pantheon` again to restart, or `pantheon verify <code>` once you have it."));
229
+ return false;
202
230
  }
203
231
  finally {
204
232
  rl.close();
@@ -532,6 +560,20 @@ function printRelayNote(relay) {
532
560
  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
561
  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
562
  }
563
+ function windowsOverridesForBoundRepos() {
564
+ const dirs = Object.values(loadAll()?.bindings ?? {}).map((b) => b.path).filter((p) => !!p && existsSync(p));
565
+ for (const r of windowsRepoOverrides(dirs))
566
+ console.error(` ${D("claude code in")} ${r.dir} ${D(r.status)}`);
567
+ }
568
+ function printJoinSteps(invite) {
569
+ console.error(`\n${B("For a teammate who'd rather not type commands, forward this whole block:")}\n`);
570
+ console.error(D(` You're invited to my Pantheon team. Paste these two lines into a terminal`));
571
+ console.error(D(` (Terminal on a Mac, Command Prompt on Windows), one at a time:`));
572
+ console.error(` ${C("npm i -g @join-pantheon/cli")}`);
573
+ console.error(` ${C(`pantheon join ${invite}`)}`);
574
+ console.error(D(` The second line asks for your email, sends you a 6-digit code, and sets up`));
575
+ console.error(D(` the coding agents on your computer. Needs Node.js 22.13 or newer: nodejs.org.`));
576
+ }
535
577
  const HUD_ACTION_MAX_AGE_MS = 30_000;
536
578
  function unattendedProject() {
537
579
  if (argv.includes("--no-worker"))
@@ -644,6 +686,24 @@ async function readFailure(cfg, res, what) {
644
686
  }
645
687
  return new Error(`relay answered ${res.status} for ${what}${why ? `: ${why}` : ""}`);
646
688
  }
689
+ async function decisionBriefFor(cfg, client, o) {
690
+ const { decisionBrief, decisionTitle, stripCeremony } = await import("./decision-ui.js");
691
+ const act = client.seenAct(o.ref) ?? (await fetchAct(cfg, o.ref, 6000).catch(() => null))?.act ?? null;
692
+ const g = o.goal ? client.goals.get(o.goal) : undefined;
693
+ const thread = o.goal ? await fetchHistory(cfg, { goal: o.goal }).catch(() => []) : [];
694
+ const recent = o.goal
695
+ ? selectActs(thread, { goal: o.goal, last: 7 }).filter((a) => a.id !== o.ref).slice(-6)
696
+ .map((a) => ({ from: a.from, act: a.act, text: a.summary || a.body, at: a.ts }))
697
+ : [];
698
+ const from = stripCeremony(o.body).from ?? act?.from;
699
+ return decisionBrief({
700
+ team: cfg.space, decisionId: o.id, ref: o.ref, title: decisionTitle(o.body),
701
+ from: !from || from === cfg.member ? "your agent" : from,
702
+ question: act?.body ?? o.body,
703
+ goal: g ? { id: g.id, title: g.title, ...(g.acceptance ? { acceptance: g.acceptance } : {}) } : null,
704
+ recent,
705
+ });
706
+ }
647
707
  async function fetchAct(cfg, id, budgetMs = Number.POSITIVE_INFINITY) {
648
708
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
649
709
  const deadline = Date.now() + budgetMs;
@@ -1005,7 +1065,8 @@ async function main() {
1005
1065
  console.error(`${B("pantheon")} — signed in as ${C(cfg.account.username)}. ${D(`Your plate: pantheon brief · all commands: pantheon help`)}`);
1006
1066
  return;
1007
1067
  }
1008
- return interactiveOnboard();
1068
+ await interactiveOnboard();
1069
+ return;
1009
1070
  }
1010
1071
  const askedHelpOnly = argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h") && ["pending", "hook"].includes(cmd);
1011
1072
  if (!HELP_OPAQUE.has(cmd) || askedHelpOnly) {
@@ -1076,7 +1137,7 @@ async function main() {
1076
1137
  if (setupFlags.errors.length)
1077
1138
  die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
1078
1139
  const wp = unattendedProject();
1079
- requireSignInOnHostedRelay(flag("relay"));
1140
+ await requireSignInOnHostedRelay(flag("relay"));
1080
1141
  await requireSignInOnAccountsRelay(flag("relay"));
1081
1142
  console.error(`${B("pantheon start")}\n`);
1082
1143
  if (!flag("space") && !repoInfo(process.cwd()))
@@ -1127,8 +1188,7 @@ async function main() {
1127
1188
  console.error(r.inviteMayWake
1128
1189
  ? ` ${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")}`
1129
1190
  : 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>`));
1130
- console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
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}`)}`);
1191
+ printJoinSteps(link ?? r.invite);
1132
1192
  }
1133
1193
  if (!r.reach.shareable && r.reach.hint) {
1134
1194
  console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
@@ -1137,6 +1197,7 @@ async function main() {
1137
1197
  if (r.claimed) {
1138
1198
  await bindReposHere(argv.includes("--yes"));
1139
1199
  await workerAfterBinding(wp.how);
1200
+ windowsOverridesForBoundRepos();
1140
1201
  }
1141
1202
  printReady(r.tools);
1142
1203
  if (r.claimed)
@@ -1187,8 +1248,9 @@ async function main() {
1187
1248
  if (link)
1188
1249
  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)}`);
1189
1250
  console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
1190
- console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
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}`)}`);
1251
+ if (inv.note)
1252
+ console.error(`\x1b[33m ! ${inv.note}\x1b[0m`);
1253
+ printJoinSteps(link ?? raw);
1192
1254
  client.close();
1193
1255
  return;
1194
1256
  }
@@ -1299,12 +1361,18 @@ async function main() {
1299
1361
  parsed = await resolveInviteCode(invite, candidates).catch((err) => die(err.message.replace(/^INVITE_CODE_[A-Z]+:\s*/, "")));
1300
1362
  console.error(D(`That was just the code — it opens "${parsed.space}" on ${new URL(parsed.relay).host}. Joining that team.\n`));
1301
1363
  }
1364
+ if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY) && process.stdin.isTTY) {
1365
+ const ok = await interactiveOnboard(parsed.relay, { joining: parsed.space });
1366
+ if (!ok || !identity()?.account)
1367
+ die(`not signed in yet — the invite is still good.\n When the code arrives: pantheon verify <code>\n Then run this same \`pantheon join\` command again.`);
1368
+ console.error("");
1369
+ }
1302
1370
  if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
1303
1371
  die(`sign in first — this relay verifies email.\n New here: pantheon signup --email you@work.com --username you\n Have an account: pantheon login --email you@work.com\n Then run this same \`pantheon join\` command again — the invite is still good.`);
1304
1372
  }
1305
1373
  const wp = unattendedProject();
1306
1374
  console.error(`${B("pantheon join")}\n`);
1307
- const r = await qsJoin({
1375
+ const joinOnce = () => qsJoin({
1308
1376
  invite: parsed,
1309
1377
  ...(flag("me") ? { me: flag("me") } : {}),
1310
1378
  ...(argv.includes("--reset") ? { reset: true } : {}),
@@ -1314,6 +1382,15 @@ async function main() {
1314
1382
  unattended: wp.project,
1315
1383
  workerHow: wp.how,
1316
1384
  log: (l) => console.error(l),
1385
+ });
1386
+ const r = await joinOnce().catch(async (e) => {
1387
+ if (/NOT_SIGNED_IN/.test(e.message) && process.stdin.isTTY && !identity()?.account) {
1388
+ if (await interactiveOnboard(parsed.relay, { joining: parsed.space }) && identity()?.account) {
1389
+ console.error("");
1390
+ return joinOnce();
1391
+ }
1392
+ }
1393
+ throw e;
1317
1394
  }).catch((e) => {
1318
1395
  const m = e.message;
1319
1396
  if (/BAD_INVITE/.test(m)) {
@@ -1371,6 +1448,7 @@ async function main() {
1371
1448
  }
1372
1449
  }
1373
1450
  await workerAfterBinding(wp.how);
1451
+ windowsOverridesForBoundRepos();
1374
1452
  {
1375
1453
  const joined = load();
1376
1454
  if (joined)
@@ -2673,8 +2751,10 @@ async function main() {
2673
2751
  case "signup":
2674
2752
  case "login": {
2675
2753
  const purpose = cmd;
2676
- if (!flag("email") && process.stdin.isTTY)
2677
- return interactiveOnboard(flag("relay"));
2754
+ if (!flag("email") && process.stdin.isTTY) {
2755
+ await interactiveOnboard(flag("relay"));
2756
+ return;
2757
+ }
2678
2758
  const email = flag("email") ?? die(`usage: pantheon ${purpose} --email you@work.com${purpose === "signup" ? " --username <name>" : ""} [--relay wss://...]`);
2679
2759
  const username = purpose === "signup" ? (flag("username") ?? die("usage: pantheon signup --email you@work.com --username <name>")) : undefined;
2680
2760
  const prior = identity();
@@ -3001,7 +3081,8 @@ async function main() {
3001
3081
  if (to === cfg.member)
3002
3082
  die("you cannot delegate authority to yourself");
3003
3083
  const matches = (rule, domain) => rule === domain || rule === "*" || (rule.endsWith(".*") && (domain === rule.slice(0, -2) || domain.startsWith(rule.slice(0, -1))));
3004
- const owns = (domain) => Object.keys(profile.authority).some((rule) => matches(rule, domain));
3084
+ const teamAuthority = teamProfile(profile, cfg.spaceId, cfg.space).authority;
3085
+ const owns = (domain) => Object.keys(teamAuthority).some((rule) => matches(rule, domain));
3005
3086
  const notOwned = domains.filter((d) => !owns(d));
3006
3087
  if (notOwned.length)
3007
3088
  die(`you cannot delegate authority you do not own: ${notOwned.join(", ")}`);
@@ -3480,7 +3561,23 @@ async function main() {
3480
3561
  return;
3481
3562
  }
3482
3563
  if (hud?.action === "details") {
3483
- const wd = await workdirFor(cfg, hud).catch(() => null);
3564
+ let tcfg = cfg;
3565
+ let found = hud.act ? await fetchAct(cfg, hud.act, 8000).catch(() => null) : null;
3566
+ if (hud.act && !found) {
3567
+ const f = loadAll();
3568
+ for (const m of Object.values(f?.spaces ?? {})) {
3569
+ const c = configFor(f, m, null);
3570
+ if (c.space === cfg.space)
3571
+ continue;
3572
+ const x = await fetchAct(c, hud.act, 6000).catch(() => null);
3573
+ if (x) {
3574
+ found = x;
3575
+ tcfg = c;
3576
+ break;
3577
+ }
3578
+ }
3579
+ }
3580
+ const wd = await workdirFor(tcfg, { ...hud, space: tcfg.space }).catch(() => null);
3484
3581
  if (wd?.path) {
3485
3582
  try {
3486
3583
  process.chdir(wd.path);
@@ -3489,21 +3586,40 @@ async function main() {
3489
3586
  }
3490
3587
  if (wd)
3491
3588
  console.error(D(` ${wd.note}`));
3492
- const a = hud.act ? (await fetchAct(cfg, hud.act))?.act : undefined;
3589
+ const a = found?.act;
3590
+ const goalTitle = found?.goalTitle;
3493
3591
  let o;
3494
- if (hud.decision) {
3495
- const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
3496
- await c2.connect().catch((e) => die(connectFailure(e, cfg.relay)));
3497
- o = c2.obligations.find((x) => x.id === hud.decision);
3498
- c2.close();
3592
+ let owed = [];
3593
+ {
3594
+ const c2 = new PantheonClient(tcfg, undefined, { role: "cli" });
3595
+ try {
3596
+ await c2.connect();
3597
+ if (hud.decision)
3598
+ o = c2.obligations.find((x) => x.id === hud.decision);
3599
+ const g = a?.goal ?? o?.goal ?? null;
3600
+ owed = c2.obligations.filter((x) => g && x.goal === g && (x.owner === tcfg.member || x.owner === `human:${tcfg.member}`));
3601
+ }
3602
+ catch { }
3603
+ finally {
3604
+ c2.close();
3605
+ }
3499
3606
  }
3607
+ const req = a?.ref ? (await fetchAct(tcfg, a.ref, 6000).catch(() => null))?.act : undefined;
3608
+ const mine = !!a && a.from === tcfg.member;
3609
+ const peer = mine ? (req?.from ?? "a teammate") : (a?.from ?? "a teammate");
3610
+ const meta = (a?.meta ?? {});
3611
+ const block = (t, max) => {
3612
+ const ls = t.trim().split("\n");
3613
+ return ls.slice(0, max).map((l) => ` ${l}`).join("\n") + (ls.length > max ? `\n ${D(`… ${ls.length - max} more lines — 3 shows the whole thread`)}` : "");
3614
+ };
3500
3615
  if (a) {
3501
- console.log(formatAct(a));
3502
- if (a.ref) {
3503
- const r = (await fetchAct(cfg, a.ref))?.act;
3504
- if (r)
3505
- console.log(D("\nin reply to:\n") + formatAct(r, { short: true }));
3506
- }
3616
+ console.log(`${B(mine ? `Your worker answered ${peer}` : `${a.from} sent ${a.act === "ASK" ? "a question" : a.act === "DELIVERED" ? "a delivery" : "a message"}`)}${D(` · team ${tcfg.space}${goalTitle ? ` · "${goalTitle}"` : ""} · ${ago(a.ts)}`)}`);
3617
+ if (req)
3618
+ console.log(`\n${B(mine ? `What ${peer} asked` : "In reply to")}\n${block(req.body, 12)}`);
3619
+ console.log(`\n${B(mine ? `What it replied (${a.act === "DELIVERED" ? "delivered" : a.act === "ANSWER" ? "answer" : a.act.toLowerCase()})` : "What they said")}\n${block(a.body, 20)}`);
3620
+ if (meta.branch)
3621
+ console.log(`\n${B("The work")}\n branch ${C(meta.branch)}${meta.commit ? ` at ${meta.commit.slice(0, 12)}` : ""}\n ${D(`review it here: git log -p ${meta.branch}`)}`);
3622
+ console.log(`\n${B("Owed by you")}${owed.length ? "\n" + owed.map((x) => ` • ${x.body}`).join("\n") : ` ${D("nothing in this goal right now")}`}`);
3507
3623
  }
3508
3624
  else if (!o)
3509
3625
  console.log(D("that act is no longer in what the relay holds"));
@@ -3526,7 +3642,26 @@ async function main() {
3526
3642
  break;
3527
3643
  if (choice === "1" && agent) {
3528
3644
  rl.close();
3529
- const prompt = o ? dp(o.id, dt(o.body)) : `Read Pantheon act ${a.id}${goalId ? ` and the thread of goal ${goalId}` : ""}. Explain in plain words what happened and whether anything is owed by me, then give me numbered next steps. Do not act until I choose.`;
3645
+ let prompt = o ? dp(o.id, dt(o.body)) : "";
3646
+ if (o) {
3647
+ const c3 = new PantheonClient(tcfg, undefined, { role: "cli" });
3648
+ try {
3649
+ await c3.connect();
3650
+ prompt = await decisionBriefFor(tcfg, c3, o);
3651
+ }
3652
+ catch { }
3653
+ finally {
3654
+ c3.close();
3655
+ }
3656
+ }
3657
+ else {
3658
+ const { runBrief } = await import("./decision-ui.js");
3659
+ prompt = runBrief({ team: tcfg.space, peer, goal: goalTitle ? { title: goalTitle } : null, request: req?.body ?? "(the request is no longer held by the relay)",
3660
+ reply: mine ? { act: a.act, text: a.body } : null, ...(meta.branch ? { branch: meta.branch } : {}), ...(meta.commit ? { commit: meta.commit } : {}),
3661
+ owedByMe: owed.map((x) => x.body), ok: true });
3662
+ if (!mine)
3663
+ prompt = prompt.replace(/^Pantheon: my worker handled .*$/m, `Pantheon: ${a.from}'s ${a.act.toLowerCase()} in "${tcfg.space}"`);
3664
+ }
3530
3665
  console.error(D(`opening ${agent.id}…`));
3531
3666
  await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
3532
3667
  return;
@@ -3606,6 +3741,19 @@ async function main() {
3606
3741
  await launchAgent(bin, (p) => [p], hud.prompt, cwd ?? undefined);
3607
3742
  return;
3608
3743
  }
3744
+ if (hud.decision) {
3745
+ const cl = new PantheonClient(cfg, undefined, { role: "cli" });
3746
+ try {
3747
+ await cl.connect();
3748
+ const o = cl.obligations.find((x) => x.id === hud.decision);
3749
+ if (o)
3750
+ hud.prompt = await decisionBriefFor(cfg, cl, o);
3751
+ }
3752
+ catch { }
3753
+ finally {
3754
+ cl.close();
3755
+ }
3756
+ }
3609
3757
  if (hud.prompt) {
3610
3758
  const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
3611
3759
  if (!agent)
@@ -3624,24 +3772,50 @@ async function main() {
3624
3772
  catch { }
3625
3773
  }
3626
3774
  }
3627
- const client = new PantheonClient(cfg, undefined, { role: "cli" });
3628
- await client.connect().catch((e) => die(connectFailure(e, cfg.relay)));
3629
- const waiting = client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`);
3630
- client.close();
3631
- const one = id ? waiting.find((o) => o.id === id) : waiting.length === 1 ? waiting[0] : undefined;
3632
- if (id && !one)
3633
- die(`no decision ${id} is waiting on you`);
3634
- const prompt = one ? dp(one.id, dt(one.body)) : waiting.length > 1 ? adp(waiting.length) : null;
3775
+ const every = (() => { const f = loadAll(); return f && Object.keys(f.spaces ?? {}).length ? Object.values(f.spaces).map((m) => configFor(f, m, null)) : [cfg]; })();
3776
+ const open = [];
3777
+ const clients = [];
3778
+ for (const tc of every) {
3779
+ const cl = new PantheonClient(tc, undefined, { role: "cli" });
3780
+ try {
3781
+ await cl.connect();
3782
+ }
3783
+ catch {
3784
+ cl.close();
3785
+ continue;
3786
+ }
3787
+ clients.push(cl);
3788
+ for (const o of cl.obligations)
3789
+ if (o.kind === "HUMAN" && o.owner === `human:${tc.member}`)
3790
+ open.push({ cfg: tc, client: cl, o });
3791
+ }
3792
+ const closeAll = () => { for (const c of clients)
3793
+ c.close(); };
3794
+ const one = id ? open.find((x) => x.o.id === id) : open.length === 1 ? open[0] : undefined;
3795
+ if (id && !one) {
3796
+ closeAll();
3797
+ die(`no decision ${id} is waiting on you in ${every.length > 1 ? `any of your ${every.length} teams` : `"${cfg.space}"`}`);
3798
+ }
3799
+ let prompt = null;
3800
+ let cwd;
3801
+ if (one) {
3802
+ prompt = await decisionBriefFor(one.cfg, one.client, one.o).catch(() => dp(one.o.id, dt(one.o.body)));
3803
+ cwd = (await workdirFor(one.cfg, { action: "open", space: one.cfg.space, ...(one.o.goal ? { goal: one.o.goal } : {}) }).catch(() => null))?.path ?? undefined;
3804
+ }
3805
+ else if (open.length > 1)
3806
+ prompt = adp(open.length);
3807
+ closeAll();
3635
3808
  if (!prompt) {
3636
3809
  console.log(D("no decisions waiting on you — nothing to explain"));
3637
3810
  return;
3638
3811
  }
3639
- const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
3812
+ const agent = pickExplainAgent(loadProfile(one?.cfg ?? cfg).agent.primary_surface, detectAvailable());
3640
3813
  if (!agent)
3641
3814
  die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
3642
- console.error(D(`opening ${agent.id} with the question…`));
3815
+ console.error(D(`opening ${agent.id} with the question${one ? ` (team "${one.cfg.space}")` : ""}…`));
3643
3816
  const { spawnSync } = await import("node:child_process");
3644
- await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(prompt)), prompt);
3817
+ const finalPrompt = prompt;
3818
+ await launchAgent(agent.binary, (p) => launchArgs(agent.id, p, sessionName(finalPrompt)), finalPrompt, cwd);
3645
3819
  return;
3646
3820
  }
3647
3821
  case "approvals": {
@@ -3817,10 +3991,31 @@ async function main() {
3817
3991
  return;
3818
3992
  }
3819
3993
  case "tray-detail": {
3820
- const key = argv[1] ?? die("usage: pantheon tray-detail <act id>");
3821
- const cfgNow = load() ?? die("not joined to a team");
3994
+ const key = argv[1] ?? die("usage: pantheon tray-detail <act id> [--space <team>]");
3995
+ const fileNow = loadAll();
3996
+ const named = flag("space") && fileNow ? findMembership(fileNow, flag("space")) : undefined;
3997
+ const tryCfgs = [
3998
+ ...(named && fileNow ? [configFor(fileNow, named, null)] : []),
3999
+ ...(fileNow ? Object.values(fileNow.spaces ?? {}).filter((m) => m !== named).map((m) => configFor(fileNow, m, null)) : []),
4000
+ ];
4001
+ if (!tryCfgs.length) {
4002
+ const c0 = load();
4003
+ if (c0)
4004
+ tryCfgs.push(c0);
4005
+ }
4006
+ if (!tryCfgs.length)
4007
+ die("not joined to a team");
3822
4008
  const { humanize } = await import("./humanize.js");
3823
- const found = await fetchAct(cfgNow, key);
4009
+ let found = null;
4010
+ let cfgNow = tryCfgs[0];
4011
+ for (const c of tryCfgs) {
4012
+ const x = await fetchAct(c, key, 8000).catch(() => null);
4013
+ if (x) {
4014
+ found = x;
4015
+ cfgNow = c;
4016
+ break;
4017
+ }
4018
+ }
3824
4019
  if (!found) {
3825
4020
  console.log(JSON.stringify({ ok: false }));
3826
4021
  return;
@@ -3833,8 +4028,12 @@ async function main() {
3833
4028
  c2.close();
3834
4029
  }
3835
4030
  const { headline, blocks } = humanize(a.body, { ...(goalTitle ? { goalTitle } : {}), from: a.from });
4031
+ const asker = a.from !== cfgNow.member ? a.from : a.sig?.role === "run" ? "your worker" : "your agent";
4032
+ const { answerOptions } = await import("./decision-ui.js");
4033
+ const options = ["ESCALATE", "HUMAN_REQUIRED", "ASK"].includes(a.act) ? answerOptions(a.body) : [];
3836
4034
  console.log(JSON.stringify({
3837
- ok: true, id: a.id, act: a.act, from: a.from, ts: a.ts,
4035
+ ok: true, id: a.id, act: a.act, from: a.from, ts: a.ts, asker, team: cfgNow.space,
4036
+ ...(options.length ? { options } : {}),
3838
4037
  ...(a.summary ? { summary: a.summary } : {}),
3839
4038
  ...(goalTitle ? { goal_title: goalTitle } : {}),
3840
4039
  headline, blocks, raw: a.body,
@@ -4062,6 +4261,7 @@ async function main() {
4062
4261
  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)`)));
4063
4262
  client.close();
4064
4263
  }
4264
+ windowsOverridesForBoundRepos();
4065
4265
  console.error(D(` Agents opened in this repo now speak as you in "${m.name}". Restart any that are open.`));
4066
4266
  }
4067
4267
  else {
@@ -4092,6 +4292,7 @@ async function main() {
4092
4292
  await client.bindRepo(r.fingerprint, r.origin, base).catch((e) => die(e.message));
4093
4293
  if (cfg.spaceId)
4094
4294
  bindRepo(r, cfg.spaceId);
4295
+ windowsOverridesForBoundRepos();
4095
4296
  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)")}`);
4096
4297
  }
4097
4298
  else {
package/dist/client.js CHANGED
@@ -247,7 +247,7 @@ export class PantheonClient {
247
247
  this.invitesList = msg.invites;
248
248
  break;
249
249
  case "invite":
250
- this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
250
+ this.inviteUrl = { url: msg.url, expires_at: msg.expires_at, ...(typeof msg.note === "string" ? { note: msg.note } : {}) };
251
251
  break;
252
252
  case "welcome":
253
253
  if (this.cfg.spaceId && msg.space_id && msg.space_id !== this.cfg.spaceId) {
@@ -622,7 +622,7 @@ export class PantheonClient {
622
622
  }
623
623
  const r = verifyAct(rec, [this.cfg.spaceId, this.spaceId, this.cfg.space], pin?.fp, pin?.key);
624
624
  if (trusted(r.trust) && rec.sig && !this.verifiedIds.has(rec.id)) {
625
- this.verifiedIds.set(rec.id, { member: rec.from, nonce: rec.sig.nonce });
625
+ this.verifiedIds.set(rec.id, { member: rec.from, nonce: rec.sig.nonce, sig: rec.sig.s });
626
626
  if (this.verifiedIds.size > 5000)
627
627
  this.verifiedIds.delete(this.verifiedIds.keys().next().value);
628
628
  }
@@ -660,7 +660,7 @@ export class PantheonClient {
660
660
  async noteAnswered(ref) {
661
661
  const known = this.verifiedIds.get(ref);
662
662
  if (known) {
663
- markAnswered(this.pinTeam, known.member, known.nonce, ref);
663
+ markAnswered(this.pinTeam, known.member, known.nonce, ref, undefined, known.sig);
664
664
  return true;
665
665
  }
666
666
  let r = this.seen.get(ref) ?? null;
package/dist/config.js CHANGED
@@ -599,6 +599,7 @@ export function toJoinLink(cfg) {
599
599
  export function fromInvite(invite) {
600
600
  const clean = invite
601
601
  .replace(/\u001b\[[0-9;]*m/g, "")
602
+ .replace(/[\r\n\t]+/g, " ")
602
603
  .replace(/[\u0000-\u001f\u007f]/g, "")
603
604
  .trim()
604
605
  .replace(/^['"`]|['"`]$/g, "")