@join-pantheon/cli 0.33.5 → 0.33.6-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/dist/agents.js +114 -13
- package/dist/board.js +4 -0
- package/dist/claude-confine.js +46 -2
- package/dist/cli.js +194 -23
- package/dist/client.js +143 -6
- package/dist/doctor.js +49 -7
- package/dist/file-lock.js +2 -2
- package/dist/git-safe.js +5 -1
- package/dist/home-guard.js +57 -2
- package/dist/home-safety.js +2 -0
- package/dist/linux-sandbox.js +71 -0
- package/dist/machine.js +18 -0
- package/dist/mcp.js +13 -5
- package/dist/notifier.js +57 -7
- package/dist/persona.js +34 -10
- package/dist/pickup.js +82 -0
- package/dist/profile.js +24 -1
- package/dist/protocol.js +19 -2
- package/dist/relay.js +128 -20
- package/dist/removed.js +132 -0
- package/dist/run-failure.js +13 -0
- package/dist/setup-guide.js +27 -4
- package/dist/store.js +59 -0
- package/dist/tray.js +45 -7
- package/dist/usage.js +97 -0
- package/dist/which.js +3 -2
- package/dist/worker-fleet.js +33 -3
- package/dist/worker-policy.js +58 -0
- package/dist/worker.js +215 -29
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
+
import { clearRemoval, recordRemoval, removalFor, removalHoldMs, removalRefusal, REMOVED_CHECK_MS, REMOVED_FIRST_MS, REMOVED_MAX_MS } from "./removed.js";
|
|
2
3
|
import { adoptSpaceId, loadAll, rememberTeamRepos, updateMembershipToken } from "./config.js";
|
|
3
4
|
import { honourClearedAuthority } from "./profile.js";
|
|
4
5
|
import { markContinued } from "./continued.js";
|
|
@@ -35,9 +36,13 @@ export class PantheonClient {
|
|
|
35
36
|
cid = 0;
|
|
36
37
|
static REFUSED_RETRY_MS = 5 * 60_000;
|
|
37
38
|
static REFUSED_GRACE_MS = 2 * 60_000;
|
|
39
|
+
static REMOVED_CHECK_MS = REMOVED_CHECK_MS;
|
|
38
40
|
get refused() { return this.connected ? null : this.refusedCode; }
|
|
39
41
|
refusedCode = null;
|
|
40
42
|
refusedSince = 0;
|
|
43
|
+
get removedFrom() { return this.connected ? null : this.removal; }
|
|
44
|
+
removal = null;
|
|
45
|
+
refusedAsRemoval = false;
|
|
41
46
|
seq = 0;
|
|
42
47
|
readSeq = 0;
|
|
43
48
|
head = 0;
|
|
@@ -180,6 +185,7 @@ export class PantheonClient {
|
|
|
180
185
|
return;
|
|
181
186
|
}
|
|
182
187
|
let settled = false;
|
|
188
|
+
this.refusedAsRemoval = false;
|
|
183
189
|
let heard = false;
|
|
184
190
|
const fail = reject;
|
|
185
191
|
reject = (err) => fail(heard ? Object.assign(err, { relayAnswered: true }) : err);
|
|
@@ -277,6 +283,8 @@ export class PantheonClient {
|
|
|
277
283
|
this.connected = true;
|
|
278
284
|
this.pending = null;
|
|
279
285
|
this.refusedCode = null;
|
|
286
|
+
this.removal = null;
|
|
287
|
+
this.removedWaitMs = 0;
|
|
280
288
|
this.session = msg.session;
|
|
281
289
|
this.everConnected = true;
|
|
282
290
|
this.head = msg.seq;
|
|
@@ -317,12 +325,17 @@ export class PantheonClient {
|
|
|
317
325
|
this.spaceId = msg.space_id;
|
|
318
326
|
this.cfg.spaceId = msg.space_id;
|
|
319
327
|
}
|
|
328
|
+
try {
|
|
329
|
+
clearRemoval({ space: this.cfg.space, spaceId: this.cfg.spaceId ?? msg.space_id ?? null, relay: this.cfg.relay });
|
|
330
|
+
}
|
|
331
|
+
catch { }
|
|
320
332
|
this.repos = msg.repos ?? [];
|
|
321
333
|
this.keepRepos();
|
|
322
334
|
this.plan = msg.plan ?? null;
|
|
323
335
|
this.adoptKnownRepo();
|
|
324
336
|
this.backoff = 500;
|
|
325
337
|
this.roster = msg.roster;
|
|
338
|
+
this.learnRelayNow(msg.now);
|
|
326
339
|
this.obligations = msg.obligations;
|
|
327
340
|
this.members = msg.members;
|
|
328
341
|
this.features = msg.features ?? [];
|
|
@@ -371,6 +384,7 @@ export class PantheonClient {
|
|
|
371
384
|
break;
|
|
372
385
|
case "roster":
|
|
373
386
|
this.roster = msg.roster;
|
|
387
|
+
this.learnRelayNow(msg.now);
|
|
374
388
|
break;
|
|
375
389
|
case "obligations":
|
|
376
390
|
this.obligations = msg.list;
|
|
@@ -382,7 +396,7 @@ export class PantheonClient {
|
|
|
382
396
|
const fn = msg.cid ? this.acks.get(msg.cid) : undefined;
|
|
383
397
|
if (fn && msg.cid) {
|
|
384
398
|
this.acks.delete(msg.cid);
|
|
385
|
-
fn({ id: msg.id, seq: msg.seq });
|
|
399
|
+
fn({ id: msg.id, seq: msg.seq, ...(typeof msg.note === "string" && msg.note ? { note: msg.note.slice(0, 300) } : {}) });
|
|
386
400
|
}
|
|
387
401
|
break;
|
|
388
402
|
}
|
|
@@ -393,6 +407,15 @@ export class PantheonClient {
|
|
|
393
407
|
if (code && !this.refusedCode)
|
|
394
408
|
this.refusedSince = Date.now();
|
|
395
409
|
this.refusedCode = code;
|
|
410
|
+
const removed = code === "REVOKED" ? removalRefusal(this.lastError) : null;
|
|
411
|
+
this.removal = removed;
|
|
412
|
+
this.refusedAsRemoval = !!removed;
|
|
413
|
+
if (removed) {
|
|
414
|
+
try {
|
|
415
|
+
recordRemoval({ space: this.cfg.space, spaceId: this.cfg.spaceId ?? null, relay: this.cfg.relay, member: this.cfg.member }, removed.by);
|
|
416
|
+
}
|
|
417
|
+
catch { }
|
|
418
|
+
}
|
|
396
419
|
}
|
|
397
420
|
if (msg.code === "SPACE_DELETED")
|
|
398
421
|
this.closed = true;
|
|
@@ -435,17 +458,64 @@ export class PantheonClient {
|
|
|
435
458
|
reject(new Error(this.lastError ?? "relay connection failed"));
|
|
436
459
|
}
|
|
437
460
|
const slow = !!this.refused && Date.now() - this.refusedSince >= PantheonClient.REFUSED_GRACE_MS;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
461
|
+
if (slow && this.removal && this.refusedAsRemoval)
|
|
462
|
+
this.waitOutRemoval();
|
|
463
|
+
else {
|
|
464
|
+
const wait = slow ? PantheonClient.REFUSED_RETRY_MS : Math.round(this.backoff * (0.5 + Math.random()));
|
|
465
|
+
setTimeout(() => { if (!this.closed)
|
|
466
|
+
this.connect().catch(() => { }); }, wait);
|
|
467
|
+
}
|
|
441
468
|
this.backoff = Math.min(this.backoff * 2, 15_000);
|
|
442
469
|
};
|
|
443
470
|
ws.on("close", down);
|
|
444
471
|
ws.on("error", (e) => { this.lastError = String(e.message ?? e); down(); });
|
|
445
472
|
});
|
|
446
473
|
}
|
|
474
|
+
waitOutRemoval() {
|
|
475
|
+
const team = { space: this.cfg.space, spaceId: this.cfg.spaceId ?? null, relay: this.cfg.relay, member: this.cfg.member };
|
|
476
|
+
let rec = null;
|
|
477
|
+
try {
|
|
478
|
+
rec = removalFor(team);
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
rec = null;
|
|
482
|
+
}
|
|
483
|
+
this.removedWaitMs = Math.min(this.removedWaitMs ? this.removedWaitMs * 2 : REMOVED_FIRST_MS, REMOVED_MAX_MS);
|
|
484
|
+
const now = Date.now();
|
|
485
|
+
const until = Math.max(rec ? rec.last + removalHoldMs(rec) : 0, now + this.removedWaitMs);
|
|
486
|
+
const next = () => Math.max(1_000, Math.min(PantheonClient.REMOVED_CHECK_MS, until - Date.now()));
|
|
487
|
+
const tick = () => {
|
|
488
|
+
this.removalTimer = null;
|
|
489
|
+
if (this.closed || this.connected)
|
|
490
|
+
return;
|
|
491
|
+
let gone = false;
|
|
492
|
+
if (rec) {
|
|
493
|
+
try {
|
|
494
|
+
gone = !removalFor(team);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
gone = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (gone || Date.now() >= until) {
|
|
501
|
+
this.connect().catch(() => { });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this.removalTimer = setTimeout(tick, next());
|
|
505
|
+
};
|
|
506
|
+
if (this.removalTimer)
|
|
507
|
+
clearTimeout(this.removalTimer);
|
|
508
|
+
this.removalTimer = setTimeout(tick, next());
|
|
509
|
+
}
|
|
510
|
+
removalTimer = null;
|
|
511
|
+
removedWaitMs = 0;
|
|
447
512
|
handOff() {
|
|
448
513
|
this.closed = true;
|
|
514
|
+
this.stopRenewing();
|
|
515
|
+
if (this.removalTimer) {
|
|
516
|
+
clearTimeout(this.removalTimer);
|
|
517
|
+
this.removalTimer = null;
|
|
518
|
+
}
|
|
449
519
|
for (const w of this.waiters) {
|
|
450
520
|
clearTimeout(w.timer);
|
|
451
521
|
w.resolve([]);
|
|
@@ -463,6 +533,11 @@ export class PantheonClient {
|
|
|
463
533
|
}
|
|
464
534
|
close() {
|
|
465
535
|
this.closed = true;
|
|
536
|
+
this.stopRenewing();
|
|
537
|
+
if (this.removalTimer) {
|
|
538
|
+
clearTimeout(this.removalTimer);
|
|
539
|
+
this.removalTimer = null;
|
|
540
|
+
}
|
|
466
541
|
for (const w of this.waiters) {
|
|
467
542
|
clearTimeout(w.timer);
|
|
468
543
|
w.resolve([]);
|
|
@@ -764,8 +839,55 @@ export class PantheonClient {
|
|
|
764
839
|
const o = this.obligations.find((x) => x.id === obligation);
|
|
765
840
|
if (o)
|
|
766
841
|
this.claimedByMe.set(o.id, o);
|
|
842
|
+
if (this.role === "worker") {
|
|
843
|
+
this.held.add(obligation);
|
|
844
|
+
this.startRenewing();
|
|
845
|
+
}
|
|
767
846
|
return r;
|
|
768
847
|
}
|
|
848
|
+
static CLAIM_RENEW_MS = 10 * 60_000;
|
|
849
|
+
held = new Set();
|
|
850
|
+
renewTimer = null;
|
|
851
|
+
startRenewing() {
|
|
852
|
+
if (this.renewTimer)
|
|
853
|
+
return;
|
|
854
|
+
this.renewTimer = setInterval(() => { void this.renewClaims(); }, PantheonClient.CLAIM_RENEW_MS);
|
|
855
|
+
this.renewTimer.unref?.();
|
|
856
|
+
}
|
|
857
|
+
stopRenewing() {
|
|
858
|
+
if (this.renewTimer) {
|
|
859
|
+
clearInterval(this.renewTimer);
|
|
860
|
+
this.renewTimer = null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
forgetClaim(id) {
|
|
864
|
+
this.held.delete(id);
|
|
865
|
+
if (!this.held.size)
|
|
866
|
+
this.stopRenewing();
|
|
867
|
+
}
|
|
868
|
+
get renewing() { return [...this.held]; }
|
|
869
|
+
async renewClaims() {
|
|
870
|
+
if (!this.connected)
|
|
871
|
+
return 0;
|
|
872
|
+
let renewed = 0;
|
|
873
|
+
for (const id of [...this.held]) {
|
|
874
|
+
if (!id.startsWith("cont:") && !this.obligations.some((o) => o.id === id)) {
|
|
875
|
+
this.held.delete(id);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
await this.rpc({ t: "claim", obligation: id });
|
|
880
|
+
renewed++;
|
|
881
|
+
}
|
|
882
|
+
catch (e) {
|
|
883
|
+
if (/^(NO_SUCH_OBLIGATION|NO_SUCH_ACT|NOT_YOURS|NOT_ALLOWED_TO_WAKE|PLAN_LIMIT): /.test(e.message))
|
|
884
|
+
this.held.delete(id);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!this.held.size)
|
|
888
|
+
this.stopRenewing();
|
|
889
|
+
return renewed;
|
|
890
|
+
}
|
|
769
891
|
dissolutionNotices(rec) {
|
|
770
892
|
const out = [];
|
|
771
893
|
for (const [id, o] of this.claimedByMe) {
|
|
@@ -851,12 +973,27 @@ export class PantheonClient {
|
|
|
851
973
|
if (c.cwdRepoOrigin)
|
|
852
974
|
c.repoOrigin = c.cwdRepoOrigin;
|
|
853
975
|
}
|
|
854
|
-
reportWorkerState(state, detail, until) {
|
|
976
|
+
reportWorkerState(state, detail, until, idleMs, more = {}) {
|
|
977
|
+
const v2 = this.features.includes("worker-state-2");
|
|
978
|
+
const st = state === "unavailable" && !v2 ? "holding" : state;
|
|
979
|
+
const d = !v2 && more.reason ? `${detail ?? ""} [${more.reason}]`.trim() : detail;
|
|
980
|
+
const untilIn = until ? Math.max(0, Math.round(until - this.relayNow())) : undefined;
|
|
855
981
|
try {
|
|
856
|
-
this.ws?.send(JSON.stringify({ t: "worker_state", state, ...(
|
|
982
|
+
this.ws?.send(JSON.stringify({ t: "worker_state", state: st, ...(d ? { detail: d.slice(0, 200) } : {}), ...(until ? { until } : {}),
|
|
983
|
+
...(v2 && untilIn !== undefined ? { until_in: Math.min(untilIn, 7 * 24 * 3600_000) } : {}),
|
|
984
|
+
...(v2 && idleMs !== undefined ? { idle_ms: Math.max(0, Math.min(3_600_000, Math.round(idleMs))) } : {}),
|
|
985
|
+
...(v2 && more.reason ? { reason: more.reason } : {}),
|
|
986
|
+
...(v2 && more.ref && /^(cont:)?[a-z]{1,3}_[A-Za-z0-9_-]{4,64}$/.test(more.ref) ? { ref: more.ref } : {}),
|
|
987
|
+
...(v2 && more.goal && /^[a-z]{1,3}_[A-Za-z0-9_-]{4,64}$/.test(more.goal) ? { goal: more.goal } : {}) }));
|
|
857
988
|
}
|
|
858
989
|
catch { }
|
|
859
990
|
}
|
|
991
|
+
relayNow() { return Date.now() + this.relaySkewMs; }
|
|
992
|
+
relaySkewMs = 0;
|
|
993
|
+
learnRelayNow(now) {
|
|
994
|
+
if (typeof now === "number" && Number.isFinite(now) && now > 0)
|
|
995
|
+
this.relaySkewMs = now - Date.now();
|
|
996
|
+
}
|
|
860
997
|
deleteSpace(confirm) {
|
|
861
998
|
return this.rpc({ t: "delete_space", confirm });
|
|
862
999
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { describeWindows, scanMcpWindows } from "./mcp-windows.js";
|
|
|
6
6
|
import { grantsSeen, measureMcp, toolsReport, HOST_STARTUP_ALLOWANCE_MS, STARTUP_WARN_MS } from "./measure.js";
|
|
7
7
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
8
8
|
import { loadProfile, profileInitialized, reservedDomains, teamProfile, wakeMode } from "./profile.js";
|
|
9
|
-
import { isApprovedWaker, loadPolicy, relayKnowsAdmitters, unsandboxedAllowed } from "./worker-policy.js";
|
|
9
|
+
import { inWokenRun, isApprovedWaker, loadPolicy, relayKnowsAdmitters, requestReadinessRecheck, unsandboxedAllowed } from "./worker-policy.js";
|
|
10
10
|
import { commandState } from "./command-state.js";
|
|
11
11
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
12
12
|
import { homedir, platform } from "node:os";
|
|
@@ -19,8 +19,11 @@ import { load, resolveFor, loadAll, configFor, CONFIG_PATH } from "./config.js";
|
|
|
19
19
|
import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
|
|
20
20
|
import { approvalSummary, chosenApproval, copilotMcpConfigFile, kiroMcpConfigFile, onPath } from "./install.js";
|
|
21
21
|
import { hookState, readIntent, agentHookStates, repairNote } from "./notify.js";
|
|
22
|
-
import { loadPersonas } from "./persona.js";
|
|
23
|
-
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
22
|
+
import { loadPersonas, olderTeammatesNote } from "./persona.js";
|
|
23
|
+
import { workerProc, menubarProc, notifierProc, workerFlagRunMinutes } from "./machine.js";
|
|
24
|
+
import { isCatchAllDomain } from "./protocol.js";
|
|
25
|
+
import { probeLinuxSandbox, sandboxDoctorRow } from "./linux-sandbox.js";
|
|
26
|
+
import { removalKey, removalRefusal, removals, removalWords, teamsOf } from "./removed.js";
|
|
24
27
|
import { agentById, chooseAgent, describeChoice, detectAvailable, folderReaderLine, unattendedRefusal, skippedSummary, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
25
28
|
import { loginShellLookup, npmSpawn, onPathNamed, which, whichWindows } from "./which.js";
|
|
26
29
|
import { VERSION } from "./version.js";
|
|
@@ -151,6 +154,18 @@ function newestRelease() {
|
|
|
151
154
|
return null;
|
|
152
155
|
}
|
|
153
156
|
}
|
|
157
|
+
function repoBoundToTeam(cfg) {
|
|
158
|
+
try {
|
|
159
|
+
const f = loadAll();
|
|
160
|
+
const m = f && Object.values(f.spaces ?? {}).find((x) => x.id === cfg.spaceId || x.name === cfg.space);
|
|
161
|
+
if (!f || !m)
|
|
162
|
+
return false;
|
|
163
|
+
return Object.entries(f.bindings ?? {}).some(([k, b]) => b.space === m.id && !k.startsWith("d_") && existsSync(join(b.path, ".git")));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
154
169
|
export async function doctor(projectDir) {
|
|
155
170
|
const out = [];
|
|
156
171
|
const add = (level, name, detail, fix, share) => out.push({ level, name, detail, ...(fix ? { fix } : {}), ...(share ? { share } : {}) });
|
|
@@ -240,6 +255,10 @@ export async function doctor(projectDir) {
|
|
|
240
255
|
}
|
|
241
256
|
else if (here.reason === "stale")
|
|
242
257
|
add("fail", "team", `this ${here.repo ? "repo" : "folder"} is bound to a team this machine has left (${here.binding?.space})`, "pantheon use <team> --here, or pantheon join <invite>", "bound to a team this machine has left");
|
|
258
|
+
for (const { team, record } of removals(teamsOf(loadAll())).filter((x) => x.record.team !== removalKey(cfg) && x.record.team !== `${cfg.relay}|${cfg.space}`)) {
|
|
259
|
+
const w = removalWords(team, record.by);
|
|
260
|
+
add("warn", "removed", `${w.by ? `${w.by} removed you` : "you were removed"} from "${w.name}" (${new Date(record.at).toISOString().slice(0, 10)}) — this machine stopped connecting to it and checks now and then (at most every 6 hours)`, `to come back, you need a new invite from someone in "${w.name}" (pantheon join <invite>) or for a member to let you back in (pantheon admit ${w.member}); to forget it here: pantheon leave "${w.name}"`, "removed from a team");
|
|
261
|
+
}
|
|
243
262
|
if (here.repo && !here.repo.shared)
|
|
244
263
|
add("warn", "team", "this repo has no origin remote", "DELIVERED must name a commit a teammate can fetch; add a remote before delivering", "repo has no origin remote");
|
|
245
264
|
const nonce = "doctor-selftest";
|
|
@@ -251,6 +270,9 @@ export async function doctor(projectDir) {
|
|
|
251
270
|
if (profileInitialized(prof)) {
|
|
252
271
|
const owns = reservedDomains(teamProfile(prof, cfg.spaceId, cfg.space));
|
|
253
272
|
add("pass", "profile", `${prof.human.display_name} — ${prof.human.title}${owns.length ? `; you personally decide: ${owns.join(", ")}` : "; no human-required domains yet"}`, undefined, `published, ${owns.length} human-required domain${owns.length === 1 ? "" : "s"}`);
|
|
273
|
+
const vague = Object.keys(teamProfile(prof, cfg.spaceId, cfg.space).authority).filter((d) => isCatchAllDomain(d));
|
|
274
|
+
if (vague.length)
|
|
275
|
+
add("warn", "profile", `your profile claims ${vague.map((d) => `"${d}"`).join(", ")} — not a decision area, so no decision is ever routed by it`, "running pantheon setup drops it — name the areas you decide personally: pantheon setup --decides deployment,pricing (or --decides none); it is never published either way", "claims a catch-all decision area");
|
|
254
276
|
}
|
|
255
277
|
else {
|
|
256
278
|
add("warn", "profile", "no profile published — decisions cannot be routed to you, and a HUMAN_REQUIRED naming you arrives marked unverified", "pantheon profile set --name <you> --title <role> --human-required <domains you personally decide>", "none published");
|
|
@@ -305,6 +327,9 @@ export async function doctor(projectDir) {
|
|
|
305
327
|
if (!inRepo && folderHere) {
|
|
306
328
|
add("pass", "git", "a plain folder — messages, questions and decisions work here; code is handed over from a repo", undefined, "plain folder, no git needed");
|
|
307
329
|
}
|
|
330
|
+
else if (!inRepo && !repoBoundToTeam(cfg)) {
|
|
331
|
+
add("info", "git", "not a git repo — fine: questions, answers and decisions work anywhere; only handing over code needs one", undefined, "no repo; not needed for messages and decisions");
|
|
332
|
+
}
|
|
308
333
|
else if (!inRepo) {
|
|
309
334
|
add("warn", "git", `${projectDir} is not a git repo`, "DELIVERED requires a pushed branch + commit, so the work needs to live in a repo you both clone", "not a git repo");
|
|
310
335
|
}
|
|
@@ -321,6 +346,8 @@ export async function doctor(projectDir) {
|
|
|
321
346
|
agents.length
|
|
322
347
|
? add("pass", "agent cli", agents.join(", "), undefined, agents.join(", "))
|
|
323
348
|
: add("warn", "agent cli", "found none of claude / codex / cursor-agent / copilot / kiro-cli on PATH", "fine if you only drive agents through a GUI (Conductor, Cursor, VS Code, the Kiro IDE), but `pantheon worker` needs a CLI to spawn", "none of claude / codex / cursor-agent / copilot / kiro-cli on PATH");
|
|
349
|
+
if (!inWokenRun())
|
|
350
|
+
requestReadinessRecheck();
|
|
324
351
|
if (agents.length) {
|
|
325
352
|
try {
|
|
326
353
|
let unsandboxedOk = false;
|
|
@@ -491,7 +518,9 @@ export async function doctor(projectDir) {
|
|
|
491
518
|
}
|
|
492
519
|
client.pending
|
|
493
520
|
? add("fail", "membership", "registered but not admitted", `ask an admitted member to run: pantheon admit ${cfg.member} (confirm fingerprint ${fingerprint(cfg.publicKey)} with them by voice first)`, "registered, not admitted")
|
|
494
|
-
: (
|
|
521
|
+
: removalRefusal(client.lastError)
|
|
522
|
+
? ((r) => add("fail", "membership", `removed from "${cfg.space}"${r.by ? ` by ${r.by}` : ""} — this machine's worker, alerts and menu bar stopped connecting to it (they check now and then, at most every 6 hours)`, `to come back, you need a new invite from someone in "${cfg.space}" (pantheon join <invite>) or for a member to let you back in (pantheon admit ${cfg.member}); to forget it here: pantheon leave "${cfg.space}"`, "refused: removed from the team"))(removalRefusal(client.lastError))
|
|
523
|
+
: ((r) => add("fail", "membership", `refused: ${r?.message ?? client.lastError ?? "unknown"}`, (r?.fix ?? "run pantheon status for the relay's reason").replace("<you>", cfg.member), `refused: ${r?.code ?? String(client.lastError ?? "unknown").split(/[\s:]/)[0]}`))(refusalOf(client.lastError ?? ""));
|
|
495
524
|
client.close();
|
|
496
525
|
return out;
|
|
497
526
|
}
|
|
@@ -620,10 +649,18 @@ export async function doctor(projectDir) {
|
|
|
620
649
|
else
|
|
621
650
|
add("pass", "commands", `woken runs: ${cs.summary}`, undefined, cs.level === "off" ? "woken runs edit files only" : `woken runs may run commands (${cs.allowBash ? "--allow-bash" : "granted here"})`);
|
|
622
651
|
}
|
|
652
|
+
{
|
|
653
|
+
const { loadPolicy, effectiveRunMinutes, describeRunMinutes, effectiveIdleSeconds, describeIdleSeconds } = await import("./worker-policy.js");
|
|
654
|
+
const wf = workerFlagRunMinutes();
|
|
655
|
+
const lim = wf !== null ? { minutes: wf, from: "flag" } : effectiveRunMinutes({ policy: loadPolicy() });
|
|
656
|
+
add("pass", "run limit", `${describeRunMinutes(lim)} — a run still going then is stopped, tried once more, then handed to you`, undefined, `${lim.minutes} min (${lim.from === "default" ? "default" : lim.from === "flag" ? "worker flag" : "set here"})`);
|
|
657
|
+
const idle = effectiveIdleSeconds({ policy: loadPolicy() });
|
|
658
|
+
add("pass", "idle wait", `${idle.seconds ? `${describeIdleSeconds(idle.seconds)} of quiet in your session before the worker takes a request` : "none — the worker takes a request as soon as no turn is in flight in your session"} (${idle.from === "default" ? "the default" : "set on this machine"}; pantheon policy set idle-seconds <0-900>)`, undefined, `${idle.seconds}s (${idle.from === "default" ? "default" : "set here"})`);
|
|
659
|
+
}
|
|
623
660
|
if (cs.level !== "off" && host.platform === "linux") {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
add(
|
|
661
|
+
const r = sandboxDoctorRow(probeLinuxSandbox(), { container: host.kind === "container" });
|
|
662
|
+
if (r)
|
|
663
|
+
add(r.level, "sandbox", r.detail, r.fix, r.share);
|
|
627
664
|
}
|
|
628
665
|
}
|
|
629
666
|
if (host.platform === "linux" && !host.ephemeral) {
|
|
@@ -673,6 +710,11 @@ export async function doctor(projectDir) {
|
|
|
673
710
|
broken.length
|
|
674
711
|
? add("warn", "personas", `${personas.map((p) => p.name).join(", ")} — ${broken.map((p) => `${p.name}: ${p.warnings[0]}`).join("; ")}`, "a persona line that does not parse grants nothing and filters nothing — pantheon persona show <name>", `${personas.length}, ${broken.length} that do not parse`)
|
|
675
712
|
: add("pass", "personas", `${personas.map((p) => p.name).join(", ")} — the worker only takes work a persona covers`, undefined, `${personas.length}`);
|
|
713
|
+
for (const p of personas) {
|
|
714
|
+
const n = olderTeammatesNote(p);
|
|
715
|
+
if (n)
|
|
716
|
+
add("warn", "personas", `${p.name} (committed in the repo) ${n}`, undefined, "a committed persona uses a key older versions do not know");
|
|
717
|
+
}
|
|
676
718
|
}
|
|
677
719
|
const open = [...client.goals.values()].filter((g) => g.state !== "CLOSED");
|
|
678
720
|
const stateLine = `${client.head} acts in the ledger, ${open.length} open goal(s) — restored across relay restarts`;
|
package/dist/file-lock.js
CHANGED
|
@@ -147,11 +147,11 @@ export function brokenStateFix(broken) {
|
|
|
147
147
|
parts.push(`repair or delete ${files.join(", ")} (one made read-only or locked: make it writable again; one that is a folder or not a file: delete it)`);
|
|
148
148
|
return parts.join("; ");
|
|
149
149
|
}
|
|
150
|
-
export function withFileLock(f, fn) {
|
|
150
|
+
export function withFileLock(f, fn, waitMs = WAIT_MS) {
|
|
151
151
|
mkdirSync(dirname(f), { recursive: true, mode: 0o700 });
|
|
152
152
|
const lock = `${f}.lock`;
|
|
153
153
|
const token = `${process.pid} ${randomBytes(12).toString("hex")}`;
|
|
154
|
-
const deadline = Date.now() +
|
|
154
|
+
const deadline = Date.now() + waitMs;
|
|
155
155
|
const made = `${lock}.${randomBytes(6).toString("hex")}.new`;
|
|
156
156
|
writeFileSync(made, token, { mode: 0o600, flag: "wx" });
|
|
157
157
|
try {
|
package/dist/git-safe.js
CHANGED
|
@@ -168,12 +168,16 @@ function hooksHash(common) {
|
|
|
168
168
|
return h.digest("hex");
|
|
169
169
|
}
|
|
170
170
|
function cfgwtHash(gitdir) {
|
|
171
|
+
let body;
|
|
171
172
|
try {
|
|
172
|
-
|
|
173
|
+
body = readFileSync(join(gitdir, "config.worktree"));
|
|
173
174
|
}
|
|
174
175
|
catch {
|
|
175
176
|
return null;
|
|
176
177
|
}
|
|
178
|
+
if (/^\uFEFF?[ \t\r\n]*$/.test(body.toString("utf8")))
|
|
179
|
+
return null;
|
|
180
|
+
return createHash("sha256").update(body).digest("hex");
|
|
177
181
|
}
|
|
178
182
|
function inspect(worktree, a, snapshot = false) {
|
|
179
183
|
const common = a.common;
|
package/dist/home-guard.js
CHANGED
|
@@ -12,7 +12,9 @@ export const CONTINUED_FILE = "continued.json";
|
|
|
12
12
|
export const CURSOR_FILE = "worker-cursor.json";
|
|
13
13
|
export const PINS_FILE = "keys.json";
|
|
14
14
|
export const NONCES_FILE = "nonces.json";
|
|
15
|
-
export const
|
|
15
|
+
export const REMOVED_FILE = "removed.json";
|
|
16
|
+
export const RECHECK_FILE = "readiness-recheck.stamp";
|
|
17
|
+
export const GUARDED_FILES = [POLICY_FILE, CONFIG_FILE, PROFILE_FILE, HUD_FILE, CONTINUED_FILE, CURSOR_FILE, PINS_FILE, NONCES_FILE, REMOVED_FILE, RECHECK_FILE];
|
|
16
18
|
const sha = (b) => createHash("sha256").update(b).digest("hex");
|
|
17
19
|
const read = (f) => { try {
|
|
18
20
|
return statSync(f).isFile() ? readFileSync(f) : null;
|
|
@@ -195,8 +197,42 @@ export function narrowPolicy(before, now, keepAsks = askedHere) {
|
|
|
195
197
|
out.wakeTeamFlag = flags;
|
|
196
198
|
if ("wakeNo" in now || Object.keys(wakeNo).length)
|
|
197
199
|
out.wakeNo = wakeNo;
|
|
200
|
+
const minutes = (x) => (typeof x === "number" && Number.isInteger(x) && x >= RUN_MINUTES_MIN && x <= RUN_MINUTES_MAX ? x : RUN_MINUTES_DEFAULT);
|
|
201
|
+
if (!same(b.runMinutes, now.runMinutes) && minutes(now.runMinutes) > minutes(b.runMinutes)) {
|
|
202
|
+
if ("runMinutes" in b)
|
|
203
|
+
out.runMinutes = b.runMinutes;
|
|
204
|
+
else
|
|
205
|
+
delete out.runMinutes;
|
|
206
|
+
keys.push("runMinutes raised");
|
|
207
|
+
}
|
|
208
|
+
if (!same(b.idleSeconds, now.idleSeconds)) {
|
|
209
|
+
if ("idleSeconds" in b)
|
|
210
|
+
out.idleSeconds = b.idleSeconds;
|
|
211
|
+
else
|
|
212
|
+
delete out.idleSeconds;
|
|
213
|
+
keys.push("idleSeconds changed");
|
|
214
|
+
}
|
|
198
215
|
return { policy: out, keys };
|
|
199
216
|
}
|
|
217
|
+
export const RUN_MINUTES_DEFAULT = 60, RUN_MINUTES_MIN = 5, RUN_MINUTES_MAX = 180;
|
|
218
|
+
export function narrowRemoved(before, now) {
|
|
219
|
+
const b = obj(before), n = obj(now);
|
|
220
|
+
const value = {};
|
|
221
|
+
const keys = [];
|
|
222
|
+
for (const [k, v] of Object.entries(n)) {
|
|
223
|
+
if (!(k in b)) {
|
|
224
|
+
keys.push(`${k} added`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (!same(b[k], v)) {
|
|
228
|
+
keys.push(`${k} changed`);
|
|
229
|
+
value[k] = b[k];
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
value[k] = v;
|
|
233
|
+
}
|
|
234
|
+
return { value, keys };
|
|
235
|
+
}
|
|
200
236
|
export function narrowConfig(before, now) {
|
|
201
237
|
const keys = [];
|
|
202
238
|
const out = { ...now };
|
|
@@ -444,7 +480,7 @@ export function restoreHome(s) {
|
|
|
444
480
|
continue;
|
|
445
481
|
}
|
|
446
482
|
const locked = (fn) => {
|
|
447
|
-
if (!(f.rel === PINS_FILE || f.rel === NONCES_FILE || f.rel === CONTINUED_FILE))
|
|
483
|
+
if (!(f.rel === PINS_FILE || f.rel === NONCES_FILE || f.rel === CONTINUED_FILE || f.rel === REMOVED_FILE))
|
|
448
484
|
return fn();
|
|
449
485
|
let ran = false;
|
|
450
486
|
try {
|
|
@@ -462,6 +498,25 @@ export function restoreHome(s) {
|
|
|
462
498
|
catch {
|
|
463
499
|
writeFileSync(file, text, { mode: 0o600 });
|
|
464
500
|
} };
|
|
501
|
+
if (f.rel === RECHECK_FILE) {
|
|
502
|
+
if (f.bytes)
|
|
503
|
+
writeFileSync(file, f.bytes, { mode: 0o600 });
|
|
504
|
+
else
|
|
505
|
+
rmSync(file, { force: true });
|
|
506
|
+
out.push({ rel: f.rel, what: f.bytes ? "reverted" : "removed", keys: [], quiet: true });
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (f.rel === REMOVED_FILE) {
|
|
510
|
+
locked(() => {
|
|
511
|
+
const n = narrowRemoved(a, parse(read(file)));
|
|
512
|
+
if (n.keys.length) {
|
|
513
|
+
put(JSON.stringify(n.value, null, 2) + "\n");
|
|
514
|
+
trustWrite(file);
|
|
515
|
+
out.push({ rel: f.rel, what: "reverted", keys: n.keys, quiet: true });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
465
520
|
if (f.rel === PINS_FILE || f.rel === NONCES_FILE) {
|
|
466
521
|
locked(() => {
|
|
467
522
|
const cur = parse(read(file));
|
package/dist/home-safety.js
CHANGED
|
@@ -16,6 +16,8 @@ export const PANTHEON_HOME_ENTRIES = [
|
|
|
16
16
|
{ name: "worker-policy.json", what: "the worker's wake policy", identity: true },
|
|
17
17
|
{ name: "spend.json", what: "the spend ledger", identity: true },
|
|
18
18
|
{ name: "personas", what: "the worker's personas", dir: true, identity: true },
|
|
19
|
+
{ name: "removed.json", what: "the teams this machine was removed from", identity: true },
|
|
20
|
+
{ name: "readiness-recheck.stamp", what: "the mark that has the worker look again for an agent it can run" },
|
|
19
21
|
{ name: "worker-state.json", what: "the worker's last state" },
|
|
20
22
|
{ name: "worker-run.json", what: "the worker's record of a run in flight" },
|
|
21
23
|
{ name: "run-slot.json", what: "the worker's run slot" },
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { onPathNamed } from "./which.js";
|
|
6
|
+
export function sandboxProbeArgs(weakerNested = false) {
|
|
7
|
+
return ["--unshare-user", "--unshare-pid", "--unshare-net", "--cap-drop", "ALL", "--die-with-parent", "--new-session",
|
|
8
|
+
"--ro-bind", "/", "/", ...(weakerNested ? ["--bind", "/proc", "/proc"] : ["--proc", "/proc"]), "--dev", "/dev", "true"];
|
|
9
|
+
}
|
|
10
|
+
export const SANDBOX_PROBE_ARGS = sandboxProbeArgs(false);
|
|
11
|
+
export function claudeWeakerNested(home = homedir()) {
|
|
12
|
+
try {
|
|
13
|
+
const j = JSON.parse(readFileSync(join(home, ".claude", "settings.json"), "utf8"));
|
|
14
|
+
return j?.sandbox?.enableWeakerNestedSandbox === true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const SANDBOX_PROBE_TIMEOUT_MS = 5_000;
|
|
21
|
+
const realRun = (cmd, args, timeoutMs) => {
|
|
22
|
+
const r = spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"], timeout: timeoutMs, killSignal: "SIGKILL" });
|
|
23
|
+
return { status: r.status, stderr: String(r.stderr ?? ""), timedOut: !!r.error && /ETIMEDOUT/.test(String(r.error.code ?? r.error.message)) };
|
|
24
|
+
};
|
|
25
|
+
export function probeLinuxSandbox(o = {}) {
|
|
26
|
+
if ((o.os ?? platform()) !== "linux")
|
|
27
|
+
return null;
|
|
28
|
+
const onPath = o.onPath ?? ((n) => onPathNamed(n, "linux"));
|
|
29
|
+
const missing = ["bwrap", "socat"].filter((b) => !onPath(b));
|
|
30
|
+
if (missing.length)
|
|
31
|
+
return { ok: false, missing };
|
|
32
|
+
let r;
|
|
33
|
+
try {
|
|
34
|
+
r = (o.run ?? realRun)("bwrap", sandboxProbeArgs(o.weakerNested ?? claudeWeakerNested()), SANDBOX_PROBE_TIMEOUT_MS);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return { ok: false, error: e.message.split("\n")[0].slice(0, 200) };
|
|
38
|
+
}
|
|
39
|
+
if (r.status === 0)
|
|
40
|
+
return { ok: true };
|
|
41
|
+
const line = r.stderr.split("\n").map((l) => l.trim()).find(Boolean);
|
|
42
|
+
return { ok: false, error: r.timedOut ? `bwrap did not finish within ${SANDBOX_PROBE_TIMEOUT_MS / 1000} s` : (line ?? `bwrap exited ${r.status ?? "on a signal"}`).replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 200) };
|
|
43
|
+
}
|
|
44
|
+
const NAMESPACE_RE = /Operation not permitted|mount proc|setting up uid map|No permissions to create new namespace|user namespace|uid_map|Permission denied/i;
|
|
45
|
+
export function sandboxAdvice(p, where) {
|
|
46
|
+
if (p.missing?.length) {
|
|
47
|
+
return {
|
|
48
|
+
says: `${p.missing.join(" and ")} not found — Claude Code's sandbox cannot start, so every woken command run will be refused`,
|
|
49
|
+
fix: "install them: sudo apt install bubblewrap socat (Debian/Ubuntu) or sudo dnf install bubblewrap socat (Fedora)",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const says = `Claude Code's sandbox cannot start here (${p.error ?? "bubblewrap failed"}) — every woken command run is refused, so runs can edit files but not test, commit or push`;
|
|
53
|
+
if (where.container) {
|
|
54
|
+
return { says, fix: "this looks like a container, which likely forbids the namespaces and /proc mount the sandbox needs: run the container privileged (or with user namespaces allowed), or run the worker on a VM or a real machine. Pantheon never runs commands outside the sandbox instead." };
|
|
55
|
+
}
|
|
56
|
+
if (NAMESPACE_RE.test(p.error ?? "")) {
|
|
57
|
+
return { says, fix: "likely cause: unprivileged user namespaces are restricted (Ubuntu 23.10+/24.04 do this with AppArmor). To lift it: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 — and to keep it after a reboot, put kernel.apparmor_restrict_unprivileged_userns=0 in /etc/sysctl.d/60-userns.conf. A narrower route is an AppArmor profile that allows bwrap to create user namespaces. Then run pantheon doctor again." };
|
|
58
|
+
}
|
|
59
|
+
return { says, fix: "run the probe yourself to see more: bwrap " + SANDBOX_PROBE_ARGS.join(" ") + " — then pantheon doctor again" };
|
|
60
|
+
}
|
|
61
|
+
export function sandboxDoctorRow(p, where) {
|
|
62
|
+
if (!p)
|
|
63
|
+
return null;
|
|
64
|
+
if (p.ok)
|
|
65
|
+
return { level: "pass", detail: "Claude Code's sandbox likely starts here (bubblewrap: user, pid and network namespaces, no capabilities, its /proc)", share: "sandbox likely starts" };
|
|
66
|
+
const a = sandboxAdvice(p, where);
|
|
67
|
+
return p.missing?.length
|
|
68
|
+
? { level: "warn", detail: a.says, fix: a.fix, share: `sandbox tools missing: ${p.missing.join(", ")}` }
|
|
69
|
+
: { level: "fail", detail: a.says, fix: a.fix, share: `sandbox cannot start${where.container ? " (container)" : ""}` };
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=linux-sandbox.js.map
|
package/dist/machine.js
CHANGED
|
@@ -162,7 +162,25 @@ function bareProc(kind) {
|
|
|
162
162
|
}
|
|
163
163
|
return { kind, running: false, managedBy: svc.managedBy ?? "none", ...svc };
|
|
164
164
|
}
|
|
165
|
+
export function runBy(p) {
|
|
166
|
+
if ((p.managedBy === "launchd" || p.managedBy === "systemd") && p.label)
|
|
167
|
+
return `service ${p.label}`;
|
|
168
|
+
if (p.managedBy === "host")
|
|
169
|
+
return `background host (${p.label ?? "the Pantheon task"})`;
|
|
170
|
+
return "started by hand";
|
|
171
|
+
}
|
|
165
172
|
export function workerProc() { return bareProc("worker"); }
|
|
173
|
+
export function workerFlagRunMinutes(home = CONFIG_DIR, running = () => workerProc().running) {
|
|
174
|
+
try {
|
|
175
|
+
const s = JSON.parse(readFileSync(join(home, "worker-state.json"), "utf8"));
|
|
176
|
+
if (typeof s.run_minutes !== "number" || !Number.isFinite(s.run_minutes))
|
|
177
|
+
return null;
|
|
178
|
+
return running() ? s.run_minutes : null;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
166
184
|
export function notifierProc() { return bareProc("notifier"); }
|
|
167
185
|
export function menubarProc() { return bareProc("menubar"); }
|
|
168
186
|
const BARE_PATTERN = {
|
package/dist/mcp.js
CHANGED
|
@@ -23,6 +23,7 @@ import { protocol, sessionInstructions } from "./mcp-instructions.js";
|
|
|
23
23
|
import { recentRuns } from "./spend.js";
|
|
24
24
|
import { refusalOf } from "./refusal.js";
|
|
25
25
|
import { signedRead } from "./reads.js";
|
|
26
|
+
import { pickupEstimate } from "./pickup.js";
|
|
26
27
|
const ACT_HELP = {
|
|
27
28
|
NOTE: "FYI. Creates no obligation.",
|
|
28
29
|
ASK: "A question a named peer owes you an answer to. Requires `to`.",
|
|
@@ -282,10 +283,8 @@ export async function runMcp() {
|
|
|
282
283
|
const open = [...client.goals.values()].filter((g) => g.state !== "CLOSED");
|
|
283
284
|
const reach = (p) => {
|
|
284
285
|
const head = client.head;
|
|
285
|
-
const
|
|
286
|
-
const worker =
|
|
287
|
-
? `; their worker is holding — ${w.detail ?? "deferring to a live session"}${w.until && w.until > Date.now() ? `, ~${Math.round((w.until - Date.now()) / 1000)}s more` : ""}`
|
|
288
|
-
: w?.state === "running" ? `; their worker is running${w.detail ? ` (${w.detail})` : ""}` : "";
|
|
286
|
+
const est = p.worker ? pickupEstimate(p, { now: client.relayNow() }) : null;
|
|
287
|
+
const worker = est && est.kind !== "soon" && est.kind !== "offline" ? `; ${est.says}` : "";
|
|
289
288
|
return reachText(p, head) + worker;
|
|
290
289
|
};
|
|
291
290
|
const reachText = (p, head) => {
|
|
@@ -438,6 +437,14 @@ export async function runMcp() {
|
|
|
438
437
|
return text(fmtAll(acts) +
|
|
439
438
|
(work.length ? `\n\nYOURS TO HANDLE — pantheon_claim first:\n${work.map((o) => ` ${o.id} ${o.kind} — ${o.body}`).join("\n")}` : ""));
|
|
440
439
|
}));
|
|
440
|
+
const pickupLines = (to) => {
|
|
441
|
+
const mayWake = client.members.find((m) => m.member === cfg.member)?.may_wake;
|
|
442
|
+
const lines = to.filter((t) => t !== cfg.member).map((t) => {
|
|
443
|
+
const peer = client.roster.find((m) => m.id === t) ?? { id: t, sessions: [] };
|
|
444
|
+
return ` ${pickupEstimate(peer, { now: client.relayNow(), ...(mayWake !== undefined ? { mayWake } : {}) }).says}`;
|
|
445
|
+
});
|
|
446
|
+
return lines.length ? `\n\nwhen it will be picked up (tell your human if it is not soon):\n${lines.join("\n")}` : "";
|
|
447
|
+
};
|
|
441
448
|
server.registerTool("pantheon_send", {
|
|
442
449
|
title: "Send an act",
|
|
443
450
|
description: `Say one thing to your peers, as a typed act. Acts:\n` +
|
|
@@ -512,7 +519,8 @@ export async function runMcp() {
|
|
|
512
519
|
? `\n\nnote: this machine could not identify the request ${ref} (it was never read here and the relay did not serve it), so it is not recorded as answered — a copy of it arriving later would not be recognised. If one does, check with its sender before acting on it.`
|
|
513
520
|
: "";
|
|
514
521
|
const linkNote = privateLinkNote(body, summary);
|
|
515
|
-
|
|
522
|
+
const pickup = act === "ASK" ? pickupLines(to ?? []) : "";
|
|
523
|
+
return text(`sent ${act} — id=${r.id} seq=${r.seq}${pickup}${survived}${unrecorded}${linkNote ? `\n\nnote: ${linkNote}` : ""}\n\nNow call pantheon_wait to hear back.`);
|
|
516
524
|
}));
|
|
517
525
|
server.registerTool("pantheon_open_goal", {
|
|
518
526
|
title: "Open a goal",
|