@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 +7 -0
- package/dist/agents.js +45 -15
- package/dist/claude-confine.js +60 -6
- package/dist/cli.js +8 -0
- package/dist/client.js +8 -1
- package/dist/doctor.js +3 -2
- package/dist/held-delivery.js +247 -0
- package/dist/home-safety.js +2 -0
- package/dist/machine.js +1 -0
- package/dist/mcp-instructions.js +54 -16
- package/dist/mcp.js +39 -11
- package/dist/pickup.js +9 -3
- package/dist/protocol.js +2 -1
- package/dist/worker.js +394 -28
- package/helper/macos/prebuilt/manifest.json +1 -1
- package/package.json +1 -1
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:
|
|
762
|
-
|
|
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
|
|
2724
|
-
if (
|
|
2725
|
-
const
|
|
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: "
|
|
2730
|
-
|
|
2731
|
-
|
|
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
|
|
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
|
-
|
|
3191
|
-
if (
|
|
3192
|
-
|
|
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`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.33.
|
|
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": {
|