@join-pantheon/cli 0.33.4-rc.1 → 0.33.5-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/accounts.js CHANGED
@@ -349,8 +349,10 @@ export class Accounts {
349
349
  this.db.exec("BEGIN");
350
350
  try {
351
351
  let account;
352
+ let welcome = false;
352
353
  if (v.purpose === "signup") {
353
354
  const existing = this.accountByEmail(e);
355
+ welcome = existing?.status !== "active";
354
356
  if (existing?.status === "suspended") {
355
357
  this.db.exec("ROLLBACK");
356
358
  return { ok: false, reason: "suspended" };
@@ -378,7 +380,7 @@ export class Accounts {
378
380
  run("DELETE FROM verify_failures WHERE email = ?").run(keyId);
379
381
  this.db.exec("COMMIT");
380
382
  this.audit({ actor_account: account.id, event: v.purpose === "signup" ? "email_verified" : "login", target: device.fingerprint, ip });
381
- return { ok: true, account, session, device };
383
+ return { ok: true, account, session, device, welcome };
382
384
  }
383
385
  catch (err) {
384
386
  this.db.exec("ROLLBACK");
package/dist/agents.js CHANGED
@@ -93,6 +93,29 @@ export function detectAvailable() {
93
93
  }
94
94
  return out;
95
95
  }
96
+ export function detectedAgentId(home = homedir()) {
97
+ const onPath = detectAvailable()[0]?.backend.id;
98
+ if (onPath)
99
+ return onPath;
100
+ for (const [dir, id] of [[".claude", "claude-code"], [".codex", "codex"], [".cursor", "cursor"], [".kiro", "kiro"], [".copilot", "copilot"]]) {
101
+ if (existsSync(join(home, dir)))
102
+ return id;
103
+ }
104
+ return null;
105
+ }
106
+ export function recordedAgent(prior, opts = {}) {
107
+ const known = (x) => (x && x.trim() && x !== "unknown" ? x : null);
108
+ const env = known((opts.env ?? process.env).PANTHEON_AGENT);
109
+ if (env)
110
+ return env;
111
+ const detect = () => { try {
112
+ return known((opts.detect ?? detectedAgentId)());
113
+ }
114
+ catch {
115
+ return null;
116
+ } };
117
+ return (opts.fresh ? detect() ?? known(prior) : known(prior) ?? detect()) ?? "unknown";
118
+ }
96
119
  export function detectOnPath() {
97
120
  for (const a of AGENTS) {
98
121
  if (resolveBinary(a))
@@ -11,7 +11,8 @@ export function claudeHomeConflict(env = process.env, os = platform(), home = ho
11
11
  return null;
12
12
  return `HOME (${env.HOME}) is not your Windows profile (${home}), whose ~/.claude.json Claude Code reads — it was left as it is`;
13
13
  }
14
- export const claudeUserEntryShape = (entry) => ({ type: "stdio", command: entry.command, args: [...entry.args], env: {} });
14
+ export const CLAUDE_AGENT_ENV = { PANTHEON_AGENT: "claude-code" };
15
+ export const claudeUserEntryShape = (entry) => ({ type: "stdio", command: entry.command, args: [...entry.args], env: { ...CLAUDE_AGENT_ENV } });
15
16
  const STALE_MS = 10_000;
16
17
  const WAIT_MS = 15_000;
17
18
  const sleepSync = (ms) => { try {
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, runNotifiers,
23
23
  import { linuxDesktopReachable } from "./notice-linux.js";
24
24
  import { desktopUnavailable, hostEnv } from "./host-env.js";
25
25
  import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, teamProfile, wakeDefault, wakeMode } from "./profile.js";
26
- import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
26
+ import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom, decisionAreasProblem } from "./setup-guide.js";
27
27
  import { selectActs, formatAct, resolveRef } from "./history-view.js";
28
28
  import { doctor, render, shareBlock } from "./doctor.js";
29
29
  import { FEEDBACK_ADDRESS } from "./feedback.js";
@@ -31,7 +31,7 @@ import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, d
31
31
  import { assess } from "./untrusted.js";
32
32
  import { report as spendReport, recentRuns } from "./spend.js";
33
33
  import { renderBrief, renderTeam } from "./brief.js";
34
- import { resolveDataDir, saveAll } from "./config.js";
34
+ import { resolveDataDir, saveAll, withConfigLock } from "./config.js";
35
35
  import { currentPlan, installNetwork, redactProxy, redactText, normalizeProxy, proxyToRemember, describeNetError, describeRelayFailure, checkRelayCertificate, interceptedText, interceptionExplanation, relayJson } from "./network.js";
36
36
  import { createInterface } from "node:readline";
37
37
  import { homedir } from "node:os";
@@ -39,6 +39,7 @@ import { removeHook, decideStop, writeIntent, clearIntent, readIntent, repairNot
39
39
  import { pending, render as renderPending } from "./pending.js";
40
40
  import { signedRead } from "./reads.js";
41
41
  import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel, bindableFolder, startReconnects } from "./quickstart.js";
42
+ import { recordedAgent } from "./agents.js";
42
43
  import { alreadyMemberLine, askedLine, configureLines, fetchSeats, inUseLine, offerSeats, seatsForTeam, seatsToMove, startGate, RECOVER_WAITING_EXIT } from "./recover-offer.js";
43
44
  import { shellArg, teamNameProblem } from "./team-name.js";
44
45
  import { folderRefusalHere, isTemporaryRefusal, projectFolderAnswer } from "./folder-guard.js";
@@ -253,7 +254,7 @@ async function interactiveOnboard(relayArg, opts = {}) {
253
254
  const authUser = mode === "signup" ? signupUsername : undefined;
254
255
  save({ ...(prior ?? {}), relay, member: signupUsername || prior?.member || "me",
255
256
  space: prior?.space ?? "", token: prior?.token ?? "",
256
- publicKey: kp.publicKey, privateKey: kp.privateKey, agent: prior?.agent ?? "unknown",
257
+ publicKey: kp.publicKey, privateKey: kp.privateKey, agent: recordedAgent(prior?.agent),
257
258
  pendingAuth: { email, ...(authUser ? { username: authUser } : {}), purpose: mode, at: recentPend?.at ?? (already && pend?.email === email && pend.at ? pend.at : Date.now()) } });
258
259
  if (started.j.reused) {
259
260
  console.error(`\n ${C("A code was sent")} to ${B(email)} ${Math.max(1, Math.round((Date.now() - recentPend.at) / 60_000))} min ago — enter that one, or type 'resend' for a new one. ${SPAM_HINT}\n`);
@@ -285,7 +286,7 @@ async function interactiveOnboard(relayArg, opts = {}) {
285
286
  if (v.status === 200) {
286
287
  const acct = v.j.account;
287
288
  const cfg = { ...(prior ?? {}), relay, member: acct.username, space: prior?.space ?? "", token: prior?.token ?? "",
288
- publicKey: kp.publicKey, privateKey: kp.privateKey, agent: prior?.agent ?? "unknown",
289
+ publicKey: kp.publicKey, privateKey: kp.privateKey, agent: recordedAgent(prior?.agent),
289
290
  account: { username: acct.username, email: acct.email, verified_at: Date.now() } };
290
291
  delete cfg.pendingAuth;
291
292
  save(cfg);
@@ -867,11 +868,12 @@ async function guidedSetup(cfg, project, opts = {}) {
867
868
  catch {
868
869
  return [];
869
870
  } },
870
- publish: async (p) => {
871
+ publish: async (p, keep) => {
871
872
  const c = new PantheonClient(cfg, undefined, { role: "cli" });
872
873
  try {
873
874
  await c.connect();
874
- await c.publishProfile(organizationProfile(p, cfg.spaceId, cfg.space));
875
+ await c.publishProfile(organizationProfile(p, cfg.spaceId, cfg.space), keep);
876
+ return [...c.profiles.values()];
875
877
  }
876
878
  finally {
877
879
  c.close();
@@ -1129,6 +1131,7 @@ ${B("several teams on one machine")}
1129
1131
  pantheon leave <team> drop one team from this machine (its token and repo bindings)
1130
1132
  pantheon space delete <team> --yes dissolve a team you founded: disconnects everyone, frees a plan slot
1131
1133
  pantheon profile set --space <team> … authority that applies in one team only (e.g. --human-required pricing)
1134
+ pantheon profile clear-authority <member> [--space <team>] founder: take a member's decision claims off (they can set them again)
1132
1135
 
1133
1136
  ${B("day to day")}
1134
1137
  pantheon status who is here, what is being worked on, what needs you
@@ -1652,7 +1655,7 @@ async function main() {
1652
1655
  publicKey: kp.publicKey,
1653
1656
  privateKey: kp.privateKey,
1654
1657
  display: flag("display"),
1655
- agent: flag("agent", "unknown"),
1658
+ agent: flag("agent") ?? recordedAgent(had?.agent, { fresh: true }),
1656
1659
  ...(had?.account ? { account: had.account } : {}),
1657
1660
  };
1658
1661
  save(cfg);
@@ -1853,6 +1856,21 @@ async function main() {
1853
1856
  }
1854
1857
  case "rewire": {
1855
1858
  const rows = installForUser(undefined, undefined, { direct: argv.includes("--direct") }).map((t) => ({ tool: t.tool, status: t.status }));
1859
+ try {
1860
+ const said = withConfigLock(() => {
1861
+ const f = loadAll();
1862
+ if (!f || (f.agent && f.agent !== "unknown"))
1863
+ return null;
1864
+ const a = recordedAgent(f.agent, { fresh: true });
1865
+ if (a === "unknown")
1866
+ return null;
1867
+ saveAll({ ...f, agent: a });
1868
+ return a;
1869
+ });
1870
+ if (said)
1871
+ rows.push({ tool: "agent", status: `recorded as ${said} (was unknown)` });
1872
+ }
1873
+ catch { }
1856
1874
  try {
1857
1875
  const { refreshUserHooks } = await import("./notify.js");
1858
1876
  const h = refreshUserHooks();
@@ -2300,6 +2318,8 @@ async function main() {
2300
2318
  const domains = flag("domains");
2301
2319
  if (domains)
2302
2320
  meta.decision_domains = cleanList(domains.split(","));
2321
+ if (act === "HUMAN_REQUIRED" && to && !inWokenRun())
2322
+ meta.named_by_requester = true;
2303
2323
  let goalArg = flag("goal") ?? null;
2304
2324
  let ref = flag("ref") ?? null;
2305
2325
  if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
@@ -2346,6 +2366,11 @@ async function main() {
2346
2366
  : /GOAL_REQUIRED|NO_SUCH_GOAL|GOAL_CLOSED/.test(e.message) ? `${e.message.replace(/ — open one with pantheon_open_goal/, "").replace(/pantheon_open_goal/g, "pantheon goal open")}\n Open one from here: pantheon goal open "<what it is about>" --with <member> then add --goal <id> (pantheon goal list shows the open ones)`
2347
2367
  : e.message));
2348
2368
  console.log(`sent ${act} id=${r.id} seq=${r.seq}`);
2369
+ {
2370
+ const note = (await import("./secret-guard.js")).privateLinkNote(body, summary);
2371
+ if (note)
2372
+ console.error(`\x1b[33mnote\x1b[0m: ${note}`);
2373
+ }
2349
2374
  if (flag("space"))
2350
2375
  console.error(D(` in "${cfg.space}"`));
2351
2376
  else if (resolveFor(process.cwd()).reason === "default")
@@ -3282,7 +3307,7 @@ async function main() {
3282
3307
  if (r.status !== 200)
3283
3308
  die(String(r.j.error ?? `signup failed (${r.status})`));
3284
3309
  save({ ...(prior ?? {}), relay, member: username ?? prior?.member ?? "me", space: prior?.space ?? "", token: prior?.token ?? "",
3285
- publicKey: kp.publicKey, privateKey: kp.privateKey, agent: prior?.agent ?? "unknown",
3310
+ publicKey: kp.publicKey, privateKey: kp.privateKey, agent: recordedAgent(prior?.agent),
3286
3311
  pendingAuth: { email, ...(username ? { username } : {}), purpose, at: Date.now() } });
3287
3312
  if (r.j.sent === false && r.j.via === "resend") {
3288
3313
  console.error(`${B("signup accepted, but the email could not be sent just now.")}`);
@@ -3476,14 +3501,43 @@ async function main() {
3476
3501
  return;
3477
3502
  }
3478
3503
  case "profile": {
3504
+ if (argv[1] === "clear-authority") {
3505
+ if (inWokenRun())
3506
+ die("a woken run cannot clear anyone's decision claims — a person has to run this here");
3507
+ const who = argv[2] && !argv[2].startsWith("--") ? argv[2] : die("usage: pantheon profile clear-authority <member> [--space <team>]");
3508
+ let tc;
3509
+ if (flag("space")) {
3510
+ const pf = loadAll() ?? die("not configured — pantheon join <invite> first");
3511
+ const m = findMembership(pf, flag("space")) ?? die(`you are not in a team called "${flag("space")}"`);
3512
+ tc = configFor(pf, m, null);
3513
+ }
3514
+ else
3515
+ tc = need();
3516
+ const client = new PantheonClient(tc, undefined, { role: "cli" });
3517
+ await client.connect().catch((e) => die(connectFailure(e, tc.relay)));
3518
+ const claimed = Object.keys(client.profiles.get(who)?.authority ?? {});
3519
+ try {
3520
+ await client.clearAuthority(who);
3521
+ }
3522
+ catch (e) {
3523
+ client.close();
3524
+ const msg = e.message;
3525
+ die(/BAD_MESSAGE/.test(msg) && /discriminator|\bt\b/.test(msg)
3526
+ ? `"${tc.space}"'s relay runs a Pantheon older than 0.33.5, which cannot clear someone's claims — nothing changed. Ask ${who} to run pantheon setup --decides none, or upgrade the relay.`
3527
+ : msg);
3528
+ }
3529
+ client.close();
3530
+ console.error(`${C("cleared")} — ${who}'s decision claims in "${tc.space}"${claimed.length ? ` (${claimed.join(", ")})` : ""}. The team was told in a NOTE; ${who} can set them again with pantheon setup --decides`);
3531
+ return;
3532
+ }
3479
3533
  const cfg = need();
3480
3534
  const sub = argv[1] ?? "show";
3481
3535
  let profile = loadProfile(cfg);
3482
- const publish = async () => {
3536
+ const publish = async (keep = []) => {
3483
3537
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
3484
3538
  try {
3485
3539
  await client.connect();
3486
- await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space));
3540
+ await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space), keep);
3487
3541
  }
3488
3542
  finally {
3489
3543
  client.close();
@@ -3515,6 +3569,11 @@ async function main() {
3515
3569
  const name = flag("name");
3516
3570
  const title = flag("title");
3517
3571
  const responsibilities = listFlag("responsibilities");
3572
+ for (const f of ["human-required", "agent-can-decide"]) {
3573
+ const bad = decisionAreasProblem(f, flag(f));
3574
+ if (bad)
3575
+ die(bad);
3576
+ }
3518
3577
  const humanRequired = listFlag("human-required");
3519
3578
  const agentCanDecide = listFlag("agent-can-decide");
3520
3579
  const capabilities = listFlag("capabilities");
@@ -3546,7 +3605,7 @@ async function main() {
3546
3605
  const tcl = new PantheonClient(tc, undefined, { role: "cli" });
3547
3606
  try {
3548
3607
  await tcl.connect();
3549
- await tcl.publishProfile(organizationProfile(profile, m.id, m.name));
3608
+ await tcl.publishProfile(organizationProfile(profile, m.id, m.name), [...(humanRequired ?? []), ...(agentCanDecide ?? [])]);
3550
3609
  console.error(`${C("published")} into "${m.name}"`);
3551
3610
  }
3552
3611
  catch (e) {
@@ -3579,7 +3638,7 @@ async function main() {
3579
3638
  console.error(`${C("saved")} — ${surface ? `decision notifications open ${surface}` : "local organization profile updated"}`);
3580
3639
  if (publicChange) {
3581
3640
  try {
3582
- await publish();
3641
+ await publish([...(humanRequired ?? []), ...(agentCanDecide ?? [])]);
3583
3642
  console.error(`${C("published")} — the organization can now route decisions using this authority`);
3584
3643
  }
3585
3644
  catch (e) {
@@ -3645,7 +3704,7 @@ async function main() {
3645
3704
  console.error(`${C("published")} — organization profile is current`);
3646
3705
  }
3647
3706
  else if (sub !== "show") {
3648
- die("usage: pantheon profile [show | init ... | publish | set ... | delegate ... | undelegate ...]");
3707
+ die("usage: pantheon profile [show | init ... | publish | set ... | delegate ... | undelegate ... | clear-authority <member>]");
3649
3708
  }
3650
3709
  console.log(JSON.stringify(profile, null, 2));
3651
3710
  return;
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import WebSocket from "ws";
2
- import { adoptSpaceId, rememberTeamRepos, updateMembershipToken } from "./config.js";
2
+ import { adoptSpaceId, loadAll, rememberTeamRepos, updateMembershipToken } from "./config.js";
3
+ import { honourClearedAuthority } from "./profile.js";
3
4
  import { markContinued } from "./continued.js";
4
5
  import { markAnswered, nonceOf } from "./nonces.js";
5
6
  import { signedRead } from "./reads.js";
@@ -248,9 +249,11 @@ export class PantheonClient {
248
249
  case "members":
249
250
  this.members = msg.members;
250
251
  this.observePins();
252
+ this.honourCleared();
251
253
  break;
252
254
  case "profiles":
253
255
  this.profiles = new Map((msg.profiles ?? []).map((p) => [p.member, p]));
256
+ this.honourCleared();
254
257
  break;
255
258
  case "claimed":
256
259
  break;
@@ -325,6 +328,7 @@ export class PantheonClient {
325
328
  this.features = msg.features ?? [];
326
329
  this.observePins();
327
330
  this.profiles = new Map((msg.profiles ?? []).map((p) => [p.member, p]));
331
+ this.honourCleared();
328
332
  for (const g of msg.goals)
329
333
  this.goals.set(g.id, g);
330
334
  if (!settled) {
@@ -527,6 +531,30 @@ export class PantheonClient {
527
531
  });
528
532
  }
529
533
  async send(a) {
534
+ let inferredTo = false;
535
+ if ((a.act === "ANSWER" || a.act === "ACCEPT" || a.act === "OBJECT") && a.ref && a.to == null) {
536
+ const author = await this.authorOf(a.ref);
537
+ const goal = a.goal ? this.goals.get(a.goal) : undefined;
538
+ const entry = author ? this.members.find((m) => m.member === author) : undefined;
539
+ const known = !!entry && entry.admitted && !entry.revoked_at;
540
+ if (author && author !== this.cfg.member && known && (!a.goal || goal?.members.includes(author))) {
541
+ a = { ...a, to: [author] };
542
+ inferredTo = true;
543
+ }
544
+ }
545
+ if (inferredTo) {
546
+ try {
547
+ return await this.sendAct(a);
548
+ }
549
+ catch (e) {
550
+ if (!/NO_SUCH_MEMBER|NOT_IN_GOAL/.test(e.message))
551
+ throw e;
552
+ return this.sendAct({ ...a, to: null });
553
+ }
554
+ }
555
+ return this.sendAct(a);
556
+ }
557
+ async sendAct(a) {
530
558
  const repo = this.cfg.repo;
531
559
  const meta = a.act === "DELIVERED" && repo && !(a.meta && "repo" in a.meta) ? { ...(a.meta ?? {}), repo } : a.meta;
532
560
  const id = newActId();
@@ -756,9 +784,63 @@ export class PantheonClient {
756
784
  setMayWake(member, mayWake) {
757
785
  return this.rpc({ t: "admit", member, may_wake: mayWake, wake_only: true });
758
786
  }
759
- publishProfile(profile) {
787
+ publishProfile(profile, keep = []) {
788
+ const own = this.profiles.get(this.cfg.member);
789
+ if (own?.authority_cleared) {
790
+ try {
791
+ honourClearedAuthority(this.cfg, own, { keep: [...keep], force: true, teams: this.teamCount(), founder: this.founder() });
792
+ }
793
+ catch { }
794
+ const gone = new Set(own.authority_cleared.by === this.founder() ? own.authority_cleared.domains.filter((d) => !keep.includes(d)) : []);
795
+ profile = { ...profile, authority: Object.fromEntries(Object.entries(profile.authority).filter(([d]) => !gone.has(d))) };
796
+ }
760
797
  return this.rpc({ t: "publish_profile", profile });
761
798
  }
799
+ async authorOf(id) {
800
+ const seen = this.seen.get(id);
801
+ if (seen)
802
+ return seen.from;
803
+ const read = (async () => {
804
+ try {
805
+ const res = await signedRead(this.cfg, "/act", { id }, { timeoutMs: 300 });
806
+ return res.ok ? (await res.json()).act?.from ?? null : null;
807
+ }
808
+ catch {
809
+ return null;
810
+ }
811
+ })();
812
+ let timer;
813
+ const late = new Promise((r) => { timer = setTimeout(() => r(null), 300); });
814
+ try {
815
+ return await Promise.race([read, late]);
816
+ }
817
+ finally {
818
+ clearTimeout(timer);
819
+ }
820
+ }
821
+ clearAuthority(member) {
822
+ return this.rpc({ t: "clear_authority", member });
823
+ }
824
+ founder() {
825
+ return this.members.find((m) => m.founder)?.member ?? null;
826
+ }
827
+ teamCount() {
828
+ try {
829
+ return Object.keys(loadAll()?.spaces ?? {}).length;
830
+ }
831
+ catch {
832
+ return 1;
833
+ }
834
+ }
835
+ honourCleared() {
836
+ const own = this.profiles.get(this.cfg.member);
837
+ if (!own?.authority_cleared)
838
+ return;
839
+ try {
840
+ honourClearedAuthority(this.cfg, own, { teams: this.teamCount(), founder: this.founder() });
841
+ }
842
+ catch { }
843
+ }
762
844
  adoptKnownRepo() {
763
845
  const c = this.cfg;
764
846
  if (c.repo || !c.cwdRepo)
@@ -1,4 +1,14 @@
1
1
  const oneLine = (value) => value.replace(/\s+/g, " ").trim();
2
+ export function namedMarker(from, meta, passed) {
3
+ if (meta?.authority !== "named")
4
+ return null;
5
+ const p = (x, n) => String(x).replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029\[\]]+/g, " ").slice(0, n);
6
+ const over = (passed ?? []).slice(0, 4).map((o) => `${p(o.member, 48)} (${o.domains.slice(0, 4).map((d) => p(d, 48)).join(", ")})`);
7
+ return `named by ${p(from, 48)}${over.length ? ` — passes over ${over.join(", ")}` : ""}`;
8
+ }
9
+ export function namedMarkerIn(text) {
10
+ return /\[(named by [^\]\n]{1,400})\]/.exec(text ?? "")?.[1] ?? null;
11
+ }
2
12
  const LAYERS = [
3
13
  /^([a-z0-9._-]+)\s+escalated\s*:\s*/i,
4
14
  /^([a-z0-9._-]+)\s+needs? a decision\s*:\s*/i,
package/dist/doctor.js CHANGED
@@ -226,8 +226,18 @@ export async function doctor(projectDir) {
226
226
  add("pass", "team", folderHere ? `this folder is bound to "${here.cfg.space}" (${here.binding?.path ?? projectDir}) — no git needed` : `this repo is bound to "${here.cfg.space}"${here.repo?.origin ? ` (${here.repo.origin})` : ""}`, undefined, `this ${folderHere ? "folder" : "repo"} is bound to a team (${nTeams} on this machine)`);
227
227
  else if (here.reason === "single")
228
228
  add("pass", "team", `"${cfg.space}" — your only team, so every folder resolves to it`, undefined, "one team on this machine");
229
- else if (here.reason === "unbound")
230
- add("fail", "team", `this folder is not bound to any of your ${nTeams} teams — agents opened here will refuse to send`, `from the team's checkout: pantheon use <team> --here (pantheon spaces lists them)`, `folder not bound to any of ${nTeams} teams`);
229
+ else if (here.reason === "unbound") {
230
+ const all = loadAll();
231
+ const names = new Map(Object.values(all?.spaces ?? {}).map((m) => [m.id, m.name]));
232
+ const bound = Object.values(all?.bindings ?? {}).filter((b) => names.has(b.space));
233
+ if (bound.length) {
234
+ const list = bound.slice(0, 4).map((b) => `${b.path} (${names.get(b.space)})`).join(", ") + (bound.length > 4 ? `, and ${bound.length - 4} more` : "");
235
+ add("info", "team", `run doctor from a project folder to check it; bound here: ${list}`, undefined, `run outside a project; ${bound.length} folder${bound.length === 1 ? "" : "s"} bound on this machine`);
236
+ }
237
+ else {
238
+ add("fail", "team", `this folder is not bound to any of your ${nTeams} teams — agents opened here will refuse to send`, `from the team's checkout: pantheon use <team> --here (pantheon spaces lists them)`, `folder not bound to any of ${nTeams} teams`);
239
+ }
240
+ }
231
241
  else if (here.reason === "stale")
232
242
  add("fail", "team", `this ${here.repo ? "repo" : "folder"} is bound to a team this machine has left (${here.binding?.space})`, "pantheon use <team> --here, or pantheon join <invite>", "bound to a team this machine has left");
233
243
  if (here.repo && !here.repo.shared)