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