@join-pantheon/cli 0.21.3 → 0.22.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/README.md CHANGED
@@ -45,21 +45,27 @@ out with `pantheon sessions`; every identity and membership event is in `pantheo
45
45
  audit`. (Self-hosting or on a LAN/tailnet? Accounts are optional — skip straight
46
46
  to `pantheon start`.)
47
47
 
48
- **Then, from your project's repo:**
48
+ **Then, from your project's repo — or anywhere, if there is no repo yet (see below):**
49
49
 
50
50
  ```bash
51
51
  pantheon start
52
52
  ```
53
53
 
54
- **Your teammate, from their clone of the project:**
54
+ **Your teammate, from their clone of the project if they have one:**
55
55
 
56
56
  ```bash
57
57
  pantheon join pantheons://relay.joinpantheon.network/acme#C1qhJfdHBaPjuG3jF48vcqrP
58
58
  ```
59
59
 
60
60
  Signed in, `pantheon start` puts your space on the hosted relay, so that link works from any
61
- network — no tunnel, no VPN. The space is named after your repo; if another team already took
62
- that name, start picks `<repo>-<you>` and says so (or choose your own with `--space`).
61
+ network — no tunnel, no VPN. The space is named after the folder you run it from (your repo,
62
+ usually); if another team already took that name, start picks `<name>-<you>` and says so. Choose
63
+ your own with `--space` — always do that when there is no repo to name it after.
64
+
65
+ **You do not need a git repository.** Messages, questions, decisions and escalations work from any
66
+ folder. A repo adds exactly two things: handing over code (a `DELIVERED` names a pushed branch and
67
+ commit) and letting a teammate wake this machine while you are away; `pantheon repo add` binds one
68
+ whenever you have it.
63
69
 
64
70
  That's it — one command each sets up the **whole machine**: a relay and space (you) or membership
65
71
  (them), pantheon wired into whichever agents each of you already has (**user scope** — every project,
@@ -67,13 +73,17 @@ every Conductor window, nothing to commit), the wake-up hook so an agent finishe
67
73
  waiting for, the **worker** so the machine answers the team even while its human is away (on by
68
74
  default when run inside a git repo; `--no-worker` opts out — and a peer still needs an explicit
69
75
  `--may-wake` grant before they can use it), native decision alerts, and the menu bar. Each command
70
- ends with a "You're set" summary, then a short guided setup on a terminal: whether teammates may wake
71
- your machine while you are away, what a woken run may execute, who you are and which decisions are
72
- yours alone (so agents stop and ask you), and which alerts you want. Plain questions, mostly options;
73
- `pantheon setup` re-runs it any time — and an agent setting you up can run `pantheon setup --questions`, ask you in chat, and pass your answers back as flags, so nobody has to open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
76
+ ends with a "You're set" summary, then a short guided setup on a terminal: three questions — your
77
+ name, your role, and (inside a repo) whether a run a teammate wakes may execute commands — and one
78
+ screen of defaults to keep or change: who may wake your machine while you are away, which alerts you
79
+ want, which decisions are yours alone (so agents stop and ask you), the worker's playbook and its base
80
+ branch. `pantheon setup` re-runs it any time. An agent setting you up runs `pantheon setup --questions`,
81
+ asks you in chat, and passes your answers back as flags — to `pantheon setup --yes`, or straight to
82
+ `pantheon start` / `pantheon join`, which apply them the moment the team exists — so nobody has to
83
+ open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
74
84
 
75
85
  Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
76
- `pantheon install --user` (from your project's repo).
86
+ `pantheon install --user` (from your project's repo if you have one).
77
87
 
78
88
  **Consent happens once, here.** Setup pre-approves exactly the six pantheon *message* tools in
79
89
  each agent's own permission system (Claude Code `permissions.allow`, Cursor `Mcp()` tokens,
@@ -451,6 +461,7 @@ pantheon doctor preflight: node, keys, MCP entry, relay, memb
451
461
  pantheon worker / service stay answerable while you are away (install as a service)
452
462
  pantheon persona list / init / show — what a woken worker may do
453
463
  pantheon spaces / use / repo several teams on one machine — what is bound where; bind this repo; a team's repos
464
+ pantheon history / show read the thread, or one act, in full with ids — from a terminal, no agent needed
454
465
  pantheon sessions · logout your devices and sessions; sign this machine out
455
466
  pantheon account delete remove your account and everything the relay holds on you
456
467
  pantheon upgrade pull the latest release and re-wire your agents
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ process.emitWarning = ((warning, ...rest) => {
8
8
  });
9
9
  import { randomBytes } from "node:crypto";
10
10
  import { readFileSync } from "node:fs";
11
- import { dirname, join, resolve } from "node:path";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
12
  import { PantheonClient } from "./client.js";
13
13
  import { fromInvite, load, save, toInvite, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
14
14
  import { fmtLimit } from "./plans.js";
@@ -20,6 +20,7 @@ import { Worker } from "./worker.js";
20
20
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
21
21
  import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
22
22
  import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
23
+ import { selectActs, formatAct, resolveRef } from "./history-view.js";
23
24
  import { doctor, render } from "./doctor.js";
24
25
  import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
25
26
  import { assess } from "./untrusted.js";
@@ -321,6 +322,13 @@ async function guidedSetup(cfg, project, opts = {}) {
321
322
  rl?.close();
322
323
  }
323
324
  }
325
+ async function fetchHistory(cfg, since = 0) {
326
+ const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
327
+ const res = await fetch(`${base}/history?space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}&since=${since}`, { signal: AbortSignal.timeout(8000) });
328
+ if (!res.ok)
329
+ throw new Error(`relay answered ${res.status} for the ledger`);
330
+ return (await res.json()).acts ?? [];
331
+ }
324
332
  function workerMidRun() {
325
333
  try {
326
334
  const s = JSON.parse(readFileSync(join(CONFIG_DIR, "worker-state.json"), "utf8"));
@@ -365,11 +373,16 @@ ${B("first time — verify your email once")}
365
373
  pantheon login --email you@work.com a second machine, same account
366
374
  pantheon whoami · pantheon sessions who you are; your devices & sessions
367
375
 
368
- ${B("getting going — then two commands, run from your project's repo")}
376
+ ${B("getting going — two commands, from your project's repo if you have one")}
369
377
  pantheon start you: sets up this whole machine + one line to send your teammate
370
378
  pantheon join <line> them: that one line sets up their whole machine
371
379
  pantheon invite another single-use line, for a third person or a second machine
372
380
 
381
+ Both take the setup answers as flags (--name, --title, --wake, --commands, … — pantheon setup
382
+ --questions lists them), so an agent sets a person up in one command. No repo? Messages, questions
383
+ and decisions work anywhere; a repo only adds handing over code and being woken. Name the team
384
+ with --space when there is no repo to name it after.
385
+
373
386
  One command does everything: agents wired and pre-approved, wake-up hook (your
374
387
  agent finishes what a peer is waiting for), worker (this machine answers the
375
388
  team while you are away — on when run from a git repo; a peer still needs an
@@ -416,9 +429,11 @@ ${B("control")}
416
429
  pantheon leave remove what pantheon put on this machine
417
430
  pantheon panic cut off all unattended work on this machine, now
418
431
  pantheon doctor check this machine end to end
419
- pantheon setup the guided choices: who may wake this machine, what a run may execute,
420
- what you personally decide, which alerts you want — re-run any time
421
- (--questions prints them for an agent to ask you; answers go back as flags)
432
+ pantheon setup three questions — your name, your role, may a woken run execute commands —
433
+ then one screen of defaults to keep or change: who may wake this machine,
434
+ alerts, what you personally decide, playbook, base branch. Re-run any time.
435
+ (--questions prints them for an agent to ask you; answers go back as flags,
436
+ to setup --yes or straight to start / join)
422
437
  pantheon upgrade pull the latest pantheon, rebuild, and re-wire your agents
423
438
 
424
439
  ${D("more: pantheon help --all")}
@@ -432,6 +447,9 @@ ${B("less common")}
432
447
  pantheon whoami your name and fingerprint
433
448
  pantheon pending what a live agent window has not yet seen
434
449
  pantheon say <ACT> <text> send one act by hand (NOTE, ASK, ANSWER, PROPOSE, ...)
450
+ replies pick the act they answer; --ref <id | seq | last> to choose
451
+ pantheon history the thread in full, with ids (--goal <id>, --from <who>, --last N, --short)
452
+ pantheon show <id | seq> one act in full, and what it replied to
435
453
  pantheon invite mint another single-use invite link to send
436
454
  pantheon install re-wire this machine's agents (after a Node/agent change)
437
455
  pantheon mcp the server your agent runs; not for you
@@ -457,9 +475,14 @@ async function main() {
457
475
  switch (cmd) {
458
476
  case "start": {
459
477
  const port = Number(flag("port", "8787"));
478
+ const setupFlags = parseSetupFlags(argv);
479
+ if (setupFlags.errors.length)
480
+ die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
460
481
  const wp = unattendedProject();
461
482
  requireSignInOnHostedRelay(flag("relay"));
462
483
  console.error(`${B("pantheon start")}\n`);
484
+ if (!flag("space") && !repoInfo(process.cwd()))
485
+ console.error(D(` No git repo here, so the team will be named after this folder: "${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9._-]/g, "-") || "team"}". Pass --space <name> to choose.\n`));
463
486
  const r = await qsStart({
464
487
  port,
465
488
  dataDir: resolveDataDir(flag("data")).dir,
@@ -489,7 +512,7 @@ async function main() {
489
512
  if (r.invite) {
490
513
  console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
491
514
  console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
492
- console.error(D(` Tell them to run it from their project's git repo. Single use, expires in ${Math.round(Number(flag("ttl", "1440")) / 60)}h.`));
515
+ console.error(D(` From their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(Number(flag("ttl", "1440")) / 60)}h.`));
493
516
  console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
494
517
  console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
495
518
  console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${r.invite}`)}`);
@@ -500,7 +523,7 @@ async function main() {
500
523
  }
501
524
  printReady(r.tools);
502
525
  if (r.claimed)
503
- await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null);
526
+ await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
504
527
  if (r.relay) {
505
528
  console.error(D(`\nKeeping the relay running here. Ctrl-C stops it (your teammate goes offline too).`));
506
529
  await new Promise(() => { });
@@ -593,7 +616,10 @@ async function main() {
593
616
  return;
594
617
  }
595
618
  case "join": {
596
- const invite = argv[1] ?? die("usage: pantheon join pantheon://host/space#token");
619
+ const invite = argv[1] ?? die("usage: pantheon join pantheon://host/space#token [setup answers as flags — pantheon setup --questions]");
620
+ const setupFlags = parseSetupFlags(argv);
621
+ if (setupFlags.errors.length)
622
+ die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
597
623
  let parsed;
598
624
  try {
599
625
  parsed = fromInvite(invite);
@@ -637,7 +663,7 @@ async function main() {
637
663
  {
638
664
  const joined = load();
639
665
  if (joined)
640
- await guidedSetup(joined, repoInfo(process.cwd())?.root ?? null);
666
+ await guidedSetup(joined, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
641
667
  }
642
668
  console.error(`\n${D('Then ask your agent: "check pantheon_status and say hello to the team".')}`);
643
669
  return;
@@ -822,7 +848,7 @@ async function main() {
822
848
  for (const o of human) {
823
849
  const who = o.owner.replace(/^human:/, "");
824
850
  const stale = Date.now() - o.created_at > 3_600_000;
825
- console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body}`);
851
+ console.log(` ${stale ? "\x1b[33m" : C("")}${who.padEnd(14)}${ageOf(o.created_at).padEnd(6)}\x1b[0m ${o.body.split("\n")[0].slice(0, 110)} ${D(o.ref)}`);
826
852
  if (who === cfg.member)
827
853
  console.log(` ${D("your peer was told this decision was coming — answer it or say it is dropped")}`);
828
854
  }
@@ -830,7 +856,10 @@ async function main() {
830
856
  if (!between.length)
831
857
  console.log(` ${D("(nothing owed)")}`);
832
858
  for (const o of between) {
833
- console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body}`);
859
+ console.log(` ${o.kind.padEnd(7)} ${o.owner.padEnd(14)}${ageOf(o.created_at).padEnd(6)} ${o.body.split("\n")[0].slice(0, 110)} ${D(o.ref)}`);
860
+ }
861
+ if (human.length || between.length || goals.some((g) => g.state !== "CLOSED")) {
862
+ console.log(D(`\n read in full: pantheon show <id> · a thread: pantheon history --goal <goal> · reply: pantheon say ANSWER "…" --ref <id>`));
834
863
  }
835
864
  client.close();
836
865
  };
@@ -931,13 +960,58 @@ async function main() {
931
960
  console.log(argv.includes("--json") ? JSON.stringify(report) : renderPending(report));
932
961
  return;
933
962
  }
963
+ case "history": {
964
+ const cfg = need();
965
+ const acts = await fetchHistory(cfg, numFlag("since", 0)).catch((e) => die(`cannot read the ledger — ${e.message}`));
966
+ const picked = selectActs(acts, {
967
+ ...(flag("goal") ? { goal: flag("goal") } : {}), ...(flag("from") ? { from: flag("from") } : {}),
968
+ last: numFlag("last", 20), presence: argv.includes("--presence"),
969
+ });
970
+ if (argv.includes("--json")) {
971
+ console.log(JSON.stringify(picked, null, 2));
972
+ return;
973
+ }
974
+ if (!picked.length) {
975
+ console.log(D("no acts match"));
976
+ return;
977
+ }
978
+ const client = new PantheonClient(cfg, undefined, { role: "cli" });
979
+ const titles = new Map();
980
+ try {
981
+ await client.connect();
982
+ for (const g of client.goals.values())
983
+ titles.set(g.id, g.title);
984
+ }
985
+ catch { }
986
+ finally {
987
+ client.close();
988
+ }
989
+ for (const a of picked)
990
+ console.log(formatAct(a, { short: argv.includes("--short"), ...(a.goal && titles.get(a.goal) ? { goalTitle: titles.get(a.goal) } : {}) }) + "\n");
991
+ console.log(D(`${picked.length} of ${acts.filter((a) => a.act !== "JOIN" && a.act !== "LEAVE").length} acts · --last N · --goal <id> · --from <member> · --short · --json · reply: pantheon say ANSWER "…" --ref <id or seq>`));
992
+ return;
993
+ }
994
+ case "show": {
995
+ const cfg = need();
996
+ const key = argv[1] ?? die("usage: pantheon show <act id | sequence number>");
997
+ const acts = await fetchHistory(cfg).catch((e) => die(`cannot read the ledger — ${e.message}`));
998
+ const a = acts.find((x) => x.id === key || String(x.seq) === key) ?? die(`no act ${key} in what the relay still holds (pantheon history lists them)`);
999
+ console.log(formatAct(a));
1000
+ if (a.ref) {
1001
+ const r = acts.find((x) => x.id === a.ref);
1002
+ if (r)
1003
+ console.log(D(`\nin reply to:\n`) + formatAct(r, { short: true }));
1004
+ }
1005
+ return;
1006
+ }
934
1007
  case "say": {
935
1008
  const cfg = need();
936
- const act = argv[1]?.toUpperCase() ?? die("usage: pantheon say <ACT> <body> [--goal id] [--to a,b] [--domains technical,gtm]");
1009
+ const act = argv[1]?.toUpperCase() ?? die("usage: pantheon say <ACT> <body> [--goal id] [--to a,b] [--ref <id | seq | last>] [--summary 'one line'] [--domains technical,gtm]");
937
1010
  const stop = argv.findIndex((a, i) => i >= 2 && a.startsWith("--"));
938
1011
  const body = argv.slice(2, stop === -1 ? undefined : stop).join(" ");
939
1012
  if (!body)
940
1013
  die("nothing to say — pantheon say NOTE 'your message here'");
1014
+ const summary = flag("summary");
941
1015
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
942
1016
  await client.connect().catch((e) => die(`cannot reach relay — ${e.message}`));
943
1017
  const to = flag("to");
@@ -950,13 +1024,28 @@ async function main() {
950
1024
  const domains = flag("domains");
951
1025
  if (domains)
952
1026
  meta.decision_domains = cleanList(domains.split(","));
1027
+ let goalArg = flag("goal") ?? null;
1028
+ let ref = flag("ref") ?? null;
1029
+ if (ref !== null || ["ANSWER", "ACCEPT", "OBJECT"].includes(act)) {
1030
+ const acts = await fetchHistory(cfg).catch(() => []);
1031
+ const rr = resolveRef(acts, ref, act, goalArg, cfg.member);
1032
+ if (rr.error) {
1033
+ client.close();
1034
+ die(rr.error);
1035
+ }
1036
+ if (rr.note)
1037
+ console.error(D(` ${rr.note}`));
1038
+ ref = rr.id;
1039
+ goalArg = goalArg ?? rr.goal;
1040
+ }
953
1041
  const r = await client.send({
954
1042
  act: act, body,
955
- goal: flag("goal") ?? null,
1043
+ goal: goalArg,
956
1044
  to: to ? to.split(",").map((s) => s.trim()) : null,
957
- ref: flag("ref") ?? null,
1045
+ ref,
1046
+ ...(summary ? { summary } : {}),
958
1047
  ...(Object.keys(meta).length ? { meta } : {}),
959
- }).catch((e) => die(e.message));
1048
+ }).catch((e) => die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'` : e.message));
960
1049
  console.log(`sent ${act} id=${r.id} seq=${r.seq}`);
961
1050
  client.close();
962
1051
  return;
@@ -2013,10 +2102,13 @@ async function main() {
2013
2102
  const find = (want) => all.find((x) => x.o.id === want);
2014
2103
  if (sub === "answer") {
2015
2104
  if (!id)
2016
- die("usage: pantheon approvals answer <obligation-id> <decision>");
2105
+ die("usage: pantheon approvals answer <obligation-id> <decision> [--summary 'one line']");
2017
2106
  const hit = find(id) ?? die(`no decision ${id} is waiting on you${conns.length > 1 ? ` in any of your ${conns.length} teams` : ""}`);
2018
- const answer = argv.slice(3).join(" ").trim() || die("state the decision; it will be written to the audit ledger");
2019
- const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq });
2107
+ const stopAt = argv.findIndex((a, i) => i >= 3 && a.startsWith("--"));
2108
+ const answer = argv.slice(3, stopAt === -1 ? undefined : stopAt).join(" ").trim() || die("state the decision; it will be written to the audit ledger");
2109
+ const summary = flag("summary");
2110
+ const sent = await hit.client.send({ act: "ANSWER", body: answer, goal: null, ref: hit.o.ref, after_seq: hit.client.readSeq, ...(summary ? { summary } : {}) })
2111
+ .catch((e) => { closeAll(); return die(/SUMMARY_REQUIRED/.test(e.message) ? `${e.message}\n Add one: --summary 'the point they must not miss'` : e.message); });
2020
2112
  console.log(`${C("answered")} ${id}${conns.length > 1 ? ` in ${B(hit.cfg.space)}` : ""} — ledger act ${sent.id}`);
2021
2113
  closeAll();
2022
2114
  return;
@@ -0,0 +1,51 @@
1
+ export function selectActs(acts, opts = {}) {
2
+ const kept = acts.filter((a) => (opts.presence || (a.act !== "JOIN" && a.act !== "LEAVE"))
3
+ && (opts.goal === undefined || a.goal === opts.goal)
4
+ && (opts.from === undefined || a.from === opts.from)
5
+ && (opts.since === undefined || a.seq > opts.since));
6
+ const n = opts.last ?? 20;
7
+ return kept.slice(Math.max(0, kept.length - n));
8
+ }
9
+ const when = (ts) => new Date(ts).toISOString().slice(0, 19).replace("T", " ") + "Z";
10
+ export function formatAct(a, opts = {}) {
11
+ const who = `${a.from}${a.role && a.role !== "interactive" ? ` (${a.role})` : ""}`;
12
+ const head = `[${a.seq}] ${a.act} from ${who}${a.to?.length ? ` to ${a.to.join(",")}` : ""} ${when(a.ts)} id=${a.id}${a.ref ? ` ref=${a.ref}` : ""}${a.goal ? ` goal=${a.goal}${opts.goalTitle ? ` "${opts.goalTitle}"` : ""}` : ""}`;
13
+ if (opts.short)
14
+ return `${head}\n ${(a.summary ?? a.body).split("\n")[0].slice(0, 160)}`;
15
+ const meta = a.meta && Object.keys(a.meta).length ? `\n meta: ${JSON.stringify(a.meta)}` : "";
16
+ const summary = a.summary ? `\n >> ${a.summary}` : "";
17
+ return `${head}${summary}\n${a.body.split("\n").map((l) => ` ${l}`).join("\n")}${meta}`;
18
+ }
19
+ const REPLIES_TO = {
20
+ ANSWER: ["ASK", "ESCALATE", "HUMAN_REQUIRED"],
21
+ ACCEPT: ["DELIVERED", "PROPOSE", "CONTRACT"],
22
+ OBJECT: ["DELIVERED", "PROPOSE", "CONTRACT"],
23
+ };
24
+ export function resolveRef(acts, ref, act, goal, me) {
25
+ const inGoal = (a) => goal === null || a.goal === goal;
26
+ if (ref && /^a_/.test(ref)) {
27
+ const hit = acts.find((a) => a.id === ref);
28
+ return { id: ref, goal: hit?.goal ?? goal };
29
+ }
30
+ if (ref && /^\d+$/.test(ref)) {
31
+ const hit = acts.find((a) => a.seq === Number(ref));
32
+ return hit ? { id: hit.id, goal: hit.goal ?? goal, note: `replying to [${hit.seq}] ${hit.act} from ${hit.from}` } : { id: null, goal, error: `no act with sequence ${ref} in what this relay still holds` };
33
+ }
34
+ const others = acts.filter((a) => a.from !== me && a.act !== "JOIN" && a.act !== "LEAVE" && inGoal(a));
35
+ if (ref === "last") {
36
+ const hit = others[others.length - 1];
37
+ return hit ? { id: hit.id, goal: hit.goal ?? goal, note: `replying to [${hit.seq}] ${hit.act} from ${hit.from}` } : { id: null, goal, error: "nothing from a peer to reply to" };
38
+ }
39
+ if (ref)
40
+ return { id: ref, goal };
41
+ const wants = REPLIES_TO[act];
42
+ if (!wants)
43
+ return { id: null, goal };
44
+ for (const kind of wants) {
45
+ const hit = [...others].reverse().find((a) => a.act === kind && (!a.to?.length || a.to.includes(me)));
46
+ if (hit)
47
+ return { id: hit.id, goal: hit.goal ?? goal, note: `replying to [${hit.seq}] ${hit.act} from ${hit.from} (pass --ref to pick another)` };
48
+ }
49
+ return { id: null, goal, error: `${act} must reference the act it answers — none from a peer found${goal ? ` in ${goal}` : ""}. See pantheon history, then --ref <id or sequence number>` };
50
+ }
51
+ //# sourceMappingURL=history-view.js.map
package/dist/mcp.js CHANGED
@@ -9,7 +9,7 @@ import { VERSION } from "./version.js";
9
9
  import { backendFromLabel } from "./agents.js";
10
10
  import { agentHookStates } from "./notify.js";
11
11
  import { reportMcpBusy, reportMcpIdle } from "./mcp-activity.js";
12
- import { loadProfile, organizationProfile, profileInitialized, routeDecision } from "./profile.js";
12
+ import { loadProfile, organizationProfile, profileInitialized, routeDecision, reservedDecisionsNote } from "./profile.js";
13
13
  import { identityLine, renderBrief } from "./brief.js";
14
14
  import { recentRuns } from "./spend.js";
15
15
  const ACT_HELP = {
@@ -60,6 +60,7 @@ YOU WERE WOKEN BY A PEER. No human is present in this session.
60
60
  `
61
61
  : "";
62
62
  const profile = loadProfile(cfg);
63
+ const reserved = reservedDecisionsNote(profile);
63
64
  const identity = profileInitialized(profile)
64
65
  ? ` You work as ${identityLine({
65
66
  member: cfg.member,
@@ -87,7 +88,8 @@ RULES
87
88
  The organization directory — not job-title guesswork — determines whose human or agent owns it.
88
89
  When a peer's human personally must decide and their agent may be offline, use HUMAN_REQUIRED and name that one peer in to.
89
90
  Do not use HUMAN_REQUIRED instead of ASK for something their delegate can answer. Their delegate settles it with ANSWER.
90
- ESCALATE and HUMAN_REQUIRED outlive a goal close until the named human has actually answered.
91
+ ESCALATE and HUMAN_REQUIRED outlive a goal close until the named human has actually answered.${reserved ? `
92
+ - ${reserved}` : ""}
91
93
  - Agree the interface BEFORE writing code that crosses it: PROPOSE, then CONTRACT once accepted.
92
94
  - Answer every ASK addressed to you. An unanswered ASK blocks a peer and eventually stalls the goal.
93
95
  - ACCEPT means you actually checked. If something is wrong, OBJECT with the reason.
package/dist/profile.js CHANGED
@@ -33,6 +33,14 @@ export function defaultProfile(cfg) {
33
33
  updated_at: 0,
34
34
  };
35
35
  }
36
+ export function reservedDecisionsNote(profile) {
37
+ const mine = Object.entries(profile.authority).filter(([, m]) => m === "human_required").map(([d]) => d);
38
+ if (!mine.length)
39
+ return "";
40
+ return `YOUR HUMAN DECIDES THESE PERSONALLY: ${mine.join(", ")}. If a peer asks you to settle one — approve a
41
+ deploy, agree a price, sign off a release — do not answer it yourself, however well you have checked the
42
+ work. Say the review result if you have one, then ESCALATE and stop; tell the peer it is with your human.`;
43
+ }
36
44
  export function wakeDefault(profile) {
37
45
  return (profile.agent.wake_default ?? "anyone") === "anyone";
38
46
  }
@@ -59,7 +59,13 @@ export function questionnaire() {
59
59
  const opt = (flag, id, label) => ` ${label} → --${flag} ${id}`;
60
60
  return [
61
61
  "pantheon setup — the questions, and the flag for each answer.",
62
- "Ask your human each one in plain words, then run: pantheon setup --yes <flags>",
62
+ "Ask your human in plain words, then pass the answers as flags — to `pantheon setup --yes`, or straight to",
63
+ "`pantheon start` / `pantheon join`, which apply them the moment the team exists.",
64
+ "",
65
+ "Only 3 and 4 need the person every time. Ask 2 whenever there is a repo (it is the one default people are",
66
+ "surprised by). The rest have defaults you may take and say out loud:",
67
+ " --wake anyone --decides none --alerts decisions --playbook yes --base <what origin calls its default>",
68
+ "Without a repo, --wake, --commands, --playbook and --base do nothing yet: skip them and say so.",
63
69
  "",
64
70
  "1. While they're away, may teammates hand work to this machine? (their agent runs here on its own,",
65
71
  " in a separate copy of the repo, and delivers on a branch for review)",
@@ -146,34 +152,66 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
146
152
  }
147
153
  const applied = [], warnings = [];
148
154
  if (io) {
149
- io.say("A few choices only you can make. Pick a number; Enter keeps the suggested one.\n");
150
- const wakeIdx = await io.choose("While you're away, may teammates hand work to this machine? Your agent would run here on its own, in a separate copy of the repo, and deliver on a branch for review.", WAKE_OPTIONS.map((o) => o.label), Math.max(0, WAKE_OPTIONS.findIndex((o) => o.id === d.wake)));
151
- a.wake = WAKE_OPTIONS[wakeIdx].id;
152
- if (a.wake !== "no" && ctx.project) {
153
- const c = await io.choose("When a teammate's request wakes this machine, what may the run do?", ["Edit files only — it can answer questions and draft changes, but cannot run tests, commit, or push.",
154
- "Run commands too — needed to run tests and to commit and push work for review. Only requests from people you granted wake permission start a run."], a.commands ? 1 : 0);
155
- a.commands = c === 1;
156
- }
157
- else if (a.wake === "no") {
158
- a.commands = false;
159
- }
155
+ io.say("Three questions, then one screen of defaults. Enter keeps the suggested answer.\n");
160
156
  a.name = (await io.ask("Your name, as teammates will see it", d.name)).trim() || d.name;
161
157
  const known = TITLE_OPTIONS.findIndex((t) => t.toLowerCase() === d.title.toLowerCase());
162
158
  const t = await io.choose("Your role, in a word — it goes on the team directory", TITLE_OPTIONS, known >= 0 ? known : d.title ? TITLE_OPTIONS.length - 1 : 0);
163
159
  a.title = t === TITLE_OPTIONS.length - 1 ? (await io.ask("Your role", d.title)).trim() || d.title || "Member" : TITLE_OPTIONS[t];
164
- const picks = await io.chooseMany("Which of these do you decide personally? Agents — yours and your teammates' — will stop and ask you before acting on them. Pick any number, or none.", DECISION_AREAS.map((x) => x.label), d.decides.map((id) => DECISION_AREAS.findIndex((x) => x.id === id)).filter((i) => i >= 0));
165
- a.decides = [...new Set(picks.map((i) => DECISION_AREAS[i].id))];
166
- const n = await io.choose("Which desktop alerts do you want?", ALERT_OPTIONS.map((o) => o.label), Math.max(0, ALERT_OPTIONS.findIndex((o) => o.id === d.notifications)));
167
- a.notifications = ALERT_OPTIONS[n].id;
160
+ const askCommands = async () => {
161
+ const undecided = profile.agent.allow_bash === undefined && preset.commands === undefined;
162
+ const c = await io.choose("Teammates can hand work to this machine while you're away. It runs in a separate copy of the repo and delivers on a branch you review. May a run also execute commands?", ["Yes — run tests, commit, and push work for review (recommended)",
163
+ "No — edit files and answer only; it cannot run tests, commit, or push"], undecided || a.commands ? 0 : 1);
164
+ a.commands = c === 0;
165
+ };
166
+ let commandsAsked = false;
168
167
  if (a.wake !== "no" && ctx.project) {
169
- const p = await io.choose("Should your worker follow a short written playbook for this repo? It says what a woken run may do here and what it must leave to you. You can edit it any time.", ["Yes — write a starter playbook now (recommended)", "Not now"], 0);
170
- a.persona = p === 0;
168
+ await askCommands();
169
+ commandsAsked = true;
171
170
  }
172
171
  const detected = ctx.project && deps.setRepoBase ? detectBaseBranch(ctx.project) : null;
173
- if (detected) {
174
- const suggested = preset.base ?? detected;
175
- const b = await io.choose("Which branch should your worker start from, and deliveries be checked against? Usually the branch this project deploys or releases from.", [`${suggested}${suggested === detected ? " (what origin calls its default)" : ""}`, "A different branch"], 0);
176
- a.base = b === 0 ? suggested : ((await io.ask("Branch name", suggested)).trim() || suggested);
172
+ const suggestedBase = detected ? (preset.base ?? detected) : undefined;
173
+ if (a.wake !== "no" && ctx.project)
174
+ a.persona = preset.persona ?? true;
175
+ const shortAlert = (id) => ALERT_OPTIONS.find((o) => o.id === id).label.split(" — ")[0].replace(/ \(recommended\)/, "");
176
+ io.say("\nThe rest has defaults. Change them now, or any time with `pantheon setup`:");
177
+ io.say(` • Wake: ${a.wake === "no" ? "off — this machine only works while you are at it"
178
+ : !ctx.project ? "on once this machine has a repo (run pantheon setup from your project's repo)"
179
+ : a.wake === "anyone" ? "teammates may hand work to this machine while you're away; invites you send grant it, and you can revoke anyone (pantheon admit <name> --revoke)"
180
+ : "only people you approve by hand (pantheon admit <name> --may-wake)"}`);
181
+ io.say(` • Alerts: ${shortAlert(a.notifications).toLowerCase()}`);
182
+ io.say(` • Decisions: ${a.decides.length ? `you personally decide ${a.decides.join(", ")}` : "none reserved for you — your agent may decide; add an area when one comes up"}`);
183
+ if (a.wake !== "no" && ctx.project)
184
+ io.say(` • Playbook: ${a.persona ? "a short starter playbook for your worker, in this repo" : "none"}`);
185
+ if (suggestedBase)
186
+ io.say(` • Base branch: ${suggestedBase}${suggestedBase === detected ? " (what origin calls its default)" : ""}`);
187
+ const keep = await io.choose("Keep these?", ["Keep them (recommended)", "Change some"], 0);
188
+ if (keep === 0) {
189
+ if (suggestedBase)
190
+ a.base = suggestedBase;
191
+ }
192
+ else {
193
+ const wakeIdx = await io.choose("While you're away, may teammates hand work to this machine? Your agent would run here on its own, in a separate copy of the repo, and deliver on a branch for review.", WAKE_OPTIONS.map((o) => o.label), Math.max(0, WAKE_OPTIONS.findIndex((o) => o.id === a.wake)));
194
+ a.wake = WAKE_OPTIONS[wakeIdx].id;
195
+ if (a.wake === "no") {
196
+ a.commands = false;
197
+ a.persona = false;
198
+ }
199
+ else if (ctx.project && !commandsAsked) {
200
+ await askCommands();
201
+ a.persona = preset.persona ?? true;
202
+ }
203
+ const picks = await io.chooseMany("Which of these do you decide personally? Agents — yours and your teammates' — will stop and ask you before acting on them. Pick any number, or none.", DECISION_AREAS.map((x) => x.label), a.decides.map((id) => DECISION_AREAS.findIndex((x) => x.id === id)).filter((i) => i >= 0));
204
+ a.decides = [...new Set(picks.map((i) => DECISION_AREAS[i].id))];
205
+ const n = await io.choose("Which desktop alerts do you want?", ALERT_OPTIONS.map((o) => o.label), Math.max(0, ALERT_OPTIONS.findIndex((o) => o.id === a.notifications)));
206
+ a.notifications = ALERT_OPTIONS[n].id;
207
+ if (a.wake !== "no" && ctx.project) {
208
+ const p = await io.choose("Should your worker follow a short written playbook for this repo? It says what a woken run may do here and what it must leave to you. You can edit it any time.", ["Yes — write a starter playbook now (recommended)", "Not now"], a.persona ? 0 : 1);
209
+ a.persona = p === 0;
210
+ }
211
+ if (suggestedBase) {
212
+ const b = await io.choose("Which branch should your worker start from, and deliveries be checked against? Usually the branch this project deploys or releases from.", [`${suggestedBase}${suggestedBase === detected ? " (what origin calls its default)" : ""}`, "A different branch"], 0);
213
+ a.base = b === 0 ? suggestedBase : ((await io.ask("Branch name", suggestedBase)).trim() || suggestedBase);
214
+ }
177
215
  }
178
216
  }
179
217
  let next = a.title ? applyProfileInit(profile, { name: a.name, title: a.title }) : { ...profile, human: { ...profile.human, display_name: a.name } };
package/dist/worker.js CHANGED
@@ -10,7 +10,7 @@ import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolic
10
10
  import { agentHookStates, repairHook } from "./notify.js";
11
11
  import { fence, assess } from "./untrusted.js";
12
12
  import { allowed, recordRun } from "./spend.js";
13
- import { loadProfile } from "./profile.js";
13
+ import { loadProfile, reservedDecisionsNote } from "./profile.js";
14
14
  import { clip, workerDoneNotice } from "./brief.js";
15
15
  import { loadPersonas, matchPersona, personaByName, personaMatches, effectivePolicy, personaBrief, describePersona, } from "./persona.js";
16
16
  import { isContinuationFor, LIMITS, shouldDeferToHuman } from "./protocol.js";
@@ -536,7 +536,8 @@ export class Worker {
536
536
  this.opts.log(`! ${detail}`);
537
537
  return { obligation: o, branch, worktree, ok: false, detail };
538
538
  }
539
- const brief = makeBrief(branch) + baseNote(baseRef, baseSha);
539
+ const reserved = reservedDecisionsNote(loadProfile(this.cfg));
540
+ const brief = makeBrief(branch) + baseNote(baseRef, baseSha) + (reserved ? `\n\n${reserved}` : "");
540
541
  writeFileSync(join(worktree, ".pantheon-task.md"), brief);
541
542
  let command;
542
543
  let args;
@@ -692,6 +693,10 @@ export class Worker {
692
693
  result = { ok: false, alreadyCurrent: false, from: VERSION, to: relayPkg ?? "?" };
693
694
  this.opts.log(`! self-upgrade failed: ${e.message}`);
694
695
  }
696
+ if (result.ok && !result.alreadyCurrent && result.to === result.from) {
697
+ this.opts.log(`* self-upgrade: the registry does not serve ${relayPkg} yet (installed ${result.to}); will check again`);
698
+ return;
699
+ }
695
700
  if (result.ok && !result.alreadyCurrent) {
696
701
  await client.send({
697
702
  act: "NOTE", goal: null, after_seq: client.readSeq,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.21.3",
3
+ "version": "0.22.0",
4
4
  "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {