@join-pantheon/cli 0.28.0 → 0.29.0

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/machine.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { homedir, platform } from "node:os";
2
+ import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync } from "node:fs";
3
+ import { homedir, hostname, networkInterfaces, platform } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { CONFIG_DIR } from "./config.js";
6
- import { plistPath } from "./service.js";
6
+ import { disableService, plistPath, serviceInstalled, serviceLogFile, unitPath, uninstallService } from "./service.js";
7
7
  import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
8
8
  const sh = (cmd) => {
9
9
  try {
@@ -46,6 +46,35 @@ function fromLaunchd(kind) {
46
46
  }
47
47
  return info;
48
48
  }
49
+ export function parseSystemdShow(out) {
50
+ const get = (k) => new RegExp(`^${k}=(.*)$`, "m").exec(out)?.[1]?.trim();
51
+ const pid = Number(get("MainPID"));
52
+ const state = get("ActiveState");
53
+ const status = get("ExecMainStatus");
54
+ return {
55
+ active: state === "active" || state === "activating" || state === "reloading",
56
+ ...(Number.isFinite(pid) && pid > 0 ? { pid } : {}),
57
+ ...(status && status !== "0" ? { lastExit: status } : {}),
58
+ };
59
+ }
60
+ function fromSystemd(kind) {
61
+ if (platform() !== "linux" || !existsSync(unitPath(kind)))
62
+ return {};
63
+ const out = sh(`systemctl --user show pantheon-${kind} -p MainPID -p ActiveState -p ExecMainStatus 2>/dev/null`);
64
+ const label = `pantheon-${kind}.service`;
65
+ if (!out)
66
+ return { managedBy: "systemd", label, running: false };
67
+ const s = parseSystemdShow(out);
68
+ return {
69
+ managedBy: "systemd", label,
70
+ ...(s.active && s.pid ? { running: true, pid: s.pid, since: uptime(s.pid) } : { running: false }),
71
+ ...(s.lastExit ? { lastExit: s.lastExit } : {}),
72
+ };
73
+ }
74
+ const fromManager = (kind) => {
75
+ const l = fromLaunchd(kind);
76
+ return l.managedBy ? l : fromSystemd(kind);
77
+ };
49
78
  function listenerOn(port) {
50
79
  const out = sh(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null | head -1`);
51
80
  const pid = out ? Number(out) : NaN;
@@ -54,48 +83,29 @@ function listenerOn(port) {
54
83
  return { pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
55
84
  }
56
85
  export function relayProc(port) {
57
- const svc = fromLaunchd("relay");
86
+ const svc = fromManager("relay");
58
87
  const held = listenerOn(port);
59
88
  if (svc.running && svc.pid)
60
- return { kind: "relay", running: true, managedBy: "launchd", ...svc };
89
+ return { kind: "relay", ...svc, running: true };
61
90
  if (held) {
62
91
  return { kind: "relay", running: true, managedBy: "foreground", pid: held.pid, ...(held.since ? { since: held.since } : {}) };
63
92
  }
64
- return { kind: "relay", running: false, managedBy: svc.label ? "launchd" : "none", ...svc };
65
- }
66
- export function workerProc() {
67
- const svc = fromLaunchd("worker");
68
- if (svc.running && svc.pid)
69
- return { kind: "worker", running: true, managedBy: "launchd", ...svc };
70
- const bare = sh(`pgrep -f "pantheon.* worker" | head -1`);
71
- const pid = bare ? Number(bare) : NaN;
72
- if (Number.isFinite(pid)) {
73
- return { kind: "worker", running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
74
- }
75
- return { kind: "worker", running: false, managedBy: svc.label ? "launchd" : "none", ...svc };
76
- }
77
- export function notifierProc() {
78
- const svc = fromLaunchd("notifier");
79
- if (svc.running && svc.pid)
80
- return { kind: "notifier", running: true, managedBy: "launchd", ...svc };
81
- const bare = sh(`pgrep -f "pantheon.* notifier" | head -1`);
82
- const pid = bare ? Number(bare) : NaN;
83
- if (Number.isFinite(pid)) {
84
- return { kind: "notifier", running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
85
- }
86
- return { kind: "notifier", running: false, managedBy: svc.label ? "launchd" : "none", ...svc };
93
+ return { kind: "relay", running: false, managedBy: svc.managedBy ?? "none", ...svc };
87
94
  }
88
- export function menubarProc() {
89
- const svc = fromLaunchd("menubar");
95
+ function bareProc(kind, pattern) {
96
+ const svc = fromManager(kind);
90
97
  if (svc.running && svc.pid)
91
- return { kind: "menubar", running: true, managedBy: "launchd", ...svc };
92
- const bare = sh(`pgrep -f "pantheon.* menubar|pantheon-tray" | head -1`);
98
+ return { kind, ...svc, running: true };
99
+ const bare = sh(`pgrep -f "${pattern}" | head -1`);
93
100
  const pid = bare ? Number(bare) : NaN;
94
101
  if (Number.isFinite(pid)) {
95
- return { kind: "menubar", running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
102
+ return { kind, running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
96
103
  }
97
- return { kind: "menubar", running: false, managedBy: svc.label ? "launchd" : "none", ...svc };
104
+ return { kind, running: false, managedBy: svc.managedBy ?? "none", ...svc };
98
105
  }
106
+ export function workerProc() { return bareProc("worker", "pantheon.* worker"); }
107
+ export function notifierProc() { return bareProc("notifier", "pantheon.* notifier"); }
108
+ export function menubarProc() { return bareProc("menubar", "pantheon.* menubar|pantheon-tray"); }
99
109
  export function wiring() {
100
110
  const out = [];
101
111
  const claude = sh(`claude mcp list 2>/dev/null | grep -i "^pantheon"`);
@@ -129,7 +139,7 @@ export function wiring() {
129
139
  export function logFileFor(kind, proc) {
130
140
  if (proc.logPath && existsSync(proc.logPath))
131
141
  return proc.logPath;
132
- for (const c of [join(process.cwd(), ".pantheon", `${kind}.log`), join(CONFIG_DIR, `${kind}.log`)]) {
142
+ for (const c of [serviceLogFile(kind), join(process.cwd(), ".pantheon", `${kind}.log`), join(CONFIG_DIR, `${kind}.log`)]) {
133
143
  if (existsSync(c))
134
144
  return c;
135
145
  }
@@ -149,32 +159,223 @@ export function dataSummary(dir) {
149
159
  const spaces = files.split("\n").length;
150
160
  return `${acts} acts across ${spaces} space${spaces > 1 ? "s" : ""}`;
151
161
  }
152
- const defaultStopIo = () => ({
153
- proc: (kind) => kind === "relay" ? relayProc(8787) : kind === "worker" ? workerProc() : kind === "menubar" ? menubarProc() : notifierProc(),
154
- installed: (kind) => existsSync(plistPath(kind)),
162
+ const defaultStopIo = (port = 8787) => ({
163
+ proc: (kind) => kind === "relay" ? relayProc(port) : kind === "worker" ? workerProc() : kind === "menubar" ? menubarProc() : notifierProc(),
164
+ installed: (kind) => serviceInstalled(kind),
155
165
  sh,
166
+ disable: (kind) => disableService(kind),
167
+ cmdline: (pid) => sh(`ps -o command= -p ${pid}`),
156
168
  });
157
- export function stopProc(kind, io = defaultStopIo()) {
169
+ export const isPantheonCommand = (cmd) => /pantheon/i.test(cmd) || /\bcli\.js\s+(relay|start)\b/.test(cmd);
170
+ export function stopProcResult(kind, io = defaultStopIo()) {
158
171
  const p = io.proc(kind);
172
+ const ours = io.installed(kind);
173
+ if (!ours && (p.managedBy === "launchd" || p.managedBy === "systemd") && p.running) {
174
+ return { lines: [`${kind}: running under ${p.managedBy}, but not installed from this home — left alone`], stopped: false };
175
+ }
176
+ const killLive = (q) => {
177
+ if (!q.running || !q.pid)
178
+ return null;
179
+ if (kind === "relay") {
180
+ const cmd = (io.cmdline ?? (() => null))(q.pid);
181
+ if (!cmd || !isPantheonCommand(cmd)) {
182
+ return { line: `relay: the port is held by pid ${q.pid}${cmd ? ` (${cmd.slice(0, 80)})` : ""}, which is not Pantheon's relay — left alone`, killed: false };
183
+ }
184
+ }
185
+ io.sh(`kill ${q.pid}`);
186
+ return { line: `${kind}: stopped pid ${q.pid}${q.managedBy === "foreground" ? " (it was started by hand, not as a service)" : ""}`, killed: true };
187
+ };
188
+ if (ours) {
189
+ const lines = [];
190
+ let disabled = false;
191
+ try {
192
+ io.disable(kind);
193
+ disabled = true;
194
+ }
195
+ catch (e) {
196
+ const how = platform() === "darwin"
197
+ ? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
198
+ : `systemctl --user disable --now pantheon-${kind}`;
199
+ lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — it may start again at login; try: ${how}`);
200
+ }
201
+ const after = io.proc(kind);
202
+ const k = killLive(after);
203
+ if (k)
204
+ lines.push(k.line);
205
+ const stillUp = !!after.running && !k?.killed;
206
+ if (disabled) {
207
+ lines.unshift(`${kind}: ${stillUp ? "disabled, but a process is still running (see below)" : p.running || k?.killed ? "stopped, and will not start again at login" : "was not running; disabled, and will not start again at login"}`);
208
+ lines.push(` start it again with: pantheon service install ${kind}`);
209
+ }
210
+ return { lines, stopped: (p.running && disabled && !stillUp) || !!k?.killed };
211
+ }
159
212
  if (!p.running)
160
- return [`${kind}: already stopped`];
161
- if (p.managedBy === "launchd") {
162
- if (!io.installed(kind))
163
- return [`${kind}: running under launchd, but not installed from this home — left alone`];
164
- const plist = plistPath(kind);
165
- const uid = process.getuid?.() ?? 0;
166
- if (io.sh(`launchctl bootout gui/${uid}/dev.pantheon.${kind} 2>&1`) !== null ||
167
- io.sh(`launchctl unload -w "${plist}" 2>&1`) !== null) {
168
- return [`${kind}: stopped, and will not restart at login`,
169
- ` start it again with: pantheon service install ${kind}`];
213
+ return { lines: [`${kind}: already stopped`], stopped: false };
214
+ const k = killLive(p);
215
+ if (k)
216
+ return { lines: [k.line], stopped: k.killed };
217
+ return { lines: [`${kind}: running but no pid found — nothing stopped`], stopped: false };
218
+ }
219
+ export function stopProc(kind, io = defaultStopIo()) {
220
+ return stopProcResult(kind, io).lines;
221
+ }
222
+ export async function waitStopped(kind, ms = 3000, proc = defaultStopIo().proc) {
223
+ const until = Date.now() + ms;
224
+ for (;;) {
225
+ if (!proc(kind).running)
226
+ return true;
227
+ if (Date.now() >= until)
228
+ return false;
229
+ await new Promise((r) => setTimeout(r, 200));
230
+ }
231
+ }
232
+ export function hostsRelay(url, port, localAddrs = localAddresses(), host = hostname()) {
233
+ if (!url)
234
+ return false;
235
+ let u;
236
+ try {
237
+ u = new URL(url.replace(/^ws/, "http"));
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ const p = Number(u.port || (u.protocol === "https:" ? 443 : 80));
243
+ if (p !== port)
244
+ return false;
245
+ const h = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
246
+ const names = new Set(["localhost", "0.0.0.0", "::", "::1", host.toLowerCase(), `${host.toLowerCase()}.local`, ...localAddrs.map((a) => a.toLowerCase())]);
247
+ return names.has(h) || /^127\./.test(h);
248
+ }
249
+ export function localAddresses() {
250
+ try {
251
+ return Object.values(networkInterfaces()).flatMap((l) => (l ?? []).map((a) => a.address));
252
+ }
253
+ catch {
254
+ return [];
255
+ }
256
+ }
257
+ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = hostsRelay) {
258
+ const lines = [];
259
+ let relayStopped = false;
260
+ for (const kind of ["relay", "worker", "notifier", "menubar"]) {
261
+ if (what !== kind && what !== "all")
262
+ continue;
263
+ const r = stopProcResult(kind, io);
264
+ lines.push(...r.lines);
265
+ if (kind === "relay")
266
+ relayStopped = r.stopped;
267
+ }
268
+ if (!relayStopped)
269
+ return { lines };
270
+ const teams = opts.teams ?? (opts.teamRelay ? [{ name: "", relay: opts.teamRelay }] : []);
271
+ const hostedHere = teams.filter((t) => hosts(t.relay, opts.port));
272
+ if (hostedHere.length) {
273
+ const named = hostedHere.map((t) => t.name).filter(Boolean);
274
+ return { lines, note: `${named.length ? `Your teammates in ${named.map((n) => `"${n}"`).join(", ")} are` : "Your teammates are"} offline until a relay is running again.` };
275
+ }
276
+ const elsewhere = [...new Set(teams.map((t) => t.relay))];
277
+ return { lines, note: `Your team${teams.length === 1 ? "'s relay is" : "s' relays are"} ${elsewhere.join(", ") || "elsewhere"}, not this one — your teammates are not affected.` };
278
+ }
279
+ const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
280
+ export function uninstallMachine(opts, io) {
281
+ const lines = [], removed = [], left = [];
282
+ const kinds = ["worker", "notifier", "menubar", ...(opts.stopRelay ? ["relay"] : [])];
283
+ for (const kind of kinds) {
284
+ if (io.installed(kind)) {
285
+ try {
286
+ for (const l of io.uninstall(kind))
287
+ lines.push(`${kind}: ${l.trim()}`);
288
+ removed.push(`the ${kind} service — stopped, and its definition deleted so it cannot return at login`);
289
+ }
290
+ catch (e) {
291
+ left.push(`the ${kind} service (${e.message.split("\n")[0]}) — pantheon service uninstall ${kind}`);
292
+ }
293
+ }
294
+ const p = io.proc(kind);
295
+ if (p.running && p.managedBy === "foreground") {
296
+ if (kind === "relay")
297
+ for (const l of io.stop(kind))
298
+ lines.push(l);
299
+ else
300
+ left.push(`a ${kind} started by hand is still running (pid ${p.pid ?? "?"}) — close its terminal, or: kill ${p.pid ?? "<pid>"}`);
301
+ }
302
+ }
303
+ if (!opts.stopRelay && (io.installed("relay") || io.proc("relay").running)) {
304
+ left.push("the relay — others may be using it. Stop and remove it too: pantheon leave --stop-relay");
305
+ }
306
+ if (io.hasIntent()) {
307
+ io.clearIntent();
308
+ removed.push("the hook intent (hook-intent.json) — nothing will put the hooks back");
309
+ }
310
+ const mcp = io.removeMcpEntries();
311
+ removed.push(...mcp.removed);
312
+ left.push(...mcp.left);
313
+ try {
314
+ if (existsSync(io.logDir)) {
315
+ rmSync(io.logDir, { recursive: true, force: true });
316
+ removed.push(`service logs (${io.logDir})`);
317
+ }
318
+ }
319
+ catch { }
320
+ const repoDir = join(opts.project, ".pantheon");
321
+ let repoLogs = 0;
322
+ for (const k of LOG_KINDS) {
323
+ const f = join(repoDir, `${k}.log`);
324
+ try {
325
+ if (existsSync(f)) {
326
+ rmSync(f, { force: true });
327
+ repoLogs++;
328
+ }
170
329
  }
171
- return [`${kind}: could not stop the service — try: launchctl bootout gui/${uid}/dev.pantheon.${kind}`];
330
+ catch { }
172
331
  }
173
- if (p.pid) {
174
- io.sh(`kill ${p.pid}`);
175
- return [`${kind}: stopped pid ${p.pid} (it was started by hand, not as a service)`];
332
+ if (repoLogs) {
333
+ let rest = [];
334
+ try {
335
+ rest = readdirSync(repoDir);
336
+ }
337
+ catch { }
338
+ if (!rest.length) {
339
+ try {
340
+ rmdirSync(repoDir);
341
+ }
342
+ catch { }
343
+ removed.push(`${repoDir} (service logs an older version wrote into your repo)`);
344
+ }
345
+ else {
346
+ removed.push(`${repoLogs} service log${repoLogs === 1 ? "" : "s"} from ${repoDir}`);
347
+ left.push(`${repoDir}: ${rest.join(", ")} — your team's files, not logs`);
348
+ }
176
349
  }
177
- return [`${kind}: running but no pid found — nothing stopped`];
350
+ return { lines, removed, left };
351
+ }
352
+ export async function panicMachine(io) {
353
+ const lines = io.stopWorker().map((l) => ` ${l}`);
354
+ const workerDown = await io.workerDown();
355
+ if (!workerDown)
356
+ lines.push(` ! the worker is still running. Stop it by hand: ${platform() === "darwin" ? "launchctl bootout gui/$(id -u)/dev.pantheon.worker" : "systemctl --user disable --now pantheon-worker"} or kill its pid (pantheon ps)`);
357
+ let allRevoked = true;
358
+ if (!io.teams.length)
359
+ lines.push(" no team on this machine, so there are no wake grants to revoke");
360
+ for (const t of io.teams) {
361
+ try {
362
+ const who = await io.revoke(t);
363
+ lines.push(who.length ? ` ${t.name}: revoked wake rights from ${who.join(", ")}` : ` ${t.name}: no peer had wake rights to revoke`);
364
+ }
365
+ catch (e) {
366
+ allRevoked = false;
367
+ lines.push(` ${t.name}: could not reach its relay (${e.message.split("\n")[0]}), so its wake grants were not changed — run pantheon panic again when it is reachable`);
368
+ }
369
+ }
370
+ if (workerDown)
371
+ lines.push("", " Nothing can start an agent run on this machine now.");
372
+ else
373
+ lines.push("", " The worker is still running — stop it as above.");
374
+ return { lines, workerDown, ok: workerDown && allRevoked };
375
+ }
376
+ export function leavePlan(all) {
377
+ const teams = Object.values(all?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
378
+ return { teams, ...(all?.member ? { member: all.member } : {}) };
178
379
  }
179
380
  export function statFile(f) {
180
381
  try {
@@ -185,4 +386,22 @@ export function statFile(f) {
185
386
  return "unreadable";
186
387
  }
187
388
  }
389
+ export function realUninstallIo(deps) {
390
+ const s = defaultStopIo(deps.port ?? 8787);
391
+ return {
392
+ installed: (k) => serviceInstalled(k),
393
+ uninstall: (k) => uninstallService(k),
394
+ stop: (k) => stopProc(k, s),
395
+ proc: (k) => {
396
+ const p = s.proc(k);
397
+ if (k === "relay" && p.running && p.managedBy === "foreground" && p.pid && !isPantheonCommand(s.cmdline?.(p.pid) ?? ""))
398
+ return { ...p, running: false };
399
+ return p;
400
+ },
401
+ removeMcpEntries: deps.removeMcpEntries,
402
+ hasIntent: deps.hasIntent,
403
+ clearIntent: deps.clearIntent,
404
+ logDir: join(CONFIG_DIR, "logs"),
405
+ };
406
+ }
188
407
  //# sourceMappingURL=machine.js.map
@@ -261,6 +261,11 @@ export async function superviseMcp() {
261
261
  await new Promise((r) => setTimeout(r, 250));
262
262
  if (!children.has(c))
263
263
  return;
264
+ try {
265
+ mkdirSync(supervisorStateDir(), { recursive: true });
266
+ writeFileSync(join(supervisorStateDir(), `retiring-${c.proc.pid}`), String(Date.now()));
267
+ }
268
+ catch { }
264
269
  try {
265
270
  c.proc.stdin.end();
266
271
  }
package/dist/mcp.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
1
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
6
  import { z } from "zod";
@@ -15,6 +18,7 @@ import { loadProfile, organizationProfile, routeDecision } from "./profile.js";
15
18
  import { renderBrief } from "./brief.js";
16
19
  import { protocol, sessionInstructions } from "./mcp-instructions.js";
17
20
  import { recentRuns } from "./spend.js";
21
+ import { refusalOf } from "./refusal.js";
18
22
  const ACT_HELP = {
19
23
  NOTE: "FYI. Creates no obligation.",
20
24
  ASK: "A question a named peer owes you an answer to. Requires `to`.",
@@ -111,15 +115,16 @@ export async function runMcp() {
111
115
  const mcpActivity = role === "interactive" && !!backend &&
112
116
  (backend.id === "claude-code" ? !hooksOn : true);
113
117
  let toolsInFlight = 0;
118
+ const activityKey = `${process.pid.toString(36)}-${randomBytes(4).toString("hex")}`;
114
119
  const activityIdle = () => {
115
120
  if (!mcpActivity)
116
121
  return;
117
- reportMcpIdle(cfg, client.session, client.readSeq || client.head).catch(() => false);
122
+ reportMcpIdle(cfg, activityKey, client.readSeq || client.head).catch(() => false);
118
123
  };
119
124
  const activityBusy = () => {
120
125
  if (!mcpActivity)
121
126
  return;
122
- reportMcpBusy(cfg, client.session).catch(() => false);
127
+ reportMcpBusy(cfg, activityKey).catch(() => false);
123
128
  };
124
129
  const activityAround = async (fn) => {
125
130
  if (mcpActivity) {
@@ -154,6 +159,11 @@ export async function runMcp() {
154
159
  return fail(`pantheon: still connecting to the relay (${Math.round((Date.now() - startedAt) / 100) / 10}s since this session started) — ` +
155
160
  `try this call again in a moment. Nothing to restart.`);
156
161
  }
162
+ const refused = connectError ? refusalOf(connectError) : null;
163
+ if (refused) {
164
+ return fail(`pantheon: the relay refused this machine (${refused.code}): ${refused.message}\n` +
165
+ `${refused.fix ? `The fix: ${refused.fix}.\n` : ""}Waiting will not clear this: it needs your human. Once they fix it, this session connects on its own — nothing to restart. Keep working locally meanwhile.`);
166
+ }
157
167
  return fail(`pantheon cannot reach the relay yet: ${connectError ?? "no answer"}. Retrying in the background; try again in a few seconds.\n` +
158
168
  `${versionNote() ?? "If this persists the relay may be down — tell your human, and keep working locally meanwhile."}`);
159
169
  }
@@ -474,7 +484,9 @@ export async function runMcp() {
474
484
  url.searchParams.set("space", cfg.space);
475
485
  url.searchParams.set("token", cfg.token);
476
486
  url.searchParams.set("goal", goal);
477
- return fetch(url).then(async (res) => {
487
+ return fetch(url, { signal: AbortSignal.timeout(10_000) }).then(async (res) => {
488
+ if (!res.ok)
489
+ return fail(`the relay answered ${res.status} for this goal's thread — try again in a moment`);
478
490
  const { acts } = (await res.json());
479
491
  const hits = acts.filter((a) => a.goal === goal).slice(-(limit ?? 60));
480
492
  return text(`goal ${g.id} "${g.title}" [${g.state}] ${g.acts}/${g.budget} acts${deadlineLabel(g.deadline)}\nacceptance: ${g.acceptance || "(none)"}\n\n` +
@@ -490,11 +502,26 @@ export async function runMcp() {
490
502
  shuttingDown = true;
491
503
  if (mcpActivity && client.connected) {
492
504
  await Promise.race([
493
- reportMcpIdle(cfg, client.session, client.readSeq || client.head).catch(() => false),
505
+ reportMcpIdle(cfg, activityKey, client.readSeq || client.head).catch(() => false),
494
506
  new Promise((r) => setTimeout(r, 1_200)),
495
507
  ]);
496
508
  }
497
- client.close();
509
+ let handover = false;
510
+ if (process.env.PANTHEON_MCP_SUPERVISED) {
511
+ try {
512
+ const { supervisorStateDir } = await import("./mcp-supervisor.js");
513
+ const mark = join(supervisorStateDir(), `retiring-${process.pid}`);
514
+ if (existsSync(mark)) {
515
+ handover = true;
516
+ rmSync(mark, { force: true });
517
+ }
518
+ }
519
+ catch { }
520
+ }
521
+ if (handover)
522
+ client.handOff();
523
+ else
524
+ client.close();
498
525
  process.exit(0);
499
526
  };
500
527
  process.on("SIGINT", shutdown);
@@ -521,7 +548,7 @@ export async function runMcp() {
521
548
  connectError = e.message;
522
549
  connecting = false;
523
550
  firstAttemptDone();
524
- if (/^PENDING\b/.test(connectError)) {
551
+ if (/^PENDING\b/.test(connectError) || refusalOf(connectError)) {
525
552
  await new Promise((r) => setTimeout(r, PENDING_RETRY_MS));
526
553
  continue;
527
554
  }