@join-pantheon/cli 0.33.1-rc.1 → 0.33.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,11 +194,12 @@ this machine does not have runs nothing: the asker is told it waits, and it is n
194
194
  another repo's code. `pantheon spaces` and `doctor` say what the worker serves and what is not here.
195
195
 
196
196
  **No git at all (a PM, ops, a founder's docs).** `start` or `join` from a plain folder binds it
197
- (never your home folder). The worker serves it too: a teammate's question is answered from that
198
- folder, which the run can read but never change; anything it writes goes to a drafts folder of its
199
- own, and you are told. Answering from a folder unattended needs Claude Code on that machine (the one
200
- agent that can be opened a folder to read and held off editing it); with another agent, those requests
201
- wait for your live session, and the teammate is told.
197
+ (never your home folder). The worker serves it too, with any agent you have installed (Claude Code,
198
+ Codex, Cursor or Copilot): each run works in a copy of the folder made for it, so the folder itself is
199
+ never changed. The copy leaves out links, `.git`, `node_modules` and secret-looking files (`.env`,
200
+ keys, credentials), and a folder over 5,000 files or 200 MB is not copied: the request waits for you,
201
+ and the teammate is told why. Anything the run writes or changes in its copy is kept as drafts, and
202
+ you are told which files and where; a copy with no drafts is deleted when the run ends.
202
203
 
203
204
  ### If something is wrong
204
205
 
package/dist/accounts.js CHANGED
@@ -133,6 +133,7 @@ export class Accounts {
133
133
  CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_id);
134
134
  CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit(ts);
135
135
  CREATE INDEX IF NOT EXISTS idx_audit_space ON audit(space);
136
+ CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit(actor_account, event);
136
137
  CREATE TABLE IF NOT EXISTS blocks (
137
138
  pattern TEXT PRIMARY KEY,
138
139
  kind TEXT NOT NULL,
@@ -433,6 +434,15 @@ export class Accounts {
433
434
  deviceLive(pubkey) {
434
435
  return this.deviceByPubkey(pubkey) !== null;
435
436
  }
437
+ wasDeviceOf(accountId, fp, before) {
438
+ if (this.db.prepare("SELECT 1 FROM devices WHERE account_id = ? AND fingerprint = ? AND created_at <= ?").get(accountId, fp, before))
439
+ return true;
440
+ return !!this.db.prepare("SELECT 1 FROM audit WHERE actor_account = ? AND event = 'device_added' AND target = ? AND ts <= ?").get(accountId, fp, before);
441
+ }
442
+ lastEmailProof(accountId, fp) {
443
+ const r = this.db.prepare("SELECT MAX(ts) ts FROM audit WHERE actor_account = ? AND event IN ('login', 'email_verified') AND target = ?").get(accountId, fp);
444
+ return r?.ts ?? null;
445
+ }
436
446
  deleteAccount(accountId, ip = "") {
437
447
  const acct = this.accountById(accountId);
438
448
  if (!acct)
package/dist/agents.js CHANGED
@@ -137,6 +137,17 @@ export function agentsNeedingUnsandboxed(available = detectAvailable()) {
137
137
  return [];
138
138
  return [...new Set(available.filter((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.backend.id)).map((a) => a.backend.label))];
139
139
  }
140
+ export const FOLDER_READERS = AGENTS.map((a) => a.id);
141
+ export function readsFolders(id) {
142
+ return !!id && FOLDER_READERS.includes(id);
143
+ }
144
+ export function folderReaderLine(available) {
145
+ const takers = available.filter((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.backend.id));
146
+ if (takers.length) {
147
+ return { ok: true, line: `any installed, signed-in agent here (${takers.map((a) => a.backend.label).join(", ")}) answers requests about bound folders while you are away, working in a copy — the folder itself is never changed` };
148
+ }
149
+ return { ok: false, line: `requests about bound folders wait for you: no agent here takes runs while you are away${available.length ? ` (${KIRO_UNATTENDED_REFUSAL})` : ""}` };
150
+ }
140
151
  export const claudeSandboxable = (f, os = process.platform) => os !== "win32" && !!f.settings && versionAtLeast(f.version, "2.1.83");
141
152
  export function kiroSignedIn(binary, run = runStatus, env = process.env, home = homedir(), os = platform()) {
142
153
  if (env.KIRO_API_KEY)
package/dist/board.js CHANGED
@@ -51,6 +51,12 @@ export function clearBoardState(pid, home = CONFIG_DIR) {
51
51
  }
52
52
  export const LINK_TTL_MS = 30_000;
53
53
  export const boardLink = (port, nonce) => `http://127.0.0.1:${port}/#open=${nonce}`;
54
+ export const FOCUS_RE = /^[ao]_[A-Za-z0-9]{6,48}$/;
55
+ export function focusLink(link, focus) {
56
+ if (!FOCUS_RE.test(focus))
57
+ throw new Error("not an act or decision id");
58
+ return `${link}~focus=${focus}`;
59
+ }
54
60
  const TOKEN_RE = /^[0-9a-f]{64}$/;
55
61
  const ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
56
62
  const MAX_BODY_BYTES = 64 * 1024;
@@ -215,6 +221,11 @@ export function hudActionFor(op, item) {
215
221
  if (op.kind === "ANSWER" && item.kind !== "decision")
216
222
  return { error: "only the person it waits on can answer it — add a note or ask back" };
217
223
  return { action: "reply", kind: op.kind, body: op.body, ...goal, ...space, ...(item.kind === "decision" ? { decision: item.id } : {}), ...ref };
224
+ case "wake-allow":
225
+ case "wake-deny":
226
+ if (item.kind !== "decision" || !item.wake || !item.ref)
227
+ return { error: "only a teammate waiting on your approval can be approved or declined here" };
228
+ return { action: "wake", allow: op.op === "wake-allow", member: item.wake, decision: item.id, act: item.ref, ...space };
218
229
  case "open":
219
230
  return { action: "open", prompt: item.prompt, ...space, ...goal, ...(item.kind === "decision" ? { decision: item.id } : {}) };
220
231
  }
@@ -238,6 +249,8 @@ export function parseBoardAction(raw) {
238
249
  switch (v.op) {
239
250
  case "done": return only([]) ? { op: "done", id, ...sp } : null;
240
251
  case "open": return only([]) ? { op: "open", id, ...sp } : null;
252
+ case "wake-allow":
253
+ case "wake-deny": return only([]) ? { op: v.op, id, ...sp } : null;
241
254
  case "choose":
242
255
  return only(["index"]) && Number.isInteger(v.index) && v.index >= 0 && v.index < 10
243
256
  ? { op: "choose", id, ...sp, index: v.index } : null;
@@ -561,6 +574,10 @@ export async function startBoardServer(o = {}) {
561
574
  }
562
575
  const q = await runQueued(hud);
563
576
  const r = explainOutcome(q.r, q.nonce);
577
+ if (r.ok && hud.action === "wake") {
578
+ sendJson(res, 200, { ok: true, message: hud.allow ? `${hud.member} may start work on this machine — their waiting request runs now` : `Declined — ${hud.member}'s requests wait for you (pantheon wake allow ${hud.member} changes it)` });
579
+ return;
580
+ }
564
581
  if (r.sent) {
565
582
  sent.set(r.sent.id, { ...(item.space ? { space: item.space } : {}), at: deps.now() });
566
583
  for (const [k, v] of sent)
@@ -601,7 +618,7 @@ export async function startBoardServer(o = {}) {
601
618
  }
602
619
  return { port, token, get url() { return mintLink(); }, link: mintLink, server, get keep() { return keep; }, close, lastSeen: () => lastSeen };
603
620
  }
604
- const LINK_RE = /^http:\/\/127\.0\.0\.1:\d{1,5}\/#open=[0-9a-f]{64}$/;
621
+ const LINK_RE = /^http:\/\/127\.0\.0\.1:\d{1,5}\/#open=[0-9a-f]{64}(?:~focus=[ao]_[A-Za-z0-9]{6,48})?$/;
605
622
  export function inServiceCgroup(read = (p) => readFileSync(p, "utf8"), os = platform()) {
606
623
  if (os !== "linux")
607
624
  return false;
@@ -681,9 +698,13 @@ export function terminalCommand(home, actionId, os = platform(), env = process.e
681
698
  return { cmd: "open", args: [file], opts };
682
699
  }
683
700
  if (os === "win32") {
684
- if (argv.some((a) => /["%\r\n]/.test(a)))
701
+ const file = join(home, "bin", "pantheon-board-explain.cmd");
702
+ if ([...argv, file].some((a) => /["%\r\n]/.test(a)))
685
703
  return { error: "the path to Pantheon holds a character the Windows console cannot pass safely" };
686
- const line = `start "Pantheon" ${argv.map((a) => `"${a}"`).join(" ")}`;
704
+ const text = ["@echo off", "%*", "if errorlevel 1 (", " echo.", " echo Pantheon: the agent did not start, or stopped with an error ^(above^). Press a key to close this window.", " pause >nul", ")", "exit", ""].join("\r\n");
705
+ mkdirSync(dirname(file), { recursive: true });
706
+ writeFileSync(file, text);
707
+ const line = `start "Pantheon" "${file}" ${[...argv.slice(0, -1).map((a) => `"${a}"`), actionId].join(" ")}`;
687
708
  return { cmd: "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], opts: { ...opts, windowsVerbatimArguments: true, windowsHide: true } };
688
709
  }
689
710
  if (!env.DISPLAY && !env.WAYLAND_DISPLAY)
@@ -1074,9 +1095,11 @@ const LANES = ${JSON.stringify(LANES.map((l) => l.id))};
1074
1095
  const SESSION_KEY = "pantheon-board-session";
1075
1096
  let link = null;
1076
1097
  let session = null;
1098
+ // The item an alert opened the board on ("Approve or decline"): shown first, once.
1099
+ let focus = null;
1077
1100
  {
1078
- const m = /^#open=([0-9a-f]{64})$/.exec(location.hash);
1079
- if (m) link = m[1];
1101
+ const m = /^#open=([0-9a-f]{64})(?:~focus=([ao]_[A-Za-z0-9]{6,48}))?$/.exec(location.hash);
1102
+ if (m) { link = m[1]; focus = m[2] || null; }
1080
1103
  // The nonce leaves the address bar and this history entry before anything else runs.
1081
1104
  history.replaceState(null, "", "/");
1082
1105
  }
@@ -1195,7 +1218,12 @@ function row(item, card) {
1195
1218
  li.append(meta);
1196
1219
  if (item.context) li.append(el("p", "ctx", "For: " + item.context));
1197
1220
  const acts = el("div", "actions");
1198
- if (card) {
1221
+ if (card && item.wake) {
1222
+ // A teammate waiting on this person's approval: Approve / Just this once /
1223
+ // Decline, and no "Mark done" — that would settle the question unanswered.
1224
+ wakeButtons(item).forEach((b) => acts.append(b));
1225
+ acts.append(tag(button("Reply…", () => openDetail(item, true)), item, "reply"));
1226
+ } else if (card) {
1199
1227
  (item.choices || []).forEach((c, idx) => acts.append(tag(button(c.label, (ev) => act({ op: "choose", id: item.id, space: item.space, index: idx }, ev.currentTarget, c.label + " — sent."), idx === 0 ? "primary" : ""), item, "c" + idx)));
1200
1228
  if (item.kind === "decision") acts.append(tag(button("Mark done", (ev) => act({ op: "done", id: item.id, space: item.space }, ev.currentTarget), ""), item, "done"));
1201
1229
  acts.append(tag(button("Reply…", () => openDetail(item, true)), item, "reply"));
@@ -1208,6 +1236,17 @@ function row(item, card) {
1208
1236
  return li;
1209
1237
  }
1210
1238
 
1239
+ // Approve and Decline are pantheon wake allow|deny for the teammate the
1240
+ // question is about; "Just this once" is the question's own one-run answer.
1241
+ function wakeButtons(item, after) {
1242
+ const done = (r) => { if (after && r && r.ok) after(); };
1243
+ const out = [tag(button("Approve " + item.wake, (ev) => act({ op: "wake-allow", id: item.id, space: item.space }, ev.currentTarget).then(done), "primary"), item, "wake-allow")];
1244
+ const once = (item.choices || []).findIndex((c) => c.answer === "allow once");
1245
+ if (once >= 0) out.push(tag(button("Just this once", (ev) => act({ op: "choose", id: item.id, space: item.space, index: once }, ev.currentTarget, "Just this once — their request runs now.").then(done)), item, "wake-once"));
1246
+ out.push(tag(button("Decline", (ev) => act({ op: "wake-deny", id: item.id, space: item.space }, ev.currentTarget).then(done)), item, "wake-deny"));
1247
+ return out;
1248
+ }
1249
+
1211
1250
  async function copy(text) {
1212
1251
  try { await navigator.clipboard.writeText(text); toast("Copied — paste it into your agent."); return; }
1213
1252
  catch { /* fall back to a box the person copies from */ }
@@ -1265,15 +1304,16 @@ async function openDetail(item, focusReply) {
1265
1304
  blocks($("d-body"), [{ kind: "para", text: item.detail || "" }]);
1266
1305
  $("d-options").hidden = true; $("d-options").replaceChildren();
1267
1306
  const ch = $("d-choices"); ch.replaceChildren();
1268
- (item.choices || []).forEach((c, idx) => ch.append(button(c.label, (ev) => act({ op: "choose", id: item.id, space: item.space, index: idx }, ev.currentTarget, c.label + " — sent.").then((r) => { if (r && r.ok) d.close(); }), idx === 0 ? "primary" : "")));
1269
- ch.hidden = !(item.choices && item.choices.length);
1307
+ if (item.wake) wakeButtons(item, () => d.close()).forEach((b) => ch.append(b));
1308
+ else (item.choices || []).forEach((c, idx) => ch.append(button(c.label, (ev) => act({ op: "choose", id: item.id, space: item.space, index: idx }, ev.currentTarget, c.label + " — sent.").then((r) => { if (r && r.ok) d.close(); }), idx === 0 ? "primary" : "")));
1309
+ ch.hidden = !(item.wake || (item.choices && item.choices.length));
1270
1310
  const kind = $("d-kind"); kind.replaceChildren();
1271
1311
  for (const [v, label] of (item.kind === "decision" ? [["ANSWER", "Answer"], ["NOTE", "Note"], ["ASK", "Ask back"]] : [["NOTE", "Note"], ["ASK", "Ask back"]])) { const o = el("option", "", label); o.value = v; kind.append(o); }
1272
1312
  $("d-text").value = "";
1273
1313
  setStatus("");
1274
1314
  $("d-retract").hidden = true;
1275
1315
  const actions = $("d-actions"); actions.replaceChildren();
1276
- if (item.kind === "decision") actions.append(button("Mark done", (ev) => act({ op: "done", id: item.id, space: item.space }, ev.currentTarget).then((r) => { if (r && r.ok) d.close(); })));
1316
+ if (item.kind === "decision" && !item.wake) actions.append(button("Mark done", (ev) => act({ op: "done", id: item.id, space: item.space }, ev.currentTarget).then((r) => { if (r && r.ok) d.close(); })));
1277
1317
  actions.append(button("Explain in " + surface(), (ev) => act({ op: "open", id: item.id, space: item.space }, ev.currentTarget)));
1278
1318
  if (item.goal && item.space) actions.append(button("Show the thread", () => thread(item)));
1279
1319
  actions.append(button("Copy the question", () => copy(item.prompt)));
@@ -1394,6 +1434,12 @@ document.addEventListener("visibilitychange", () => { if (!document.hidden) refr
1394
1434
  }
1395
1435
  if (!session) { $("loading").hidden = true; banner("This board needs its link. Run pantheon board to open it.", true); return; }
1396
1436
  await refresh();
1437
+ if (focus && state) {
1438
+ const want = focus;
1439
+ focus = null;
1440
+ const hit = (state.items || []).find((i) => i.id === want || i.ref === want);
1441
+ if (hit) openDetail(hit); else toast("That is no longer waiting on you.");
1442
+ }
1397
1443
  schedule();
1398
1444
  })();
1399
1445
  })();
@@ -139,7 +139,7 @@ function userExcludedCommands(file) {
139
139
  return [];
140
140
  }
141
141
  }
142
- const SECRET_FILE_GLOBS = [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*", ".npmrc", ".netrc", "credentials*", "*.keystore", "service-account*.json"];
142
+ export const SECRET_FILE_GLOBS = [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*", ".npmrc", ".netrc", "credentials*", "*.keystore", "service-account*.json"];
143
143
  const PANTHEON_HOME_ENTRIES = ["config.json", "config.json.bak", "config.json.lock", "worker-policy.json", "profile.json", "continued.json", "bin", "runtime", "ops", "run-homes", "logs", "mcp", "data"];
144
144
  function aroundWorktree(top, keeps, named) {
145
145
  const keep = (typeof keeps === "string" ? [keeps] : [...keeps]).map((k) => resolve(k));