@join-pantheon/cli 0.33.6 → 0.33.7

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
@@ -787,3 +787,10 @@ notices the new version and switches to it within a minute, with no disconnect a
787
787
  reopen (windows opened on a pantheon older than 0.24 need one `/mcp` reconnect; `pantheon doctor`
788
788
  names them). A self-hosted relay is shared with your teammates, so it prints the restart for that
789
789
  and leaves the timing to you.
790
+
791
+ **Going back to 0.33.6 or earlier after 0.33.7.** From 0.33.7 a woken run's DELIVERED is held on
792
+ this machine until its branch is on origin (`held-deliveries.json` in the run home and in
793
+ `~/.pantheon`), and the worker sends it once it has pushed. A release before 0.33.7 does not know
794
+ those files: anything still held when you go back is never sent. Let the worker finish (or check
795
+ that both files are gone) before you downgrade, and tell the teammate where the work is if one is
796
+ left.
package/dist/agents.js CHANGED
@@ -324,18 +324,21 @@ export function chooseAgent(preferred, opts = {}) {
324
324
  const first = live.find((o) => !NEEDS_SIGN_IN_BEFORE_SPAWN.has(o.backend.id)) ?? live[0];
325
325
  return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
326
326
  }
327
- export function unattendedRefusal(c, o = {}) {
327
+ export function unattendedRefusalOf(c, o = {}) {
328
328
  if (NOT_FOR_UNATTENDED_RUNS.has(c.backend.id))
329
- return KIRO_UNATTENDED_REFUSAL;
329
+ return { cause: "not_for_unattended", says: KIRO_UNATTENDED_REFUSAL };
330
330
  if (!SANDBOXED_FOR_UNATTENDED.has(c.backend.id) && !o.unsandboxedOk) {
331
- return c.because === "pinned"
332
- ? `woken runs here are pinned to ${c.backend.label} (a persona's agent: line, or the worker's --agent), which is set to leave teammates' requests for you, so this one waits for your next session. ` +
333
- `To have ${c.backend.label} answer them: pantheon policy grant unsandboxed-runs — or unpin it so Claude Code takes them`
334
- : unsandboxedRefusal(c.backend.label);
331
+ return { cause: "unsandboxed_off", says: c.because === "pinned"
332
+ ? `woken runs here are pinned to ${c.backend.label} (a persona's agent: line, or the worker's --agent), which is set to leave teammates' requests for you, so this one waits for your next session. ` +
333
+ `To have ${c.backend.label} answer them: pantheon policy grant unsandboxed-runs — or unpin it so Claude Code takes them`
334
+ : unsandboxedRefusal(c.backend.label) };
335
335
  }
336
- if (!NEEDS_SIGN_IN_BEFORE_SPAWN.has(c.backend.id) || c.signIn?.ok)
336
+ if (!(o.needsSignIn ?? NEEDS_SIGN_IN_BEFORE_SPAWN).has(c.backend.id) || c.signIn?.ok)
337
337
  return null;
338
- return `${c.backend.label} is not signed in (${c.signIn?.detail ?? "not checked"}), and signed out it opens a browser to log in, so it is not started while you are away — ${SIGN_IN_CHECK[c.backend.id].fix}`;
338
+ return { cause: "not_signed_in", says: `${c.backend.label} is not signed in (${c.signIn?.detail ?? "not checked"}), and signed out it opens a browser to log in, so it is not started while you are away — ${SIGN_IN_CHECK[c.backend.id].fix}` };
339
+ }
340
+ export function unattendedRefusal(c, o = {}) {
341
+ return unattendedRefusalOf(c, o)?.says ?? null;
339
342
  }
340
343
  export function claudeInstallCommand(os = platform()) {
341
344
  return os === "win32" ? "irm https://claude.ai/install.ps1 | iex (in PowerShell)" : "curl -fsSL https://claude.ai/install.sh | bash";
@@ -363,6 +366,13 @@ export function unattendedReadiness(o = {}) {
363
366
  ...AGENTS.filter((a) => NOT_FOR_UNATTENDED_RUNS.has(a.id)),
364
367
  ].filter((a, i, all) => all.findIndex((x) => x.id === a.id) === i);
365
368
  const found = [];
369
+ let othersSeen = null;
370
+ const others = () => (othersSeen ??= (() => {
371
+ for (const a of AGENTS)
372
+ if (a.id !== pinned?.id && !NOT_FOR_UNATTENDED_RUNS.has(a.id) && resolve(a))
373
+ return { claude: a.id === "claude-code", first: a };
374
+ return { claude: false, first: null };
375
+ })());
366
376
  let last = null;
367
377
  for (const b of order) {
368
378
  const bin = resolve(b);
@@ -371,15 +381,29 @@ export function unattendedReadiness(o = {}) {
371
381
  found.push({ backend: b, binary: bin });
372
382
  if (NOT_FOR_UNATTENDED_RUNS.has(b.id))
373
383
  continue;
374
- const r = readinessFrom(found, { ...o, check });
384
+ const r = readinessFrom(found, { ...o, check, ...(pinned ? { others } : {}) });
375
385
  if (r.ok)
376
386
  return r;
377
387
  last = { r, n: found.length };
378
388
  }
379
- return last && last.n === found.length ? last.r : readinessFrom(found, { ...o, check });
389
+ if (last && last.n === found.length)
390
+ return last.r;
391
+ return readinessFrom(found, { ...o, check, ...(pinned ? { others } : {}) });
380
392
  }
381
393
  function readinessFrom(given, o) {
382
394
  const os = o.os ?? platform();
395
+ const pin = o.pinned && o.pinned !== "auto" ? agentById(o.pinned) : undefined;
396
+ const others = o.others ?? (() => {
397
+ const rest = given.filter((a) => a.backend.id !== pin?.id && !NOT_FOR_UNATTENDED_RUNS.has(a.backend.id));
398
+ const claude = rest.find((a) => a.backend.id === "claude-code");
399
+ return { claude: !!claude, first: (claude ?? rest[0])?.backend ?? null };
400
+ });
401
+ const unpin = `unpin it (remove the persona's agent: line, or the worker's --agent)`;
402
+ if (pin && NOT_FOR_UNATTENDED_RUNS.has(pin.id) && given.some((a) => a.backend.id === pin.id)) {
403
+ const other = others().first;
404
+ return { ok: false, code: "kiro_only", pinned: true, short: `${pin.label} is set to leave requests for them`, says: `Teammates' requests wait for your next session here: woken runs are pinned to ${pin.label}, and ${KIRO_UNATTENDED_REFUSAL}.`,
405
+ fix: other ? `${unpin} so ${other.label} takes them` : `install Claude Code (${claudeInstallCommand(os)}), then ${unpin} so it takes them` };
406
+ }
383
407
  const available = given.filter((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.backend.id));
384
408
  if (!available.length && (!o.pinned || o.pinned === "auto")) {
385
409
  const kiro = given.some((a) => a.backend.id === "kiro");
@@ -398,12 +422,15 @@ function readinessFrom(given, o) {
398
422
  const m = e.message;
399
423
  return { ok: false, code: /no agent CLI on PATH|is not on PATH/.test(m) ? "no_agent" : "no_agent_choice", short: m.replace(/ \(looked for .*\)$/, "").slice(0, 120), says: m, fix: "pantheon doctor names what is missing" };
400
424
  }
401
- const refused = unattendedRefusal(choice, { ...(o.unsandboxedOk !== undefined ? { unsandboxedOk: o.unsandboxedOk } : {}) });
402
- if (refused) {
403
- return { ok: false, code: NOT_FOR_UNATTENDED_RUNS.has(choice.backend.id) ? "kiro_only" : "unsandboxed_off", short: `${choice.backend.label} is set to leave requests for them`, says: `Teammates' requests wait for your next session here: ${refused}`,
404
- fix: `install Claude Code (${claudeInstallCommand(os)}) to have requests answered in its sandbox, or: pantheon policy grant unsandboxed-runs` };
425
+ const refused = unattendedRefusalOf(choice, { ...(o.unsandboxedOk !== undefined ? { unsandboxedOk: o.unsandboxedOk } : {}), ...(o.needsSignIn ? { needsSignIn: o.needsSignIn } : {}) });
426
+ if (refused && refused.cause !== "not_signed_in") {
427
+ const pinnedOff = choice.because === "pinned" && refused.cause === "unsandboxed_off";
428
+ const fix = pinnedOff
429
+ ? `pantheon policy grant unsandboxed-runs — or unpin ${choice.backend.label} (remove the persona's agent: line, or the worker's --agent) so Claude Code takes them${others().claude ? "" : ` (install it first: ${claudeInstallCommand(os)})`}`
430
+ : `install Claude Code (${claudeInstallCommand(os)}) to have requests answered in its sandbox, or: pantheon policy grant unsandboxed-runs`;
431
+ return { ok: false, code: refused.cause === "not_for_unattended" ? "kiro_only" : "unsandboxed_off", ...(pinnedOff ? { pinned: true } : {}), short: `${choice.backend.label} is set to leave requests for them`, says: `Teammates' requests wait for your next session here: ${refused.says}`, fix };
405
432
  }
406
- if (choice.signIn && !choice.signIn.ok && !choice.signIn.unknown) {
433
+ if (refused || (choice.signIn && !choice.signIn.ok && !choice.signIn.unknown)) {
407
434
  return { ok: false, code: "not_signed_in", short: `${choice.backend.label} isn't signed in`, says: `${choice.backend.label} is installed but not signed in, so a request while you're away would fail.`,
408
435
  fix: SIGN_IN_CHECK[choice.backend.id].fix };
409
436
  }
@@ -600,6 +627,7 @@ export function headlessSpawn(opts) {
600
627
  const extra = [];
601
628
  let env = procEnv;
602
629
  let rulesOnArgv = bashRules;
630
+ let osSandbox = false;
603
631
  if (opts.claude && opts.runHome) {
604
632
  const runTmp = runTmpDir(opts.runHome);
605
633
  const c = claudeRunSettings({
@@ -624,6 +652,7 @@ export function headlessSpawn(opts) {
624
652
  notes.push(...c.notes);
625
653
  if (c.sandboxed)
626
654
  env = { ...procEnv, ...sandboxCacheEnv(runTmp) };
655
+ osSandbox = "sandbox" in c.settings;
627
656
  }
628
657
  else if (f.disallowedTools) {
629
658
  const deny = c.settings.permissions.deny;
@@ -655,6 +684,7 @@ export function headlessSpawn(opts) {
655
684
  writeFiles,
656
685
  ...(notes.length ? { notes } : {}),
657
686
  ...(extra.includes("--setting-sources") ? { repoSettingsIgnored: true } : {}),
687
+ ...(osSandbox ? { osSandbox: true } : {}),
658
688
  };
659
689
  }
660
690
  if (opts.backend.id === "codex") {
@@ -1,9 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
- import { lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync } from "node:fs";
3
+ import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, statSync, unlinkSync } from "node:fs";
4
4
  import { writeInside } from "./safe-write.js";
5
5
  import { homedir, platform, tmpdir } from "node:os";
6
- import { isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
6
+ import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
7
7
  import { expectedLauncher, windowsManagedRoots } from "./managed.js";
8
8
  import { agentSpawn } from "./which.js";
9
9
  export const NO_CLAUDE_FEATURES = { version: null, permissionPrompts: false, settings: false, settingSources: false, disallowedTools: false };
@@ -130,9 +130,35 @@ export function absentDeniedPaths(common) {
130
130
  }
131
131
  return [...out].sort((a, b) => b.length - a.length);
132
132
  }
133
- export function removeSandboxLeftovers(absent) {
133
+ export function leavesSandboxMounts(o) {
134
+ return o.os === "linux" && o.agent === "claude-code" && o.sandbox;
135
+ }
136
+ const bwrapFile = (st, anyMode = false) => st.isFile() && st.size === 0 && st.nlink === 1 && (anyMode || (st.mode & 0o222) === 0 || (st.mode & 0o777) === 0o666);
137
+ function realFoldersBelow(root, dir) {
138
+ const rel = relative(root, dir);
139
+ if (rel.startsWith("..") || isAbsolute(rel))
140
+ return false;
141
+ let cur = root;
142
+ for (const part of rel ? rel.split(sep) : []) {
143
+ cur = join(cur, part);
144
+ try {
145
+ const st = lstatSync(cur);
146
+ if (!st.isDirectory() || st.isSymbolicLink())
147
+ return false;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ return true;
154
+ }
155
+ export function removeSandboxLeftovers(absent, common) {
156
+ const root = resolve(common);
134
157
  const removed = [];
135
158
  for (const p of absent) {
159
+ if (!p.startsWith(root + sep) || !realFoldersBelow(root, dirname(p)))
160
+ continue;
161
+ const anyMode = p === join(root, "commondir");
136
162
  let st;
137
163
  try {
138
164
  st = lstatSync(p);
@@ -141,11 +167,14 @@ export function removeSandboxLeftovers(absent) {
141
167
  continue;
142
168
  }
143
169
  try {
144
- if (st.isFile() && st.size === 0 && st.nlink === 1) {
145
- rmSync(p, { force: true });
170
+ if (bwrapFile(st, anyMode)) {
171
+ const now = lstatSync(p);
172
+ if (!bwrapFile(now, anyMode) || now.ino !== st.ino || now.dev !== st.dev || !realFoldersBelow(root, dirname(p)))
173
+ continue;
174
+ unlinkSync(p);
146
175
  removed.push(p);
147
176
  }
148
- else if (st.isDirectory() && readdirSync(p).length === 0) {
177
+ else if (st.isDirectory() && !st.isSymbolicLink() && readdirSync(p).length === 0) {
149
178
  rmdirSync(p);
150
179
  removed.push(p);
151
180
  }
@@ -154,6 +183,31 @@ export function removeSandboxLeftovers(absent) {
154
183
  }
155
184
  return removed;
156
185
  }
186
+ export function ownWorktreeConfig(worktree, common) {
187
+ try {
188
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(join(worktree, ".git"), "utf8"));
189
+ if (!m)
190
+ return null;
191
+ const gitdir = realpathSync(resolve(worktree, m[1]));
192
+ const root = realpathSync(resolve(common));
193
+ return dirname(gitdir) === join(root, "worktrees") ? join(resolve(common), "worktrees", basename(gitdir), "config.worktree") : null;
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
199
+ export function describeKept(paths) {
200
+ const out = [];
201
+ for (const p of paths) {
202
+ try {
203
+ const st = lstatSync(p);
204
+ const kind = st.isDirectory() ? "a folder" : st.isFile() ? "a file" : "not a file";
205
+ out.push(`${p}: ${kind}, ${st.size} bytes, mode ${(st.mode & 0o777).toString(8).padStart(4, "0")}, ${st.nlink} link${st.nlink === 1 ? "" : "s"}`);
206
+ }
207
+ catch { }
208
+ }
209
+ return out;
210
+ }
157
211
  const inside = (child, parent) => {
158
212
  const r = relative(parent, child);
159
213
  return r === "" || (!!r && !r.startsWith("..") && !isAbsolute(r));
package/dist/cli.js CHANGED
@@ -3017,6 +3017,14 @@ async function main() {
3017
3017
  const { unlinkSync, existsSync: exists, rmSync, readdirSync } = await import("node:fs");
3018
3018
  const spare = [];
3019
3019
  const hasConfig = !!all && exists(CONFIG_PATH);
3020
+ if (!homeRefusal) {
3021
+ const { heldDeliveriesLeft } = await import("./held-delivery.js");
3022
+ const held = heldDeliveriesLeft(CONFIG_DIR);
3023
+ if (held) {
3024
+ left.push(held);
3025
+ console.warn(`! ${held}`);
3026
+ }
3027
+ }
3020
3028
  if (homeRefusal) {
3021
3029
  left.push(refusedHomeLine(CONFIG_DIR, homeRefusal));
3022
3030
  }
package/dist/client.js CHANGED
@@ -630,10 +630,17 @@ export class PantheonClient {
630
630
  return this.sendAct(a);
631
631
  }
632
632
  async sendAct(a) {
633
+ return this.postAct(this.composeAct(a));
634
+ }
635
+ composeAct(a) {
633
636
  const repo = this.cfg.repo;
634
637
  const meta = a.act === "DELIVERED" && repo && !(a.meta && "repo" in a.meta) ? { ...(a.meta ?? {}), repo } : a.meta;
635
638
  const id = newActId();
636
639
  const sig = this.signAct(id, { ...a, ...(meta ? { meta } : {}) }, repo);
640
+ return { t: "act", id, ...a, ...(meta ? { meta } : {}), ...(repo ? { repo } : {}), sig };
641
+ }
642
+ async postAct(p) {
643
+ const { t: _t, id, sig: _s, repo: _r, ...a } = p;
637
644
  let unrecorded = false, unsignedRef = false;
638
645
  if (a.ref && this.ws?.readyState === WebSocket.OPEN && (a.act === "ANSWER" || a.act === "ACCEPT" || a.act === "OBJECT" || a.act === "DELIVERED")) {
639
646
  try {
@@ -644,7 +651,7 @@ export class PantheonClient {
644
651
  unrecorded = noted === false && this.signaturesExpected();
645
652
  unsignedRef = noted === "unsigned";
646
653
  }
647
- const r = await this.rpc({ t: "act", id, ...a, ...(meta ? { meta } : {}), ...(repo ? { repo } : {}), sig });
654
+ const r = await this.rpc(p);
648
655
  let sentId = r.id === id ? id : null;
649
656
  if (r.id !== id && this.features.includes("act-sig")) {
650
657
  let prior = this.seen.get(r.id) ?? null;
package/dist/doctor.js CHANGED
@@ -388,8 +388,9 @@ export async function doctor(projectDir) {
388
388
  }
389
389
  }
390
390
  const budget = instructionBudget(cfg);
391
- const budgetLine = `${budget.chars} chars (~${budget.tokens} tokens) of session instructions per turn; the ${budget.protocolChars}-char protocol arrives with the first tool call instead`;
392
- add(budget.chars > 1_200 ? "warn" : "pass", "mcp context", budgetLine, budget.chars > 1_200 ? "this used to be ~3,800 chars on every turn, and it competed with the work people asked for" : undefined, budgetLine);
391
+ const fromProfile = budget.chars - budget.fixedChars;
392
+ const budgetLine = `${budget.chars} chars (~${budget.tokens} tokens) of session instructions per turn — ${budget.fixedChars} Pantheon's own${fromProfile > 0 ? `, ${fromProfile} from your profile (who you are, what you reserved)` : ""}; the ${budget.protocolChars}-char protocol arrives with the first tool call instead`;
393
+ add(budget.fixedChars > 1_000 ? "warn" : "pass", "mcp context", budgetLine, budget.fixedChars > 1_000 ? "Pantheon's own part used to be ~3,800 chars on every turn, and it competed with the work people asked for" : undefined, budgetLine);
393
394
  const approval = chosenApproval();
394
395
  add("pass", "approval", approvalSummary(approval), `change with pantheon setup --ask-before-sending (or --approve-all); pantheon leave removes the Codex line`, approval);
395
396
  const measured = await measureMcp({ cwd: projectDir });
@@ -0,0 +1,247 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { normalizeOrigin } from "./config.js";
6
+ import { withFileLock } from "./file-lock.js";
7
+ import { actPayload, canonicalJson, verifyBytes, verifyDelegate } from "./identity.js";
8
+ import { proxyEnvFor } from "./network.js";
9
+ import { writeInside } from "./safe-write.js";
10
+ export const HOLD_CONFIG = "delivery-hold.json";
11
+ export const HELD_FILE = "held-deliveries.json";
12
+ export const HELD_STATE = "held-deliveries.json";
13
+ export const MAX_HELD_PER_RUN = 5;
14
+ const capped = (list) => {
15
+ const sorted = [...list].sort((a, b) => (Number(a.at) || 0) - (Number(b.at) || 0));
16
+ const kept = sorted.slice(-MAX_HELD_PER_RUN);
17
+ return { kept, dropped: sorted.length - kept.length };
18
+ };
19
+ export const NOT_PUSHED = ["stopped", "not_own_branch", "not_allowed", "mass_delete", "push_refused"];
20
+ const COMMIT_RE = /^[0-9a-f]{7,64}$/i;
21
+ const BRANCH_RE = /^[A-Za-z0-9._][A-Za-z0-9._/-]{0,199}$/;
22
+ const okBranch = (b) => typeof b === "string" && BRANCH_RE.test(b) && !b.includes("..") && !b.endsWith("/") && !b.endsWith(".lock");
23
+ const str = (v) => typeof v === "string" && v.length > 0 && v.length < 4096;
24
+ export const holdable = (branch, commit) => okBranch(branch) && typeof commit === "string" && COMMIT_RE.test(commit);
25
+ export function sameRemote(a, b) {
26
+ if (a === b)
27
+ return true;
28
+ const x = normalizeOrigin(a), y = normalizeOrigin(b);
29
+ return !!x && x === y;
30
+ }
31
+ function writeAtomic(root, file, text) {
32
+ const tmp = `${file}.${process.pid}.${Date.now().toString(36)}.tmp`;
33
+ try {
34
+ writeInside(root, tmp, text, 0o600);
35
+ renameSync(tmp, file);
36
+ }
37
+ catch (e) {
38
+ try {
39
+ rmSync(tmp, { force: true });
40
+ }
41
+ catch { }
42
+ throw e;
43
+ }
44
+ }
45
+ function readList(file) {
46
+ let text;
47
+ try {
48
+ text = readFileSync(file, "utf8");
49
+ }
50
+ catch {
51
+ return { value: null, corrupt: null };
52
+ }
53
+ try {
54
+ return { value: JSON.parse(text), corrupt: null };
55
+ }
56
+ catch {
57
+ const aside = `${file}.corrupt-${Date.now()}`;
58
+ try {
59
+ renameSync(file, aside);
60
+ }
61
+ catch { }
62
+ return { value: null, corrupt: aside };
63
+ }
64
+ }
65
+ export function writeHoldConfig(runHome, c) {
66
+ if (!c) {
67
+ try {
68
+ rmSync(join(runHome, HOLD_CONFIG), { force: true });
69
+ }
70
+ catch { }
71
+ return;
72
+ }
73
+ writeAtomic(runHome, join(runHome, HOLD_CONFIG), JSON.stringify(c) + "\n");
74
+ }
75
+ export function readHoldConfig(runHome) {
76
+ try {
77
+ const c = JSON.parse(readFileSync(join(runHome, HOLD_CONFIG), "utf8"));
78
+ return c && str(c.origin) && !c.origin.startsWith("-") && okBranch(c.branch) ? { origin: c.origin, branch: c.branch } : null;
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ export function appendHeld(runHome, h) {
85
+ const file = join(runHome, HELD_FILE);
86
+ return withFileLock(file, () => {
87
+ const { value } = readList(file);
88
+ const list = Array.isArray(value) ? value.filter((x) => x?.frame?.id !== h.frame.id) : [];
89
+ const { kept, dropped } = capped([...list, h]);
90
+ writeAtomic(runHome, file, JSON.stringify(kept) + "\n");
91
+ return dropped;
92
+ });
93
+ }
94
+ export function takeHeldFrames(runHome, keep) {
95
+ const file = join(runHome, HELD_FILE);
96
+ return withFileLock(file, () => {
97
+ const { value, corrupt } = readList(file);
98
+ const all = Array.isArray(value)
99
+ ? value.filter((x) => !!x && typeof x === "object" && !!x.frame && typeof x.frame === "object")
100
+ .map((x) => ({ frame: x.frame, at: Number(x.at) || 0 }))
101
+ : [];
102
+ const { kept: frames, dropped } = capped(all);
103
+ keep(frames);
104
+ try {
105
+ rmSync(file, { force: true });
106
+ }
107
+ catch { }
108
+ return { corrupt, dropped };
109
+ });
110
+ }
111
+ export function corruptHeldFiles(dir, base = HELD_FILE) {
112
+ try {
113
+ return readdirSync(dir).filter((n) => n.startsWith(`${base}.corrupt-`)).map((n) => join(dir, n));
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ }
119
+ export function verifyHeldFrame(f, o) {
120
+ if (!f || typeof f !== "object" || f.t !== "act" || f.act !== "DELIVERED" || typeof f.id !== "string" || !/^a_[a-z0-9]{1,40}$/i.test(f.id) || typeof f.body !== "string")
121
+ return null;
122
+ const m = (f.meta ?? {});
123
+ if (!okBranch(m.branch) || typeof m.commit !== "string" || !COMMIT_RE.test(m.commit))
124
+ return null;
125
+ const sig = f.sig;
126
+ if (!sig || sig.role !== "run" || !sig.cert || sig.pub !== o.runPub || sig.cert.pub !== o.runPub || sig.cert.run !== o.runId)
127
+ return null;
128
+ if (canonicalJson(sig.meta ?? {}) !== canonicalJson(f.meta ?? {}) || (sig.repo ?? null) !== (f.repo ?? null))
129
+ return null;
130
+ const teams = o.teams.filter((t) => !!t);
131
+ try {
132
+ const vouched = teams.some((t) => verifyDelegate(o.memberPub, t, o.member, sig.cert, sig.cert.expires));
133
+ if (!vouched)
134
+ return null;
135
+ const ok = teams.some((team) => verifyBytes(sig.pub, actPayload({
136
+ id: f.id, team, member: o.member, keyRole: "run", nonce: sig.nonce, ts: sig.ts,
137
+ act: f.act, goal: f.goal ?? null, ref: f.ref ?? null, to: f.to ?? null,
138
+ defers_to: f.defers_to ?? null, affects: f.affects ?? null, repo: f.repo ?? null,
139
+ summary: f.summary ?? null, body: f.body, meta: f.meta ?? {},
140
+ }), sig.s));
141
+ return ok ? { branch: m.branch, commit: m.commit } : null;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ function asState(v) {
148
+ const s = v;
149
+ return { v: 1, runs: Array.isArray(s?.runs) ? s.runs : [], held: Array.isArray(s?.held) ? s.held : [] };
150
+ }
151
+ export function updateHeldState(stateDir, fn) {
152
+ const file = join(stateDir, HELD_STATE);
153
+ return withFileLock(file, () => {
154
+ const { value, corrupt } = readList(file);
155
+ const s = asState(value);
156
+ const before = JSON.stringify(s);
157
+ const result = fn(s);
158
+ const after = JSON.stringify(s);
159
+ if (after !== before) {
160
+ if (!s.runs.length && !s.held.length) {
161
+ try {
162
+ rmSync(file, { force: true });
163
+ }
164
+ catch { }
165
+ }
166
+ else {
167
+ const tmp = `${file}.${process.pid}.tmp`;
168
+ writeFileSync(tmp, after + "\n", { mode: 0o600 });
169
+ renameSync(tmp, file);
170
+ }
171
+ }
172
+ return { result, corrupt };
173
+ });
174
+ }
175
+ export function heldDeliveriesLeft(home) {
176
+ const named = [];
177
+ const say = (meta) => {
178
+ const m = (meta ?? {});
179
+ named.push(typeof m.branch === "string" && typeof m.commit === "string" ? `${m.branch} at ${m.commit.slice(0, 7)}` : "a branch");
180
+ };
181
+ try {
182
+ const v = JSON.parse(readFileSync(join(home, HELD_STATE), "utf8"));
183
+ for (const h of Array.isArray(v.held) ? v.held : [])
184
+ say(h?.frame?.meta);
185
+ }
186
+ catch { }
187
+ let homes = [];
188
+ try {
189
+ homes = readdirSync(join(home, "run-homes"));
190
+ }
191
+ catch { }
192
+ for (const h of homes) {
193
+ try {
194
+ const v = JSON.parse(readFileSync(join(home, "run-homes", h, HELD_FILE), "utf8"));
195
+ for (const x of Array.isArray(v) ? v : [])
196
+ say(x?.frame?.meta);
197
+ }
198
+ catch { }
199
+ }
200
+ if (!named.length)
201
+ return null;
202
+ const n = named.length;
203
+ return `${n} deliver${n === 1 ? "y" : "ies"} a woken run held until ${n === 1 ? "its" : "their"} branch was on origin, never sent (${[...new Set(named)].join(", ")}) — ` +
204
+ `${n === 1 ? "it is" : "they are"} lost with Pantheon's files; tell your teammates where that work is (it is on those branches in this machine's checkouts)`;
205
+ }
206
+ let sshSet = null;
207
+ function ownSshCommand(cwd, env) {
208
+ if (sshSet !== null)
209
+ return sshSet;
210
+ try {
211
+ sshSet = execFileSync("git", ["config", "--get", "core.sshCommand"], { cwd, env, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim().length > 0;
212
+ }
213
+ catch {
214
+ sshSet = false;
215
+ }
216
+ return sshSet;
217
+ }
218
+ export function remoteTipIs(url, branch, commit, timeoutMs = 20_000) {
219
+ if (!str(url) || url.startsWith("-") || !okBranch(branch) || !COMMIT_RE.test(commit))
220
+ return Promise.resolve(null);
221
+ let cwd;
222
+ try {
223
+ cwd = mkdtempSync(join(tmpdir(), "pantheon-ls-remote-"));
224
+ }
225
+ catch {
226
+ return Promise.resolve(null);
227
+ }
228
+ const base = { ...process.env, ...proxyEnvFor(), GIT_TERMINAL_PROMPT: "0", GIT_CEILING_DIRECTORIES: tmpdir() };
229
+ const env = base.GIT_SSH_COMMAND || base.GIT_SSH || ownSshCommand(cwd, base) ? base : { ...base, GIT_SSH_COMMAND: "ssh -o BatchMode=yes" };
230
+ const args = ["-c", "core.fsmonitor=false", "-c", "protocol.file.allow=user", "ls-remote", "--", url, `refs/heads/${branch}`];
231
+ return new Promise((res) => {
232
+ execFile("git", args, { cwd, env, encoding: "utf8", windowsHide: true, timeout: timeoutMs, maxBuffer: 1 << 20 }, (err, out) => {
233
+ try {
234
+ rmSync(cwd, { recursive: true, force: true });
235
+ }
236
+ catch { }
237
+ if (err) {
238
+ res(null);
239
+ return;
240
+ }
241
+ const tip = (out.split("\n").find((l) => l.endsWith(`\trefs/heads/${branch}`)) ?? "").split("\t")[0].toLowerCase();
242
+ const c = commit.toLowerCase();
243
+ res(!!tip && (tip.startsWith(c) || c.startsWith(tip)));
244
+ });
245
+ });
246
+ }
247
+ //# sourceMappingURL=held-delivery.js.map
@@ -20,6 +20,8 @@ export const PANTHEON_HOME_ENTRIES = [
20
20
  { name: "readiness-recheck.stamp", what: "the mark that has the worker look again for an agent it can run" },
21
21
  { name: "worker-state.json", what: "the worker's last state" },
22
22
  { name: "worker-run.json", what: "the worker's record of a run in flight" },
23
+ { name: "held-deliveries.json", what: "deliveries woken runs held until their branch was on origin (pantheon leave and uninstall name any not yet sent)" },
24
+ { name: /^held-deliveries\.json\.corrupt-\d+$/, sample: "held-deliveries.json.corrupt-1760000000000", what: "a record of held deliveries that could not be read, set aside" },
23
25
  { name: "run-slot.json", what: "the worker's run slot" },
24
26
  { name: "worker.pid", what: "the worker's pid file" },
25
27
  { name: /^notifier-state(-.+)?\.json$/, sample: "notifier-state-team%2Fone.json", what: "the alerts' state" },
package/dist/machine.js CHANGED
@@ -773,6 +773,7 @@ export function removeHomeLeftovers(home, keep, opts = {}) {
773
773
  ["worker-policy.json", "the worker's wake policy (who may wake it, and self-upgrade) — a rejoin starts from no grants"],
774
774
  ["profile.json", "your profile (name, title, what you decide)"],
775
775
  ["spend.json", "the spend ledger"],
776
+ ["held-deliveries.json", "the deliveries woken runs held, not yet sent (named above)"],
776
777
  ["worker.pid", "the worker's pid file"],
777
778
  ["bin", "the menu bar's and alerts' files (the .command scripts, pantheon-hud, pantheon-tray)"],
778
779
  ];