@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/dist/worker.js CHANGED
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
3
3
  import { chmodSync, lstatSync, mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
- import { absentDeniedPaths, detectClaudeFeatures, describeFeatures, ensureRunTmp, NO_CLAUDE_FEATURES, removeSandboxLeftovers, runTmpDir } from "./claude-confine.js";
6
+ import { absentDeniedPaths, describeKept, ownWorktreeConfig, detectClaudeFeatures, describeFeatures, ensureRunTmp, leavesSandboxMounts, NO_CLAUDE_FEATURES, removeSandboxLeftovers, runTmpDir } from "./claude-confine.js";
7
7
  import { snapshotHome, restoreHome, describeChange, trustWrite, askedHere } from "./home-guard.js";
8
8
  import { PantheonClient } from "./client.js";
9
9
  import { createHash, randomBytes } from "node:crypto";
@@ -12,9 +12,10 @@ import { loadPins, pinScope, savePins, teamPinId, writeWhole } from "./pins.js";
12
12
  import { answeredIn, markAnswered, noteNonce } from "./nonces.js";
13
13
  import { brokenStateFiles, brokenStateFix, pidAlive, readStateFile, withFileLock } from "./file-lock.js";
14
14
  import { mkdirInside, writeInside } from "./safe-write.js";
15
+ import { corruptHeldFiles, MAX_HELD_PER_RUN, remoteTipIs, takeHeldFrames, updateHeldState, verifyHeldFrame, writeHoldConfig } from "./held-delivery.js";
15
16
  import { adoptedCommon, adoptWorktree, gitCallEnv, lfsInUse, repoHooksArgs, safeGitArgs, WorktreeTampered } from "./git-safe.js";
16
17
  import { trusted } from "./verify-act.js";
17
- import { headlessSpawn, materializeSpawnFiles, chooseAgent, agentById, describeChoice, unattendedRefusal, SANDBOXED_FOR_UNATTENDED, NOT_FOR_UNATTENDED_RUNS, claudeSandboxable, unattendedReadiness, anyUnattendedAgentOnPath, forgetMissingBinaries, quarantineRepoAgentConfig, AGENT_EDIT_SUFFIX, REPO_AGENT_CONFIG, COMMAND_GRANT_SCOPE, repoCommands, REPO_COMMANDS_FILE, claudeCommandRules, } from "./agents.js";
18
+ import { headlessSpawn, materializeSpawnFiles, chooseAgent, agentById, describeChoice, unattendedRefusal, unattendedRefusalOf, SIGN_IN_CHECK, SANDBOXED_FOR_UNATTENDED, NOT_FOR_UNATTENDED_RUNS, claudeSandboxable, unattendedReadiness, anyUnattendedAgentOnPath, forgetMissingBinaries, quarantineRepoAgentConfig, AGENT_EDIT_SUFFIX, REPO_AGENT_CONFIG, COMMAND_GRANT_SCOPE, repoCommands, REPO_COMMANDS_FILE, claudeCommandRules, } from "./agents.js";
18
19
  import { activeOutages, detectOutage, earliest, whenAgain } from "./agent-outage.js";
19
20
  import { agentSpawn, CmdLineUnsafe } from "./which.js";
20
21
  import { VERSION } from "./version.js";
@@ -758,8 +759,8 @@ export function deliveryStep(branch, canPush, ref) {
758
759
  return canPush
759
760
  ? `If you wrote code: commit it on this branch, push it (git push origin ${branch} — this branch, never any other), then
760
761
  pantheon_send DELIVERED with${r} branch ${branch} and the commit sha, so your human and your peer can review it.
761
- If the push is refused, send the DELIVERED anyway: the worker pushes this branch after you finish, and corrects
762
- the DELIVERED on the ledger if it cannot.`
762
+ If the push is refused, send the DELIVERED anyway: Pantheon holds it until this branch is on origin — the worker
763
+ pushes it after you finish and sends your DELIVERED then, or tells your peer why it could not.`
763
764
  : `This run cannot commit or push, so never send DELIVERED — your peer could not fetch it. If you wrote code,
764
765
  pantheon_send NOTE with${r || " a ref,"} saying what you changed and that it waits on branch ${branch} on this machine
765
766
  for your human to review and push. If the request asked for code, that NOTE is your reply.`;
@@ -1005,6 +1006,8 @@ export function runInFlight(dir, now = Date.now()) {
1005
1006
  return null;
1006
1007
  return { pid: r.pid, at: r.at };
1007
1008
  }
1009
+ const HELD_BACKOFF_MS = 60_000;
1010
+ const HELD_BACKOFF_MAX_MS = 30 * 60_000;
1008
1011
  export const EXIT_GRACE_MS = 3_000;
1009
1012
  const CONTINUATION_HORIZON_MS = 24 * 60 * 60_000;
1010
1013
  const HELD_CONTINUATIONS_MAX = 20;
@@ -1163,7 +1166,7 @@ export class Worker {
1163
1166
  if (state === "idle" && this.readiness && !this.readiness.ok) {
1164
1167
  state = "unavailable";
1165
1168
  detail = "can't take requests yet";
1166
- more = { ...(this.readiness.code ? { reason: this.readiness.code } : {}) };
1169
+ more = { ...(this.readiness.code ? { reason: this.readiness.pinned ? (this.readiness.code === "kiro_only" ? "pinned_kiro" : this.readiness.code === "unsandboxed_off" ? "pinned_unsandboxed" : this.readiness.code) : this.readiness.code } : {}) };
1167
1170
  }
1168
1171
  this.lastReport = { state, ...(detail ? { detail } : {}), ...(until ? { until } : {}), ...more, at: Date.now() };
1169
1172
  this.client?.reportWorkerState(state, detail, until, this.idleMs(), more);
@@ -1206,7 +1209,7 @@ export class Worker {
1206
1209
  this.opts.log(`! teammates are told this machine can't take requests yet: ${r.short} — ${r.fix}`);
1207
1210
  if (r.ok && this.readiness && !this.readiness.ok)
1208
1211
  this.opts.log(` agent ready again — teammates are told this machine takes requests`);
1209
- this.readiness = { ok: r.ok, short: r.short, ...(r.code ? { code: r.code } : {}) };
1212
+ this.readiness = { ok: r.ok, short: r.short, ...(r.code ? { code: r.code } : {}), ...(r.pinned ? { pinned: true } : {}) };
1210
1213
  }
1211
1214
  catch {
1212
1215
  this.readiness = null;
@@ -1891,11 +1894,13 @@ export class Worker {
1891
1894
  mkdirSync(dir, { recursive: true, mode: 0o700 });
1892
1895
  const file = join(dir, "config.json");
1893
1896
  let written = cfg;
1897
+ this.runKey = null;
1894
1898
  if (features.includes("delegate") && cfg.privateKey) {
1895
1899
  const run = newKeypair();
1896
1900
  const team = cfg.spaceId ?? this.client?.teamKey() ?? cfg.space;
1897
1901
  const expires = Date.now() + limitMs + 10 * 60_000;
1898
1902
  const cert = signDelegate(cfg.privateKey, team, cfg.member, run.publicKey, randomBytes(8).toString("hex"), expires);
1903
+ this.runKey = { runId: cert.run, runPub: cert.pub };
1899
1904
  written = { ...cfg, privateKey: "", delegate: { privateKey: run.privateKey, cert }, ...(cfg.spaceId ? {} : { spaceId: team }) };
1900
1905
  }
1901
1906
  else if (features.includes("act-sig") || this.client?.signaturesExpected()) {
@@ -1914,6 +1919,7 @@ export class Worker {
1914
1919
  catch { }
1915
1920
  return dir;
1916
1921
  }
1922
+ runKey = null;
1917
1923
  cleanupStep(what, step) {
1918
1924
  try {
1919
1925
  step();
@@ -2067,6 +2073,7 @@ export class Worker {
2067
2073
  this.opts.log(` ! ${p.name}: ${w}`);
2068
2074
  }
2069
2075
  this.recoverInterruptedRun();
2076
+ await this.settleLeftHeld(client);
2070
2077
  this.reportStanding(true);
2071
2078
  if (this.opts.continuations !== false)
2072
2079
  await this.restoreHeldContinuations(client);
@@ -2094,6 +2101,9 @@ export class Worker {
2094
2101
  await this.maintain(client, arrived);
2095
2102
  if (this.running)
2096
2103
  continue;
2104
+ this.cleanPendingSandbox();
2105
+ if (this.heldLeft && Date.now() >= this.heldRetryAt)
2106
+ await this.settleLeftHeld(client);
2097
2107
  if (fleetLock && fleetLock !== this) {
2098
2108
  this.writeState("watching", `waiting: a run for "${fleetLock.cfg.space}" is in progress`);
2099
2109
  this.tickHold = { reason: "busy_elsewhere", detail: "another team's run is in progress on this machine" };
@@ -2502,7 +2512,7 @@ export class Worker {
2502
2512
  liveRuns.add(pid);
2503
2513
  installRunReaper();
2504
2514
  this.attemptPid = pid;
2505
- writeRunRecord(this.stateDir(), { pid, member: this.cfg.member, space: this.cfg.space, at: Date.now(), worker: process.pid, obligation: attempt.obligation, ...(attempt.spend ? { spend: attempt.spend } : {}), ...(attempt.limit ? { limit: attempt.limit } : {}) });
2515
+ writeRunRecord(this.stateDir(), { pid, member: this.cfg.member, space: this.cfg.space, at: Date.now(), worker: process.pid, obligation: attempt.obligation, ...(attempt.spend ? { spend: attempt.spend } : {}), ...(attempt.limit ? { limit: attempt.limit } : {}), ...(attempt.sandbox ? { sandbox: attempt.sandbox } : {}) });
2506
2516
  }
2507
2517
  untrackRun(pid) {
2508
2518
  this.runPids.delete(pid);
@@ -2530,6 +2540,11 @@ export class Worker {
2530
2540
  return;
2531
2541
  if (Number.isInteger(r.pid) && typeof r.at === "number" && Date.now() - r.at <= recordLimitMs(r) && recordedRunAlive(r.pid))
2532
2542
  killRunTree(r.pid);
2543
+ const sb = r.sandbox;
2544
+ if (sb && typeof sb.common === "string" && Array.isArray(sb.absent)) {
2545
+ this.sandboxPending = { pid: Number.isInteger(r.pid) ? r.pid : 0, common: sb.common, absent: sb.absent.filter((x) => typeof x === "string") };
2546
+ this.cleanPendingSandbox();
2547
+ }
2533
2548
  try {
2534
2549
  rmSync(f, { force: true });
2535
2550
  }
@@ -2552,6 +2567,21 @@ export class Worker {
2552
2567
  stateDir() {
2553
2568
  return this.opts.home ?? process.env.PANTHEON_HOME ?? join(homedir(), ".pantheon");
2554
2569
  }
2570
+ sandboxPending = null;
2571
+ cleanPendingSandbox() {
2572
+ const p = this.sandboxPending;
2573
+ if (!p)
2574
+ return;
2575
+ if (p.pid > 0 && recordedRunAlive(p.pid))
2576
+ return;
2577
+ this.sandboxPending = null;
2578
+ try {
2579
+ const gone = removeSandboxLeftovers(p.absent, p.common);
2580
+ if (gone.length)
2581
+ this.opts.log(` confine removed ${gone.length} empty mount point${gone.length === 1 ? "" : "s"} the interrupted run's sandbox left: ${gone.map((g) => basename(g)).join(", ")}`);
2582
+ }
2583
+ catch { }
2584
+ }
2555
2585
  brokenSaid = "";
2556
2586
  sayBrokenState(broken) {
2557
2587
  const key = broken.join(",");
@@ -2720,16 +2750,19 @@ export class Worker {
2720
2750
  }
2721
2751
  return { obligation: o, branch, worktree, ok: false, refused: true, detail: diagnosis.says, diagnosis, waitUntil: choice.outage.until };
2722
2752
  }
2723
- const refused = unattendedRefusal(choice, { unsandboxedOk: this.unsandboxedOk() });
2724
- if (refused) {
2725
- const noSandbox = !SANDBOXED_FOR_UNATTENDED.has(choice.backend.id) && !NOT_FOR_UNATTENDED_RUNS.has(choice.backend.id);
2753
+ const refusal = unattendedRefusalOf(choice, { unsandboxedOk: this.unsandboxedOk() });
2754
+ if (refusal) {
2755
+ const refused = refusal.says;
2756
+ const noSandbox = refusal.cause === "unsandboxed_off";
2726
2757
  const diagnosis = noSandbox
2727
2758
  ? this.noSandboxDiagnosis(choice)
2728
- : {
2729
- cause: "no_unattended_agent",
2730
- says: `Teammates' requests wait for your next session here: ${refused}.`,
2731
- fix: "Install and sign in to another agent CLI (claude, codex, cursor-agent or copilot) for woken runs; until then requests wait for a live session.",
2732
- };
2759
+ : refusal.cause === "not_signed_in"
2760
+ ? { cause: "not_signed_in", says: `${refused}.`, fix: `Sign in once and requests are answered while you're away: ${SIGN_IN_CHECK[choice.backend.id].fix}` }
2761
+ : {
2762
+ cause: "no_unattended_agent",
2763
+ says: `Teammates' requests wait for your next session here: ${refused}.`,
2764
+ fix: "Install and sign in to another agent CLI (claude, codex, cursor-agent or copilot) for woken runs; until then requests wait for a live session.",
2765
+ };
2733
2766
  if (noSandbox && !this.parkedNoSandbox.has(o.id))
2734
2767
  this.parkedNoSandbox.set(o.id, { at: Date.now(), ...(policy.agent ? { agent: policy.agent } : {}) });
2735
2768
  if (!this.skipLogged.has(`refused:${o.id}`)) {
@@ -2849,6 +2882,8 @@ export class Worker {
2849
2882
  let bashRules = [];
2850
2883
  let fenced = null;
2851
2884
  let sandboxAbsent = [];
2885
+ let sandboxCommon = "";
2886
+ let heldRun = null;
2852
2887
  if (!client.features.includes("delegate") && client.signaturesExpected()) {
2853
2888
  const says = "this team's relay signs messages but did not offer a key for the run, so the run would have needed your own key — it was not started";
2854
2889
  this.opts.log(`! ${says}`);
@@ -2876,6 +2911,33 @@ export class Worker {
2876
2911
  for (const l of fenced.recovered.lines)
2877
2912
  this.opts.log(` config ${l}`);
2878
2913
  }
2914
+ {
2915
+ let url = null;
2916
+ if (canPush && this.runKey) {
2917
+ try {
2918
+ url = await gitOut(worktree, ["remote", "get-url", "origin"]);
2919
+ }
2920
+ catch {
2921
+ url = null;
2922
+ }
2923
+ }
2924
+ heldRun = url && this.runKey ? { team: this.pinTeam, runHome, ...this.runKey, obligation: o.id, ref: o.ref ?? null, goal: o.goal ?? null, branch, worktree, origin: url, at: Date.now() } : null;
2925
+ try {
2926
+ if (heldRun) {
2927
+ const r = heldRun;
2928
+ this.heldState((st) => { st.runs = [...st.runs.filter((x) => !(x.team === r.team && x.obligation === r.obligation)), r]; });
2929
+ }
2930
+ writeHoldConfig(runHome, heldRun ? { origin: heldRun.origin, branch } : null);
2931
+ }
2932
+ catch (e) {
2933
+ heldRun = null;
2934
+ try {
2935
+ writeHoldConfig(runHome, null);
2936
+ }
2937
+ catch { }
2938
+ this.opts.log(`! could not set up holding this run's DELIVERED until it is pushed: ${e.message}`);
2939
+ }
2940
+ }
2879
2941
  const backend = this.opts.agentCmd ? { id: "claude-code", label: "Claude Code", binaries: ["claude"] } : choice.backend;
2880
2942
  const personHome = this.opts.personHome ?? homedir();
2881
2943
  if (features && allowBash && !ensureRunTmp(runTmpDir(runHome))) {
@@ -2888,16 +2950,21 @@ export class Worker {
2888
2950
  ...(features ? { claude: { features, pantheonHome: this.stateDir(), home: personHome, userSettings: join(personHome, ".claude", "settings.json"), ...(() => { const c = adoptedCommon(worktree); return c ? { gitCommon: c } : {}; })() } } : {}),
2889
2951
  });
2890
2952
  plannedFiles = plan.writeFiles;
2891
- {
2953
+ if (leavesSandboxMounts({ os: process.platform, agent: backend.id, sandbox: !!plan.osSandbox })) {
2892
2954
  const gc = adoptedCommon(worktree);
2893
2955
  if (gc) {
2894
2956
  try {
2895
2957
  sandboxAbsent = absentDeniedPaths(gc);
2958
+ sandboxCommon = gc;
2959
+ const own = ownWorktreeConfig(worktree, gc);
2960
+ if (own && !sandboxAbsent.includes(own))
2961
+ sandboxAbsent = [own, ...sandboxAbsent];
2896
2962
  }
2897
2963
  catch {
2898
2964
  sandboxAbsent = [];
2899
2965
  }
2900
2966
  }
2967
+ this.opts.log(` confine Linux sandbox: watching ${sandboxAbsent.length} path${sandboxAbsent.length === 1 ? "" : "s"} it may leave behind${gc ? ` in ${gc}` : " (no common git dir known — none)"}`);
2901
2968
  }
2902
2969
  for (const n of plan.notes ?? [])
2903
2970
  if (!this.notesSaid.has(n)) {
@@ -2932,6 +2999,12 @@ export class Worker {
2932
2999
  for (const l of fenced.restore().lines)
2933
3000
  this.opts.log(` config ${l}`); });
2934
3001
  this.cleanupStep("sealing the run home", () => this.sealRunHome(runHome));
3002
+ this.cleanupStep("taking back the delivery hold", () => {
3003
+ writeHoldConfig(runHome, null);
3004
+ const r = heldRun;
3005
+ if (r)
3006
+ this.heldState((st) => { st.runs = st.runs.filter((x) => !(x.team === r.team && x.obligation === r.obligation)); });
3007
+ });
2935
3008
  const detail = `could not plan the run: ${e.message}`;
2936
3009
  this.opts.log(`! ${detail}`);
2937
3010
  recordRun({ at: Date.now(), goal: o.goal, obligation: o.id, ok: false, triggeredBy: (await this.actOf(client, o.ref))?.from ?? "unknown", summary: clip(o.body), space: this.spaceKey(), member: this.cfg.member }, this.opts.home);
@@ -2961,7 +3034,7 @@ export class Worker {
2961
3034
  }
2962
3035
  const pid = child.pid;
2963
3036
  if (pid)
2964
- this.trackRun(pid, { obligation: o.id, spend: spendId, limit: runLimit.ms });
3037
+ this.trackRun(pid, { obligation: o.id, spend: spendId, limit: runLimit.ms, ...(sandboxAbsent.length ? { sandbox: { common: sandboxCommon, absent: sandboxAbsent } } : {}) });
2965
3038
  let head = "", tail = "";
2966
3039
  const take = (b) => {
2967
3040
  const s = b.toString();
@@ -3015,14 +3088,17 @@ export class Worker {
3015
3088
  }
3016
3089
  finally {
3017
3090
  this.cleanupStep("removing the sandbox's empty mount points", () => {
3018
- const gone = removeSandboxLeftovers(sandboxAbsent);
3091
+ const gone = sandboxAbsent.length ? removeSandboxLeftovers(sandboxAbsent, sandboxCommon) : [];
3019
3092
  if (gone.length)
3020
3093
  this.opts.log(` confine removed ${gone.length} empty mount point${gone.length === 1 ? "" : "s"} the sandbox left: ${gone.map((g) => basename(g)).join(", ")}`);
3094
+ for (const k of describeKept(sandboxAbsent.filter((p) => !gone.includes(p))))
3095
+ this.opts.log(` confine kept ${k} — not bubblewrap's empty mount point`);
3021
3096
  });
3022
3097
  this.cleanupStep("putting the repo's agent config back", () => { if (fenced)
3023
3098
  for (const l of fenced.restore().lines)
3024
3099
  this.opts.log(` config ${l}`); });
3025
3100
  this.cleanupStep("sealing the run home", () => this.sealRunHome(runHome));
3101
+ this.cleanupStep("taking back the delivery hold", () => writeHoldConfig(runHome, null));
3026
3102
  this.cleanupStep("checking Pantheon's own files", () => this.restoreHomeAfterRun(homeBefore, o));
3027
3103
  }
3028
3104
  const logRoot = snap ? scratch : worktree;
@@ -3044,14 +3120,17 @@ export class Worker {
3044
3120
  }
3045
3121
  this.writeRunLog(logRoot, runLog, `obligation ${o.id}\nbranch ${branch}\n\n${detail}\n${extra}`);
3046
3122
  const exited = /^exit 0/.test(detail);
3047
- const sent = client.mineSince(startedAt, o.goal);
3123
+ const held = await this.settleHeld(client, heldRun ? { run: heldRun, canPush: canPush && !massDelete, massDelete } : null);
3124
+ const settled = [...held.sent, ...held.waiting];
3125
+ const settledIds = new Set(settled.map((a) => a.id));
3126
+ const sent = [...client.mineSince(startedAt, o.goal).filter((a) => !settledIds.has(a.id)), ...settled.filter((a) => o.goal == null || a.goal === o.goal)];
3048
3127
  const repliedWith = sent.find((a) => ["DELIVERED", "ANSWER", "ESCALATE", "ACCEPT", "OBJECT", "PROPOSE", "CONTRACT", "CLOSE", "WITHDRAW"].includes(a.act) || (a.act === "NOTE" && !!a.ref));
3049
3128
  const replied = requireReply ? client.repliedTo(o) || !!repliedWith : true;
3050
3129
  if (requireReply && replied && !client.repliedTo(o) && repliedWith) {
3051
3130
  this.opts.log(` reply ${repliedWith.act} ${repliedWith.id} answers ${o.id}, though the relay still lists it as owed${repliedWith.ref ? "" : " (the act named no ref)"}`);
3052
3131
  }
3053
3132
  const ok = exited && replied;
3054
- await this.checkDeliveries(client, sent, { branch, worktree, canPush: canPush && !massDelete, goal: o.goal ?? null });
3133
+ await this.checkDeliveries(client, sent.filter((a) => !settledIds.has(a.id)), { branch, worktree, canPush: canPush && !massDelete, goal: o.goal ?? null });
3055
3134
  this.runProject = null;
3056
3135
  if (snap) {
3057
3136
  const ours = new Set((plannedFiles ?? []).map((f) => (isAbsolute(f.path) ? relative(worktree, f.path) : f.path).split("\\").join("/")));
@@ -3170,6 +3249,298 @@ export class Worker {
3170
3249
  catch { }
3171
3250
  }
3172
3251
  }
3252
+ async pushRunBranch(worktree, br, commit) {
3253
+ if (!validBase(br) || (await gitOut(worktree, ["merge-base", "--is-ancestor", commit, `refs/heads/${br}`])) === null)
3254
+ return false;
3255
+ const r = await gitOut(worktree, ["push", "--quiet", "origin", `refs/heads/${br}:refs/heads/${br}`], FETCH_TIMEOUT_MS);
3256
+ return r !== null && await onRemote(worktree, br, commit);
3257
+ }
3258
+ heldLeft = false;
3259
+ heldRetryAt = 0;
3260
+ heldCorruptSaid = new Set();
3261
+ heldState(fn) {
3262
+ const { result, corrupt } = updateHeldState(this.stateDir(), fn);
3263
+ if (corrupt)
3264
+ this.sayHeldCorrupt(corrupt);
3265
+ return result;
3266
+ }
3267
+ sayHeldCorrupt(file) {
3268
+ if (this.heldCorruptSaid.has(file))
3269
+ return;
3270
+ this.heldCorruptSaid.add(file);
3271
+ this.opts.log(`! the record of held deliveries could not be read — set aside as ${file}`);
3272
+ try {
3273
+ this.opts.notify?.({ kind: "worker", who: "Your worker", heading: "Held deliveries could not be read", context: this.cfg.space,
3274
+ body: `A record of deliveries a woken run held until their branch was pushed could not be read, so they were not sent. It is kept at ${file}. Check with your teammates whether they got the work.`, ttl: 0, sound: true });
3275
+ }
3276
+ catch { }
3277
+ }
3278
+ async originHas(worktree, branch, commit) {
3279
+ if (!validBase(branch))
3280
+ return false;
3281
+ let line;
3282
+ try {
3283
+ line = await gitOut(worktree, ["ls-remote", "origin", `refs/heads/${branch}`], FETCH_TIMEOUT_MS);
3284
+ }
3285
+ catch {
3286
+ return null;
3287
+ }
3288
+ if (line === null)
3289
+ return null;
3290
+ const tip = line.split(/\s+/)[0] ?? "";
3291
+ if (!tip)
3292
+ return false;
3293
+ if (tip.startsWith(commit.toLowerCase()) || commit.toLowerCase().startsWith(tip))
3294
+ return true;
3295
+ try {
3296
+ return (await gitOut(worktree, ["merge-base", "--is-ancestor", commit, tip])) !== null;
3297
+ }
3298
+ catch {
3299
+ return false;
3300
+ }
3301
+ }
3302
+ async settleHeld(client, live) {
3303
+ const team = this.pinTeam;
3304
+ const sent = [], waiting = [];
3305
+ const registered = this.heldState((st) => st.runs.filter((r) => r.team === team));
3306
+ const runs = live ? [live.run, ...registered.filter((r) => r.runHome === live.run.runHome && r.obligation !== live.run.obligation)] : registered;
3307
+ const homes = [...new Set(runs.map((r) => r.runHome))];
3308
+ for (const home of homes) {
3309
+ try {
3310
+ const { corrupt, dropped } = takeHeldFrames(home, (frames) => {
3311
+ const add = [];
3312
+ for (const h of frames) {
3313
+ const teams = [this.cfg.spaceId, client.teamKey(), this.cfg.space];
3314
+ const r = runs.find((x) => x.runHome === home && verifyHeldFrame(h.frame, { member: this.cfg.member, memberPub: this.cfg.publicKey, teams, runId: x.runId, runPub: x.runPub }));
3315
+ if (!r) {
3316
+ this.opts.log(`! dropped a held DELIVERED that is not signed by a run of this worker's — nothing is sent, pushed or answered for it`);
3317
+ continue;
3318
+ }
3319
+ add.push({ ...h, team, obligation: r.obligation, ref: r.ref, goal: r.goal, branch: r.branch, worktree: r.worktree, origin: r.origin, tries: 0, firstTry: Date.now() });
3320
+ }
3321
+ this.heldState((st) => {
3322
+ st.held = [...st.held.filter((x) => !add.some((a) => a.frame.id === x.frame.id)), ...add];
3323
+ st.runs = st.runs.filter((x) => !(x.team === team && x.runHome === home));
3324
+ });
3325
+ });
3326
+ if (corrupt)
3327
+ this.sayHeldCorrupt(corrupt);
3328
+ if (dropped)
3329
+ this.opts.log(`! a run held more than ${MAX_HELD_PER_RUN} deliveries — only its newest ${MAX_HELD_PER_RUN} are kept, ${dropped} dropped unsent`);
3330
+ for (const f of corruptHeldFiles(home))
3331
+ this.sayHeldCorrupt(f);
3332
+ }
3333
+ catch (e) {
3334
+ this.opts.log(`! could not read what the run held: ${e.message}`);
3335
+ }
3336
+ }
3337
+ const all = this.heldState((st) => st.held.filter((x) => x.team === team)).sort((a, b) => a.at - b.at);
3338
+ if (!all.length) {
3339
+ this.heldLeft = false;
3340
+ return { sent, waiting };
3341
+ }
3342
+ const drop = (e) => this.heldState((st) => { st.held = st.held.filter((x) => x.frame.id !== e.frame.id); });
3343
+ const keep = (e, why) => this.heldState((st) => {
3344
+ const x = st.held.find((y) => y.frame.id === e.frame.id);
3345
+ if (x) {
3346
+ x.tries += 1;
3347
+ x.nextTry = Date.now() + Math.min(HELD_BACKOFF_MAX_MS, HELD_BACKOFF_MS * 2 ** Math.min(x.tries - 1, 20));
3348
+ if (why)
3349
+ x.why = why;
3350
+ }
3351
+ });
3352
+ const rec = (r, a) => ({ id: r.id, seq: r.seq, ts: Date.now(), space: this.cfg.space, from: this.cfg.member, to: a.to ?? null, goal: a.goal ?? null, act: a.act, body: a.body, ref: a.ref ?? null, ...(a.meta ? { meta: a.meta } : {}) });
3353
+ const codeOf = (e) => /^([A-Z][A-Z_]{2,40}):/.exec(e.message)?.[1] ?? null;
3354
+ const transient = (e) => !client.connected || /not connected to relay|did not answer/.test(e.message)
3355
+ || ["RATE_LIMIT", "SPACE_FULL", "STALE_VIEW", "SPACE_UNREADABLE", "TOO_MANY_REFUSALS"].includes(codeOf(e) ?? "") || /^STORAGE/.test(codeOf(e) ?? "");
3356
+ const note = async (e, summary, body) => {
3357
+ const asker = e.ref ? (await this.actOf(client, e.ref))?.from : undefined;
3358
+ const to = asker && asker !== this.cfg.member ? [asker] : undefined;
3359
+ for (const where of [{ goal: e.goal, ref: e.ref, ...(to ? { to } : {}) }, ...(to ? [{ to }] : []), {}]) {
3360
+ const a = { act: "NOTE", summary: clip(summary, 190), body, ...where };
3361
+ try {
3362
+ const r = await client.send(a);
3363
+ return rec(r, a);
3364
+ }
3365
+ catch (err) {
3366
+ if (transient(err))
3367
+ return "later";
3368
+ this.opts.log(` the note about held DELIVERED ${e.frame.id} was refused (${codeOf(err) ?? "refused"}) — trying it more plainly`);
3369
+ }
3370
+ }
3371
+ return null;
3372
+ };
3373
+ const tell = (heading, body, e, branch) => {
3374
+ try {
3375
+ this.opts.notify?.({ kind: "worker", who: "Your worker", heading, context: this.cfg.space, body, ttl: 0, sound: false,
3376
+ ...(e && branch ? { fix: { label: "Push it", dir: e.worktree, lines: [`git push origin ${branch}`], why: `the run's delivery of ${branch} is not on origin` } } : {}) });
3377
+ }
3378
+ catch { }
3379
+ };
3380
+ const GIVE_UP_MS = 24 * 3_600_000;
3381
+ const inGoal = new Map();
3382
+ const mineInGoal = async (goal) => {
3383
+ if (!goal)
3384
+ return [];
3385
+ if (!inGoal.has(goal)) {
3386
+ let acts = [];
3387
+ try {
3388
+ const res = await signedRead(this.cfg, "/history", { goal }, { timeoutMs: 10_000 });
3389
+ if (res.ok)
3390
+ acts = (await res.json()).acts ?? [];
3391
+ }
3392
+ catch { }
3393
+ inGoal.set(goal, acts.filter((a) => a.goal === goal && a.from === this.cfg.member && a.act === "DELIVERED"));
3394
+ }
3395
+ return inGoal.get(goal);
3396
+ };
3397
+ for (const e of all) {
3398
+ const t = { branch: e.frame.meta.branch, commit: e.frame.meta.commit };
3399
+ const ours = !!live && e.obligation === live.run.obligation && e.worktree === live.run.worktree;
3400
+ const pending = rec({ id: e.frame.id, seq: 0 }, e.frame);
3401
+ const later = (why) => {
3402
+ if (Date.now() - e.firstTry > GIVE_UP_MS) {
3403
+ drop(e);
3404
+ this.opts.log(`! gave up on the DELIVERED the run held for ${t.branch} at ${t.commit.slice(0, 7)}: it could not be settled in a day (${e.tries + 1} tries)`);
3405
+ tell("A delivery could not be sent", `A woken run finished work on ${t.branch} at ${t.commit.slice(0, 7)}, but Pantheon could not reach the relay or origin to tell your teammate, and has stopped trying. Tell them yourself where the work is.`);
3406
+ return;
3407
+ }
3408
+ keep(e, why);
3409
+ if (ours)
3410
+ waiting.push(pending);
3411
+ };
3412
+ if (!ours && (e.nextTry ?? 0) > Date.now())
3413
+ continue;
3414
+ if (all.some((x) => x !== e && x.obligation === e.obligation && x.at > e.at)) {
3415
+ this.opts.log(` dropped an earlier DELIVERED the run held for ${t.branch} at ${t.commit.slice(0, 7)}: the same run held a later one`);
3416
+ drop(e);
3417
+ continue;
3418
+ }
3419
+ const sameAs = (r) => r.act === "DELIVERED"
3420
+ && (e.frame.ref ? r.ref === e.frame.ref : r.goal === e.frame.goal && r.meta?.branch === t.branch);
3421
+ const signedAt = e.frame.sig?.ts ?? e.at;
3422
+ const laterSent = [...client.mineSince(e.at), ...await mineInGoal(e.frame.goal ?? null)]
3423
+ .some((r) => r.id !== e.frame.id && (r.sig?.ts ?? r.ts) > signedAt && sameAs(r));
3424
+ if (all.some((x) => x !== e && x.at > e.at && sameAs(x.frame)) || laterSent) {
3425
+ this.opts.log(` dropped the DELIVERED the run held for ${t.branch} at ${t.commit.slice(0, 7)}: a later DELIVERED for the same request went since`);
3426
+ drop(e);
3427
+ continue;
3428
+ }
3429
+ let there = ours || existsSync(e.worktree)
3430
+ ? await this.originHas(e.worktree, t.branch, t.commit)
3431
+ : await remoteTipIs(e.origin, t.branch, t.commit, FETCH_TIMEOUT_MS);
3432
+ let why = null;
3433
+ if (there === null && !ours) {
3434
+ later();
3435
+ continue;
3436
+ }
3437
+ if (!there) {
3438
+ if (e.why)
3439
+ why = e.why;
3440
+ else if (!ours)
3441
+ why = "stopped";
3442
+ else if (t.branch !== e.branch)
3443
+ why = "not_own_branch";
3444
+ else if (!live.canPush)
3445
+ why = live.massDelete ? "mass_delete" : "not_allowed";
3446
+ else {
3447
+ there = await this.pushRunBranch(e.worktree, e.branch, t.commit).catch(() => false);
3448
+ if (there)
3449
+ this.opts.log(`* pushed ${e.branch} for the run: the DELIVERED it held named ${t.commit.slice(0, 7)}, which origin did not have — repo hooks not run on Pantheon's push${lfsInUse(e.worktree) ? "; LFS objects uploaded" : ""}`);
3450
+ else
3451
+ why = "push_refused";
3452
+ }
3453
+ }
3454
+ if (there) {
3455
+ try {
3456
+ const { after_seq: _seen, ...frame } = e.frame;
3457
+ const r = await client.postAct(frame);
3458
+ this.opts.log(`* sent the DELIVERED ${r.id} the run held: ${t.branch} at ${t.commit.slice(0, 7)} is on origin now`);
3459
+ sent.push(rec(r, e.frame));
3460
+ drop(e);
3461
+ if (e.ref)
3462
+ this.markDone(`handled:${e.ref}`);
3463
+ continue;
3464
+ }
3465
+ catch (err) {
3466
+ if (transient(err)) {
3467
+ this.opts.log(` could not send the DELIVERED the run held yet (${codeOf(err) ?? "the relay is out of reach"}) — it is tried again`);
3468
+ later();
3469
+ continue;
3470
+ }
3471
+ if (codeOf(err) === "ID_TAKEN") {
3472
+ this.opts.log(` the DELIVERED ${e.frame.id} the run held is on the ledger already`);
3473
+ sent.push(pending);
3474
+ drop(e);
3475
+ if (e.ref)
3476
+ this.markDone(`handled:${e.ref}`);
3477
+ continue;
3478
+ }
3479
+ const code = codeOf(err);
3480
+ const because = code === "BAD_ACT_SIGNATURE" ? "the run's key had expired" : code === "GOAL_CLOSED" ? "the goal was closed" : code === "NOT_IN_GOAL" ? "the goal no longer includes them" : "the relay would not take it now";
3481
+ this.opts.log(`! the relay refused the DELIVERED the run held (${code ?? "refused"}) — a NOTE says the branch is on origin instead`);
3482
+ const n = await note(e, `The work is on origin: ${t.branch} at ${t.commit.slice(0, 7)}`, [
3483
+ `${this.cfg.member}'s unattended run finished this work: ${t.branch} at ${t.commit.slice(0, 12)} is on origin — fetch it and check it.`,
3484
+ `Its DELIVERED could not be sent (${because}), so this note says it instead.`,
3485
+ ].join("\n"));
3486
+ if (n === "later") {
3487
+ later();
3488
+ continue;
3489
+ }
3490
+ if (n) {
3491
+ sent.push(n);
3492
+ if (e.ref)
3493
+ this.markDone(`handled:${e.ref}`);
3494
+ }
3495
+ drop(e);
3496
+ tell("A delivery went as a note", `A woken run's work on ${t.branch} at ${t.commit.slice(0, 7)} is on origin, but its DELIVERED could not be sent (${because}). ${n ? "Your teammate was told in a note." : "Your teammate could not be told — tell them yourself."}`);
3497
+ continue;
3498
+ }
3499
+ }
3500
+ const words = why === "stopped" ? "the worker stopped before it could push it"
3501
+ : why === "not_own_branch" ? `it names ${t.branch}, not this run's own branch ${e.branch}, which the worker will not push`
3502
+ : why === "mass_delete" ? "this branch deletes most of the files tracked at its base, so it is not pushed without a person looking"
3503
+ : why === "not_allowed" ? "this run was not allowed to push"
3504
+ : "the worker's push was refused too";
3505
+ this.opts.log(`! the DELIVERED the run held names ${t.branch} at ${t.commit.slice(0, 7)}, which is not on origin — ${words}; not sent, a NOTE goes instead`);
3506
+ const n = await note(e, `Not delivered yet: ${t.branch} could not be pushed`, [
3507
+ `The work for this is on branch ${t.branch} at ${t.commit.slice(0, 12)} on ${this.cfg.member}'s machine, but it could not be pushed, so there is nothing to fetch yet.`,
3508
+ `Why: ${words}. ${this.cfg.member}'s human has been told and will push it or say what happened.`,
3509
+ ].join("\n"));
3510
+ if (n === "later") {
3511
+ this.opts.log(` the note about it could not be sent yet — it is tried again`);
3512
+ later(why ?? undefined);
3513
+ continue;
3514
+ }
3515
+ if (n) {
3516
+ sent.push(n);
3517
+ if (e.ref)
3518
+ this.markDone(`handled:${e.ref}`);
3519
+ }
3520
+ drop(e);
3521
+ tell("A delivery was not pushed", `A woken run finished work on ${t.branch} at ${t.commit.slice(0, 7)}, but it could not be pushed (${words}), so its DELIVERED was not sent; ${n ? "your teammate was told the work is here" : "your teammate could not be told — tell them yourself"}. Push it yourself: git push origin ${t.branch} — then tell them.`, e, t.branch);
3522
+ }
3523
+ const left = this.heldState((st) => st.held.filter((x) => x.team === team));
3524
+ this.heldLeft = left.length > 0;
3525
+ this.heldRetryAt = left.length ? Math.max(Date.now() + 5_000, Math.min(...left.map((x) => x.nextTry ?? 0))) : 0;
3526
+ return { sent, waiting };
3527
+ }
3528
+ async settleLeftHeld(client) {
3529
+ if (this.running || !client.connected)
3530
+ return;
3531
+ try {
3532
+ const r = JSON.parse(readFileSync(join(this.stateDir(), RUN_RECORD), "utf8"));
3533
+ if (r && r.space === this.cfg.space && Number.isInteger(r.worker) && r.worker !== process.pid && pidAlive(r.worker))
3534
+ return;
3535
+ }
3536
+ catch { }
3537
+ try {
3538
+ await this.settleHeld(client, null);
3539
+ }
3540
+ catch (e) {
3541
+ this.opts.log(`! could not settle what a run held: ${e.message}`);
3542
+ }
3543
+ }
3173
3544
  async checkDeliveries(client, sent, run) {
3174
3545
  const deliveries = sent.filter((a) => a.act === "DELIVERED");
3175
3546
  if (!deliveries.length || !run.branch)
@@ -3187,14 +3558,9 @@ export class Worker {
3187
3558
  continue;
3188
3559
  if (await onRemote(run.worktree, br, commit))
3189
3560
  continue;
3190
- let pushed = false;
3191
- if (run.canPush && br === run.branch && validBase(br)
3192
- && (await gitOut(run.worktree, ["merge-base", "--is-ancestor", commit, `refs/heads/${br}`])) !== null) {
3193
- const r = await gitOut(run.worktree, ["push", "--quiet", "origin", `refs/heads/${br}:refs/heads/${br}`], FETCH_TIMEOUT_MS);
3194
- pushed = r !== null && await onRemote(run.worktree, br, commit);
3195
- if (pushed)
3196
- this.opts.log(`* pushed ${br} for the run: its DELIVERED ${d.id} named ${commit.slice(0, 7)}, which origin did not have — repo hooks not run on Pantheon's push${lfsInUse(run.worktree) ? "; LFS objects uploaded" : ""}`);
3197
- }
3561
+ const pushed = run.canPush && br === run.branch && await this.pushRunBranch(run.worktree, br, commit);
3562
+ if (pushed)
3563
+ this.opts.log(`* pushed ${br} for the run: its DELIVERED ${d.id} named ${commit.slice(0, 7)}, which origin did not have — repo hooks not run on Pantheon's push${lfsInUse(run.worktree) ? "; LFS objects uploaded" : ""}`);
3198
3564
  if (pushed)
3199
3565
  continue;
3200
3566
  const why = br !== run.branch ? `it names ${br}, not this run's own branch ${run.branch}, which the worker will not push`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "format": 1,
3
- "built_at": "2026-10-11T16:51:24.237Z",
3
+ "built_at": "2026-10-13T06:04:13.698Z",
4
4
  "swiftc": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)",
5
5
  "helpers": {
6
6
  "pantheon-tray": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.33.6",
3
+ "version": "0.33.7",
4
4
  "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {