@join-pantheon/cli 0.33.5 → 0.33.6-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -22,12 +22,13 @@ import { Worker } from "./worker.js";
22
22
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, runNotifiers, sendDesktopNotification } from "./notifier.js";
23
23
  import { linuxDesktopReachable } from "./notice-linux.js";
24
24
  import { desktopUnavailable, hostEnv } from "./host-env.js";
25
- import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, teamProfile, wakeDefault, wakeMode } from "./profile.js";
25
+ import { commandState } from "./command-state.js";
26
+ import { AGENT_SURFACES, applyProfileInit, cleanList, dropCatchAllClaims, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, teamProfile, wakeDefault, wakeMode } from "./profile.js";
26
27
  import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom, decisionAreasProblem } from "./setup-guide.js";
27
28
  import { selectActs, formatAct, resolveRef } from "./history-view.js";
28
29
  import { doctor, render, shareBlock } from "./doctor.js";
29
30
  import { FEEDBACK_ADDRESS } from "./feedback.js";
30
- import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile, elapsed, windowsProcessInfo } from "./machine.js";
31
+ import { menubarProc, notifierProc, relayProc, runBy, workerProc, workerFlagRunMinutes, wiring, logFileFor, dataSummary, stopProc, statFile, elapsed, windowsProcessInfo } from "./machine.js";
31
32
  import { assess } from "./untrusted.js";
32
33
  import { report as spendReport, recentRuns } from "./spend.js";
33
34
  import { renderBrief, renderTeam } from "./brief.js";
@@ -39,14 +40,16 @@ import { removeHook, decideStop, writeIntent, clearIntent, readIntent, repairNot
39
40
  import { pending, render as renderPending } from "./pending.js";
40
41
  import { signedRead } from "./reads.js";
41
42
  import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel, bindableFolder, startReconnects } from "./quickstart.js";
42
- import { recordedAgent } from "./agents.js";
43
+ import { recordedAgent, unattendedReadiness } from "./agents.js";
44
+ import { pickupEstimate } from "./pickup.js";
45
+ import { loadPolicy, requestReadinessRecheck, unsandboxedAllowed } from "./worker-policy.js";
43
46
  import { alreadyMemberLine, askedLine, configureLines, fetchSeats, inUseLine, offerSeats, seatsForTeam, seatsToMove, startGate, RECOVER_WAITING_EXIT } from "./recover-offer.js";
44
47
  import { shellArg, teamNameProblem } from "./team-name.js";
45
48
  import { folderRefusalHere, isTemporaryRefusal, projectFolderAnswer } from "./folder-guard.js";
46
49
  import { installService, serviceInstalled, serviceLogFile, uninstallService } from "./service.js";
47
50
  import { LEGACY_TASK_NAME, hostAlive, hostFiles, readHostState, recordedTaskName } from "./win-host.js";
48
51
  import { ACTS, LIMITS } from "./protocol.js";
49
- import { loadPersonas, personaByName, describePersona, initPersona, personaDir } from "./persona.js";
52
+ import { loadPersonas, personaByName, describePersona, initPersona, personaDir, olderTeammatesNote } from "./persona.js";
50
53
  import { fingerprint, newKeypair, signAccount } from "./identity.js";
51
54
  import { VERSION } from "./version.js";
52
55
  import { connectFailure, signInHint } from "./refusal.js";
@@ -840,6 +843,33 @@ function unattendedProject() {
840
843
  return { project: resolve(flag("project", ".")), how: "explicit" };
841
844
  return { project: resolve("."), how: "default" };
842
845
  }
846
+ function agentReadiness(cfg) {
847
+ let unsandboxedOk = false;
848
+ try {
849
+ unsandboxedOk = unsandboxedAllowed(loadPolicy());
850
+ }
851
+ catch { }
852
+ let commandsOn = false;
853
+ try {
854
+ commandsOn = commandState({ project: process.cwd() }).level !== "off";
855
+ }
856
+ catch { }
857
+ return unattendedReadiness({ prefer: loadProfile(cfg).agent.primary_surface, unsandboxedOk, commandsOn, inContainer: hostEnv().kind === "container" });
858
+ }
859
+ function sayAgentReadiness(cfg) {
860
+ if (!cfg || inWokenRun())
861
+ return;
862
+ requestReadinessRecheck();
863
+ try {
864
+ if (loadProfile(cfg).agent.wake_enabled === false)
865
+ return;
866
+ const r = agentReadiness(cfg);
867
+ if (r.ok)
868
+ return;
869
+ console.error(`\n\x1b[33m! Before teammates send you work:\x1b[0m ${r.says}\n ${r.fix}\n ${D("Messages and decisions work without it; pantheon doctor checks again.")}`);
870
+ }
871
+ catch { }
872
+ }
843
873
  async function guidedSetup(cfg, project, opts = {}) {
844
874
  if (argv.includes("--no-guide"))
845
875
  return;
@@ -868,11 +898,14 @@ async function guidedSetup(cfg, project, opts = {}) {
868
898
  catch {
869
899
  return [];
870
900
  } },
901
+ readiness: () => agentReadiness(cfg),
871
902
  publish: async (p, keep) => {
872
903
  const c = new PantheonClient(cfg, undefined, { role: "cli" });
873
904
  try {
874
905
  await c.connect();
875
- await c.publishProfile(organizationProfile(p, cfg.spaceId, cfg.space), keep);
906
+ const r = await c.publishProfile(organizationProfile(p, cfg.spaceId, cfg.space), keep);
907
+ if (r.note)
908
+ console.error(D(` the relay ${r.note}`));
876
909
  return [...c.profiles.values()];
877
910
  }
878
911
  finally {
@@ -1152,6 +1185,8 @@ ${B("control")}
1152
1185
  pantheon worker stay answerable while you are away
1153
1186
  (--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
1154
1187
  pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
1188
+ pantheon policy set run-minutes <5-180> how long one woken run may take here (default 60)
1189
+ pantheon policy set idle-seconds <0-900> how long your session must be quiet before your worker takes a request (default 120)
1155
1190
  pantheon persona how a woken worker should behave: list / init <name> / show <name>
1156
1191
  pantheon notifier alert you when a task requires your decision
1157
1192
  pantheon menubar the menu bar item: status at a glance, pause/resume, decisions
@@ -1159,6 +1194,7 @@ ${B("control")}
1159
1194
  pantheon sessions your devices & active sessions; revoke <id> to sign one out
1160
1195
  pantheon logout sign this machine out · pantheon account delete remove your account
1161
1196
  pantheon audit [--space X] the log: who did what, when (--json to export, --since <hours>)
1197
+ pantheon usage [--space X] the team's daily message and goal counts, last 30 days (founder only; --days N)
1162
1198
  pantheon profile show, create (init), or set your agent and attention profile
1163
1199
  pantheon directory organization roles and decision authority
1164
1200
  pantheon approvals decisions waiting on you; show or answer one
@@ -1255,13 +1291,14 @@ const COMMAND_FLAGS = {
1255
1291
  say: "<ACT> <text> --to <member>[,<member>] --goal <id> --ref <id | seq | last> --summary '<one line>' --domains <a,b> --branch --commit --remote (DELIVERED) --space <team>",
1256
1292
  goal: "list | open \"<title>\" --with <member>[,…] [--acceptance \"…\"] [--deadline <ISO time>] [--list] | close <id> \"<summary>\"",
1257
1293
  history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
1294
+ usage: "--space <team> --days <n> --json",
1258
1295
  network: "--proxy <url> [--no-proxy <hosts>] | --direct | --clear",
1259
1296
  leave: "[<team>] --yes --keep-identity --stop-relay --project <dir>",
1260
1297
  uninstall: "--yes --forget-identity --stop-relay --project <dir>",
1261
1298
  stop: "[relay|worker|notifier|menubar|all] --port <n>",
1262
1299
  logs: "[relay|worker|notifier|menubar] --follow --n <lines>",
1263
- worker: "--agent <claude-code|codex|cursor|copilot|kiro|auto> --persona <name> --project <dir> --allow-bash --max-runs <n> --max-runs-day <n> --grace <s> --once",
1264
- service: "install [relay|worker|notifier|menubar] | restart <kind> | uninstall <kind> --project <dir> --allow-bash",
1300
+ worker: "--agent <claude-code|codex|cursor|copilot|kiro|auto> --persona <name> --project <dir> --allow-bash --max-runs <n> --max-runs-day <n> --run-minutes <5-180> --grace <s> --once",
1301
+ service: "install [relay|worker|notifier|menubar] | restart <kind> | uninstall <kind> --project <dir> --allow-bash --run-minutes <5-180>",
1265
1302
  doctor: "--share --project <dir>",
1266
1303
  relay: "--port <n> --host <address> --data <dir>",
1267
1304
  use: "<team> --here --yes · pantheon use --unbind (this folder or repo bound to no team again)",
@@ -1536,6 +1573,8 @@ async function main() {
1536
1573
  printReady(r.tools);
1537
1574
  if (r.claimed)
1538
1575
  await guidedSetup(load() ?? r.cfg, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes"), founder: true });
1576
+ if (r.claimed)
1577
+ sayAgentReadiness(load() ?? r.cfg);
1539
1578
  if (r.relay) {
1540
1579
  console.error(D(`\nKeeping the relay running here. Ctrl-C stops it (your teammate goes offline too).`));
1541
1580
  await new Promise(() => { });
@@ -1819,6 +1858,7 @@ async function main() {
1819
1858
  const joined = load();
1820
1859
  if (joined)
1821
1860
  await guidedSetup(joined, repoInfo(process.cwd())?.root ?? null, { preset: setupFlags.preset, force: Object.keys(setupFlags.preset).length > 0 || argv.includes("--yes") });
1861
+ sayAgentReadiness(joined);
1822
1862
  }
1823
1863
  console.error(`\n${D('Then ask your agent: "check pantheon_status and say hello to the team".')}`);
1824
1864
  return;
@@ -2020,6 +2060,7 @@ async function main() {
2020
2060
  die(errors.join("\n") + "\n pantheon setup --questions lists every answer");
2021
2061
  const cfg = need();
2022
2062
  await guidedSetup(cfg, repoInfo(process.cwd())?.root ?? null, { force: true, preset });
2063
+ sayAgentReadiness(load() ?? cfg);
2023
2064
  return;
2024
2065
  }
2025
2066
  case "version": {
@@ -2039,6 +2080,7 @@ async function main() {
2039
2080
  });
2040
2081
  await new Promise((r) => setTimeout(r, 350));
2041
2082
  const peers = client.roster.filter((m) => m.id !== cfg.member);
2083
+ const { termSafe } = await import("./board.js");
2042
2084
  console.log(`${B(cfg.space)} you are ${C(cfg.member)} ${D(cfg.relay)} ${D(`${client.head} acts`)}\n`);
2043
2085
  if (client.plan)
2044
2086
  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");
@@ -2060,6 +2102,11 @@ async function main() {
2060
2102
  ? `\x1b[33m${recent ? `their agent has not seen your last ${behind} act${behind === 1 ? "" : "s"}` : `their agent is ${behind} act${behind === 1 ? "" : "s"} behind the team`}\x1b[0m`
2061
2103
  : lag > 0 ? D("their agent has seen everything you sent") : D("their agent is caught up");
2062
2104
  console.log(` ${p.online ? C("●") : D("○")} ${p.id.padEnd(14)} ${D(p.agent.padEnd(12))} ${reach}`);
2105
+ {
2106
+ const est = pickupEstimate(p, { now: client.relayNow() });
2107
+ if (est.kind === "unavailable")
2108
+ console.log(` ${" ".repeat(16)}\x1b[33m${termSafe(est.says)}\x1b[0m`);
2109
+ }
2063
2110
  }
2064
2111
  const goals = [...client.goals.values()];
2065
2112
  console.log(`\n${B("goals")}`);
@@ -2094,8 +2141,12 @@ async function main() {
2094
2141
  console.log(`\n${B("between delegates")}`);
2095
2142
  if (!between.length)
2096
2143
  console.log(` ${D("(nothing owed)")}`);
2144
+ const myMayWake = client.members.find((m) => m.member === cfg.member)?.may_wake;
2097
2145
  for (const o of between) {
2098
2146
  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)}`);
2147
+ const peer = o.owner !== cfg.member && o.kind !== "DELIVER" ? client.roster.find((m) => m.id === o.owner) ?? { id: o.owner, sessions: [] } : null;
2148
+ if (peer)
2149
+ console.log(` ${" ".repeat(7)} ${D(termSafe(pickupEstimate(peer, { now: client.relayNow(), about: { ref: o.ref, goal: o.goal }, ...(myMayWake !== undefined ? { mayWake: myMayWake } : {}) }).says))}`);
2099
2150
  }
2100
2151
  if (human.length || between.length || goals.some((g) => g.state !== "CLOSED")) {
2101
2152
  console.log(D(`\n read in full: pantheon show <id> · a thread: pantheon history --goal <goal> · reply: pantheon say ANSWER "…" --ref <id>`));
@@ -2388,7 +2439,7 @@ async function main() {
2388
2439
  const upP = client ? client.connect().then(() => true, () => false) : Promise.resolve(false);
2389
2440
  console.log(B("this machine"));
2390
2441
  if (relay.running) {
2391
- const how = relay.managedBy === "launchd"
2442
+ const how = relay.managedBy === "launchd" || relay.managedBy === "systemd"
2392
2443
  ? `service ${relay.label}`
2393
2444
  : "started by hand — it dies when that terminal closes";
2394
2445
  console.log(` relay ${C("running")} ${how} pid ${relay.pid}${relay.since ? ` up ${relay.since}` : ""}`);
@@ -2415,7 +2466,7 @@ async function main() {
2415
2466
  }
2416
2467
  }
2417
2468
  if (worker.running) {
2418
- console.log(` worker ${C("running")} ${worker.managedBy === "launchd" ? `service ${worker.label}` : worker.managedBy === "host" ? `background host (${worker.label})` : "started by hand"} pid ${worker.pid}`);
2469
+ console.log(` worker ${C("running")} ${runBy(worker)} pid ${worker.pid}`);
2419
2470
  console.log(` ${D("a teammate approved on this machine (pantheon wake list) can start an agent run here")}`);
2420
2471
  }
2421
2472
  else {
@@ -2440,19 +2491,24 @@ async function main() {
2440
2491
  }
2441
2492
  }
2442
2493
  else if (menubar.running) {
2443
- console.log(` menu bar ${C("running")} ${menubar.managedBy === "launchd" ? `service ${menubar.label}` : "started by hand"} pid ${menubar.pid}`);
2494
+ console.log(` menu bar ${C("running")} ${runBy(menubar)} pid ${menubar.pid}`);
2444
2495
  }
2445
2496
  else {
2446
2497
  const why = desktopUnavailable(hostEnv(), "the menu bar");
2447
2498
  console.log(` menu bar ${D("not running")} ${D(why ?? "pantheon service install menubar — status and pause/resume in the menu bar")}`);
2448
2499
  }
2449
2500
  if (notifier.running) {
2450
- console.log(` notifier ${C("running")} ${notifier.managedBy === "launchd" || notifier.managedBy === "systemd" ? `service ${notifier.label}` : notifier.managedBy === "host" ? `background host (${notifier.label})` : "started by hand"} pid ${notifier.pid}`);
2501
+ console.log(` notifier ${C("running")} ${runBy(notifier)} pid ${notifier.pid}`);
2451
2502
  console.log(` ${D("native alerts are on for decisions that require you")}`);
2452
2503
  }
2453
2504
  else {
2454
2505
  console.log(` notifier ${D("not running")} ${D("human escalations only appear in pantheon status")}`);
2455
2506
  }
2507
+ const RM = await import("./removed.js");
2508
+ for (const { team, record } of RM.removals(RM.teamsOf(loadAll()))) {
2509
+ const r = RM.removalWords(team, record.by);
2510
+ console.log(` removed from "${r.name}"${r.by ? ` by ${r.by}` : ""} ${D(`on ${new Date(record.at).toISOString().slice(0, 10)} — this machine stopped connecting to it (checks now and then, at most every 6 hours); coming back needs a new invite: pantheon join <invite>`)}`);
2511
+ }
2456
2512
  console.log(`\n${B("wired into")}`);
2457
2513
  for (const w of wiring())
2458
2514
  console.log(` ${w.tool.padEnd(14)}${w.state.startsWith("not wired") ? D(w.state) : w.state}`);
@@ -2500,7 +2556,7 @@ async function main() {
2500
2556
  ? D("no signal — no turn reported since the relay came up; your next prompt reports one")
2501
2557
  : w.busy
2502
2558
  ? `a turn is in flight${w.busy_since ? ` (for ${ago(w.busy_since)})` : ""} ${D(`the worker waits; a turn past ${LIMITS.activityBusyTtlMs / 60_000}m with no Stop is read as ended`)}`
2503
- : `idle for ${ago(w.idle_since)} ${D(`the worker takes work after ${LIMITS.workerIdleMs / 60_000}m idle`)}`;
2559
+ : `idle for ${ago(w.idle_since)} ${D(await (async () => { const W = await import("./worker-policy.js"); const n = W.effectiveIdleSeconds({ policy: loadPolicy() }).seconds; return n ? `the worker takes work after ${W.describeIdleSeconds(n)} idle` : "the worker takes work as soon as no turn is in flight"; })())}`;
2504
2560
  console.log(` your activity: ${activity}`);
2505
2561
  for (const p of client.roster.filter((m) => m.id !== cfg.member)) {
2506
2562
  console.log(` ${p.id.padEnd(16)}${p.online ? C("online") : D("offline")} ${p.sessions.length} window(s)`);
@@ -2856,6 +2912,10 @@ async function main() {
2856
2912
  ` Delete it instead: pantheon space delete ${m.name} --yes\n Leave it anyway: pantheon leave ${m.name} --yes`);
2857
2913
  }
2858
2914
  removeMembership(m.id);
2915
+ try {
2916
+ (await import("./removed.js")).clearRemoval({ space: m.name, spaceId: m.id, relay: m.relay });
2917
+ }
2918
+ catch { }
2859
2919
  console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length} binding${bound.length === 1 ? "" : "s"} with it (${bound.join(", ")})` : ""}.`);
2860
2920
  sayDefaultAfter(all, m);
2861
2921
  if (others === null)
@@ -3389,6 +3449,37 @@ async function main() {
3389
3449
  console.log(D(`\nRevoke one: pantheon sessions revoke <id> · remove a device: pantheon sessions revoke <device-id> --device`));
3390
3450
  return;
3391
3451
  }
3452
+ case "usage": {
3453
+ let cfg;
3454
+ if (flag("space")) {
3455
+ const pf = loadAll() ?? die("not configured — pantheon join <invite> first");
3456
+ const m = findMembership(pf, flag("space")) ?? die(`you are not in a team called "${flag("space")}"`);
3457
+ cfg = configFor(pf, m, null);
3458
+ }
3459
+ else
3460
+ cfg = need();
3461
+ const { USAGE_SHOW_DAYS, USAGE_KEEP_DAYS, usageTable } = await import("./usage.js");
3462
+ const daysRaw = flag("days");
3463
+ const days = daysRaw === undefined ? USAGE_SHOW_DAYS : /^\d+$/.test(daysRaw) && Number(daysRaw) >= 1 && Number(daysRaw) <= USAGE_KEEP_DAYS ? Number(daysRaw) : die(`--days takes a whole number from 1 to ${USAGE_KEEP_DAYS}`);
3464
+ let res;
3465
+ try {
3466
+ res = await signedRead(cfg, "/usage", { days }, { timeoutMs: 15_000 });
3467
+ }
3468
+ catch (e) {
3469
+ die(connectFailure(e, cfg.relay));
3470
+ }
3471
+ const body = await res.json().then((b) => (b && typeof b === "object" ? b : null), () => null);
3472
+ if (!body || typeof body.usage_v !== "number")
3473
+ die(`"${cfg.space}"'s relay runs a Pantheon older than 0.33.6, which keeps no usage counts — nothing to show. Its operator can upgrade it.`);
3474
+ if (!res.ok || !Array.isArray(body.days))
3475
+ die(body.error ?? `the relay refused (HTTP ${res.status})`);
3476
+ if (argv.includes("--json")) {
3477
+ console.log(JSON.stringify(body, null, 2));
3478
+ return;
3479
+ }
3480
+ console.log(usageTable(body.space ?? cfg.space, body.days, days));
3481
+ return;
3482
+ }
3392
3483
  case "audit": {
3393
3484
  const cfg = identity() ?? die("not set up on this machine — pantheon signup first");
3394
3485
  if (!cfg.account)
@@ -3482,6 +3573,11 @@ async function main() {
3482
3573
  console.log(`${B(describePersona(p))} ${D(p.file)}`);
3483
3574
  for (const w of p.warnings)
3484
3575
  console.log(` \x1b[33m! ${w}\x1b[0m`);
3576
+ {
3577
+ const n = olderTeammatesNote(p);
3578
+ if (n)
3579
+ console.log(` \x1b[33m! ${n}\x1b[0m`);
3580
+ }
3485
3581
  console.log("");
3486
3582
  console.log(p.instructions || D("(no instructions — policy only)"));
3487
3583
  return;
@@ -3497,6 +3593,11 @@ async function main() {
3497
3593
  console.log(` ${describePersona(p)} ${D(p.file)}`);
3498
3594
  for (const w of p.warnings)
3499
3595
  console.log(` \x1b[33m! ${w}\x1b[0m`);
3596
+ {
3597
+ const n = olderTeammatesNote(p);
3598
+ if (n)
3599
+ console.log(` \x1b[33m! ${n}\x1b[0m`);
3600
+ }
3500
3601
  }
3501
3602
  return;
3502
3603
  }
@@ -3537,7 +3638,9 @@ async function main() {
3537
3638
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
3538
3639
  try {
3539
3640
  await client.connect();
3540
- await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space), keep);
3641
+ const r = await client.publishProfile(organizationProfile(profile, cfg.spaceId, cfg.space), keep);
3642
+ if (r.note)
3643
+ console.error(D(` the relay ${r.note}`));
3541
3644
  }
3542
3645
  finally {
3543
3646
  client.close();
@@ -3580,7 +3683,12 @@ async function main() {
3580
3683
  const keywords = flag("keywords");
3581
3684
  const publicChange = name !== undefined || title !== undefined || responsibilities !== undefined ||
3582
3685
  humanRequired !== undefined || agentCanDecide !== undefined || capabilities !== undefined || keywords !== undefined;
3583
- if (!surface && !publicChange) {
3686
+ const vague = dropCatchAllClaims(profile);
3687
+ if (vague.dropped.length) {
3688
+ profile = { ...vague.profile, updated_at: Date.now() };
3689
+ console.error(`${C("dropped")} ${vague.dropped.map((d) => `"${d}"`).join(", ")} from your decision claims — not a decision area, so it never routed anything`);
3690
+ }
3691
+ if (!surface && !publicChange && !vague.dropped.length) {
3584
3692
  die("usage: pantheon profile set [--surface codex|claude-code|cursor|copilot|kiro] [--name name] [--title title] [--responsibilities a,b] [--human-required a,b] [--agent-can-decide a,b] [--keywords 'domain=word|word;domain=word']");
3585
3693
  }
3586
3694
  if (flag("space")) {
@@ -3605,8 +3713,8 @@ async function main() {
3605
3713
  const tcl = new PantheonClient(tc, undefined, { role: "cli" });
3606
3714
  try {
3607
3715
  await tcl.connect();
3608
- await tcl.publishProfile(organizationProfile(profile, m.id, m.name), [...(humanRequired ?? []), ...(agentCanDecide ?? [])]);
3609
- console.error(`${C("published")} into "${m.name}"`);
3716
+ const r = await tcl.publishProfile(organizationProfile(profile, m.id, m.name), [...(humanRequired ?? []), ...(agentCanDecide ?? [])]);
3717
+ console.error(`${C("published")} into "${m.name}"${r.note ? D(` — the relay ${r.note}`) : ""}`);
3610
3718
  }
3611
3719
  catch (e) {
3612
3720
  console.error(`\x1b[33mnot published\x1b[0m — ${e.message}`);
@@ -3636,7 +3744,7 @@ async function main() {
3636
3744
  };
3637
3745
  saveProfile(profile);
3638
3746
  console.error(`${C("saved")} — ${surface ? `decision notifications open ${surface}` : "local organization profile updated"}`);
3639
- if (publicChange) {
3747
+ if (publicChange || vague.dropped.length) {
3640
3748
  try {
3641
3749
  await publish([...(humanRequired ?? []), ...(agentCanDecide ?? [])]);
3642
3750
  console.error(`${C("published")} — the organization can now route decisions using this authority`);
@@ -4070,9 +4178,52 @@ async function main() {
4070
4178
  }
4071
4179
  }
4072
4180
  case "policy": {
4073
- const { loadPolicy, savePolicy, grant, revoke, inWokenRun, CAPABILITY_ARGS } = await import("./worker-policy.js");
4181
+ const { loadPolicy, savePolicy, grant, revoke, inWokenRun, CAPABILITY_ARGS, parseRunMinutes, runMinutesProblem, effectiveRunMinutes, describeRunMinutes, RUN_MINUTES, parseIdleSeconds, idleSecondsProblem, effectiveIdleSeconds, describeIdleSeconds, IDLE_SECONDS } = await import("./worker-policy.js");
4074
4182
  const { COMMAND_GRANT_SCOPE } = await import("./agents.js");
4075
4183
  const sub = argv[1];
4184
+ if (sub === "set") {
4185
+ const key = argv[2];
4186
+ if (key !== "run-minutes" && key !== "idle-seconds")
4187
+ die(`usage: pantheon policy set run-minutes <${RUN_MINUTES.min}-${RUN_MINUTES.max} | default> (how long one woken run may take on this machine)\n pantheon policy set idle-seconds <${IDLE_SECONDS.min}-${IDLE_SECONDS.max} | default> (how long your session must be quiet before your worker takes a request)`);
4188
+ if (key === "idle-seconds") {
4189
+ if (inWokenRun())
4190
+ die("a woken run cannot change how long the worker waits for you — a person has to run this here");
4191
+ const raw = argv[3];
4192
+ const p = loadPolicy();
4193
+ if (raw === "default")
4194
+ delete p.idleSeconds;
4195
+ else {
4196
+ const n = parseIdleSeconds(raw);
4197
+ if (n === null)
4198
+ die(idleSecondsProblem(raw));
4199
+ p.idleSeconds = n;
4200
+ }
4201
+ savePolicy(p);
4202
+ const now = effectiveIdleSeconds({ policy: p });
4203
+ console.log(`${C("set")} idle wait — your worker takes a teammate's request ${now.seconds ? `after your session has been quiet ${describeIdleSeconds(now.seconds)}` : "with no extra wait once your session is quiet (a turn in flight still holds it)"}${now.from === "default" ? " (the default)" : ""} ${D("— from its next look, no restart; teammates are told the wait")}`);
4204
+ if (workerProc().running)
4205
+ console.log(D(` a worker started with --idle keeps its own value`));
4206
+ return;
4207
+ }
4208
+ if (inWokenRun())
4209
+ die("a woken run cannot change how long runs may take — a person has to run this here");
4210
+ const raw = argv[3];
4211
+ const p = loadPolicy();
4212
+ if (raw === "default")
4213
+ delete p.runMinutes;
4214
+ else {
4215
+ const n = parseRunMinutes(raw);
4216
+ if (n === null)
4217
+ die(runMinutesProblem(raw));
4218
+ p.runMinutes = n;
4219
+ }
4220
+ savePolicy(p);
4221
+ const now = effectiveRunMinutes({ policy: p });
4222
+ console.log(`${C("set")} run limit — ${describeRunMinutes(now)} ${D("— from the next run; a run already going keeps the limit it started with")}`);
4223
+ if (workerProc().running)
4224
+ console.log(D(` a worker started with --run-minutes keeps its own value; personas with max_run_minutes still lower it for what they handle`));
4225
+ return;
4226
+ }
4076
4227
  const capArg = (argv[2] ?? "").replace(/-/g, "_");
4077
4228
  const caps = Object.keys(CAPABILITY_ARGS);
4078
4229
  if (sub === "grant" || sub === "revoke") {
@@ -4096,7 +4247,7 @@ async function main() {
4096
4247
  return;
4097
4248
  }
4098
4249
  if (sub && sub !== "show")
4099
- die(`usage: pantheon policy [show | grant <capability> | revoke <capability>] capabilities: ${caps.map((c) => CAPABILITY_ARGS[c]).join(", ")}`);
4250
+ die(`usage: pantheon policy [show | grant <capability> | revoke <capability> | set run-minutes <${RUN_MINUTES.min}-${RUN_MINUTES.max}|default> | set idle-seconds <${IDLE_SECONDS.min}-${IDLE_SECONDS.max}|default>] capabilities: ${caps.map((c) => CAPABILITY_ARGS[c]).join(", ")}`);
4100
4251
  const p = loadPolicy();
4101
4252
  const w = workerProc();
4102
4253
  console.log(`${B("What this machine's worker may do")}\n`);
@@ -4132,7 +4283,19 @@ async function main() {
4132
4283
  console.log(` ${D(`ignored "${r.line}" — ${r.why}`)}`);
4133
4284
  }
4134
4285
  console.log(` runs ${D("12/hour, 40/day, one at a time, 2 failed attempts then parked (defaults; personas only lower them)")}`);
4135
- console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands|unsandboxed-runs · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
4286
+ {
4287
+ const lim = effectiveRunMinutes({ policy: p });
4288
+ const lowered = pers.filter((x) => x.maxRunMinutes !== undefined && x.maxRunMinutes < lim.minutes);
4289
+ console.log(` run limit ${describeRunMinutes(lim)}${D(` — a run still going then is stopped, tried once more, then handed to you (change: pantheon policy set run-minutes <${RUN_MINUTES.min}-${RUN_MINUTES.max}>)`)}`);
4290
+ if (lowered.length)
4291
+ console.log(` ${D(`lower for what ${lowered.map((x) => `${x.name} (${x.maxRunMinutes} min)`).join(", ")} handle${lowered.length === 1 ? "s" : ""}`)}`);
4292
+ const wf = workerFlagRunMinutes();
4293
+ if (wf !== null)
4294
+ console.log(` \x1b[33mthe running worker was started with --run-minutes ${wf}, which it uses instead\x1b[0m`);
4295
+ const idle = effectiveIdleSeconds({ policy: p });
4296
+ console.log(` idle wait ${idle.seconds ? `${describeIdleSeconds(idle.seconds)} of quiet in your session before your worker takes a request` : "none — your worker takes a request as soon as no turn is in flight in your session"}${D(` (${idle.from === "default" ? "the default" : "set on this machine"}; change: pantheon policy set idle-seconds <${IDLE_SECONDS.min}-${IDLE_SECONDS.max}>)`)}`);
4297
+ }
4298
+ console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands|unsandboxed-runs · pantheon policy set run-minutes <n> | idle-seconds <n> · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
4136
4299
  return;
4137
4300
  }
4138
4301
  case "notice-show": {
@@ -4852,6 +5015,7 @@ async function main() {
4852
5015
  return;
4853
5016
  }
4854
5017
  case "worker": {
5018
+ const runMinutesFlag = flag("run-minutes") === undefined ? undefined : (await import("./worker-policy.js")).parseRunMinutes(flag("run-minutes")) ?? die((await import("./worker-policy.js")).runMinutesProblem(flag("run-minutes")).replace("pantheon policy set run-minutes default", "leave --run-minutes off"));
4855
5019
  (await import("./machine.js")).recordWorkerPid();
4856
5020
  const allCfg = loadAll();
4857
5021
  const bindings = Object.values(allCfg?.bindings ?? {});
@@ -4862,10 +5026,11 @@ async function main() {
4862
5026
  ...(flag("persona") ? { persona: flag("persona") } : {}),
4863
5027
  continuations: !argv.includes("--no-continue"),
4864
5028
  allowBash: argv.includes("--allow-bash"),
4865
- idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000,
5029
+ ...(flag("idle") !== undefined ? { idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000 } : {}),
4866
5030
  maxRunsPerHour: numFlag("max-runs", 12),
4867
5031
  maxRunsPerDay: numFlag("max-runs-day", 40),
4868
5032
  graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
5033
+ ...(runMinutesFlag !== undefined ? { runMinutes: runMinutesFlag } : {}),
4869
5034
  once: argv.includes("--once"),
4870
5035
  ...(argv.includes("--no-notify") ? {} : {
4871
5036
  notify: (n) => { try {
@@ -4893,10 +5058,11 @@ async function main() {
4893
5058
  ...(flag("persona") ? { persona: flag("persona") } : {}),
4894
5059
  continuations: !argv.includes("--no-continue"),
4895
5060
  allowBash: argv.includes("--allow-bash"),
4896
- idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000,
5061
+ ...(flag("idle") !== undefined ? { idleMs: numFlag("idle", LIMITS.workerIdleMs / 1000) * 1000 } : {}),
4897
5062
  maxRunsPerHour: numFlag("max-runs", 12),
4898
5063
  maxRunsPerDay: numFlag("max-runs-day", 40),
4899
5064
  graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
5065
+ ...(runMinutesFlag !== undefined ? { runMinutes: runMinutesFlag } : {}),
4900
5066
  once: argv.includes("--once"),
4901
5067
  ...(argv.includes("--no-notify") ? {} : {
4902
5068
  notify: (n) => { try {
@@ -5204,7 +5370,12 @@ async function main() {
5204
5370
  extra.push("--allow-bash");
5205
5371
  if (argv.includes("--no-continue"))
5206
5372
  extra.push("--no-continue");
5207
- for (const f of ["idle", "grace", "max-runs", "max-runs-day", "scratch", "agent-cmd", "agent", "persona"]) {
5373
+ if (flag("run-minutes") !== undefined) {
5374
+ const { parseRunMinutes, runMinutesProblem } = await import("./worker-policy.js");
5375
+ if (parseRunMinutes(flag("run-minutes")) === null)
5376
+ die(runMinutesProblem(flag("run-minutes")).replace("pantheon policy set run-minutes default", "leave --run-minutes off"));
5377
+ }
5378
+ for (const f of ["idle", "grace", "max-runs", "max-runs-day", "run-minutes", "scratch", "agent-cmd", "agent", "persona"]) {
5208
5379
  if (flag(f))
5209
5380
  extra.push(`--${f}`, flag(f));
5210
5381
  }