@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/tray.js
CHANGED
|
@@ -11,6 +11,8 @@ import { plistPath, serviceInstalled, whichPantheon } from "./service.js";
|
|
|
11
11
|
import { VERSION } from "./version.js";
|
|
12
12
|
import { LIMITS } from "./protocol.js";
|
|
13
13
|
import { signedRead } from "./reads.js";
|
|
14
|
+
import { clearRemoval, recordRemoval, removalFor, removalHolds, removalRefusal } from "./removed.js";
|
|
15
|
+
import { pickupEstimate } from "./pickup.js";
|
|
14
16
|
import { allDecisionsPrompt, decisionPrompt, decisionTitle, owedPrompt, stripCeremony, tidyIds, tidyLead, waitingPrompt, workerSummary } from "./decision-ui.js";
|
|
15
17
|
import { loadProfile, SURFACE_BUNDLE_IDS } from "./profile.js";
|
|
16
18
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -116,7 +118,8 @@ function workerState(home = CONFIG_DIR) {
|
|
|
116
118
|
const raw = JSON.parse(readFileSync(join(home, "worker-state.json"), "utf8"));
|
|
117
119
|
if (!raw.at || Date.now() - raw.at > 10 * 60_000)
|
|
118
120
|
return undefined;
|
|
119
|
-
|
|
121
|
+
const line = (x) => String(x ?? "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
122
|
+
return raw.detail ? `${line(raw.state)} — ${line(raw.detail)}` : line(raw.state);
|
|
120
123
|
}
|
|
121
124
|
catch {
|
|
122
125
|
return undefined;
|
|
@@ -136,13 +139,29 @@ async function getJson(url) {
|
|
|
136
139
|
clearTimeout(t);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
|
-
async function signedObligations(cfg) {
|
|
142
|
+
async function signedObligations(cfg, removed = null) {
|
|
140
143
|
try {
|
|
141
144
|
const res = await signedRead(cfg, "/obligations", {}, { timeoutMs: LIMITS.pendingTimeoutMs });
|
|
142
|
-
|
|
145
|
+
let refusedAsRemoved = false;
|
|
146
|
+
if (removed) {
|
|
147
|
+
const team = { space: cfg.space, spaceId: cfg.spaceId ?? null, relay: cfg.relay, member: cfg.member };
|
|
148
|
+
try {
|
|
149
|
+
if (res.status === 403) {
|
|
150
|
+
const why = await res.clone().json().catch(() => null);
|
|
151
|
+
if (why?.code === "REVOKED") {
|
|
152
|
+
refusedAsRemoved = true;
|
|
153
|
+
recordRemoval(team, removalRefusal(`REVOKED: ${why.error ?? ""} Only a member`)?.by ?? removed.by);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (res.ok)
|
|
157
|
+
clearRemoval(team);
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
return { body: res.ok ? await res.json() : null, refusedAsRemoved };
|
|
143
162
|
}
|
|
144
163
|
catch {
|
|
145
|
-
return null;
|
|
164
|
+
return { body: null, refusedAsRemoved: false };
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
export async function trayStatus(cfg = load(), f = loadAll()) {
|
|
@@ -160,6 +179,7 @@ export async function trayStatus(cfg = load(), f = loadAll()) {
|
|
|
160
179
|
relay_ok: ok.every((p) => p.relay_ok),
|
|
161
180
|
decisions_ok: ok.every((p) => p.decisions_ok),
|
|
162
181
|
unavailable_spaces: ok.flatMap((p) => p.unavailable_spaces ?? []),
|
|
182
|
+
removed_spaces: ok.flatMap((p) => p.removed_spaces ?? []),
|
|
163
183
|
online: ok.reduce((n, p) => n + (p.online ?? 0), 0),
|
|
164
184
|
members: ok.reduce((n, p) => n + (p.members ?? 0), 0),
|
|
165
185
|
decisions: ok.flatMap((p) => (p.decisions ?? []).map((d) => ({ ...d, body: `[${p.space}] ${d.body}` }))),
|
|
@@ -175,10 +195,24 @@ async function trayStatusOne(cfg) {
|
|
|
175
195
|
if (!cfg)
|
|
176
196
|
return { configured: false, version: VERSION };
|
|
177
197
|
const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
178
|
-
const
|
|
198
|
+
const record = (() => { try {
|
|
199
|
+
return removalFor({ space: cfg.space, spaceId: cfg.spaceId ?? null, relay: cfg.relay, member: cfg.member });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
} })();
|
|
204
|
+
const removed = removalHolds(record);
|
|
205
|
+
const [health, probe] = await Promise.all([
|
|
179
206
|
getJson(`${base}/health?space=${encodeURIComponent(cfg.space)}`),
|
|
180
|
-
signedObligations(cfg),
|
|
207
|
+
removed ? Promise.resolve({ body: null, refusedAsRemoved: false }) : signedObligations(cfg, record),
|
|
181
208
|
]);
|
|
209
|
+
const obligations = probe.body;
|
|
210
|
+
const rosterRaw = obligations?.roster;
|
|
211
|
+
const roster = Array.isArray(rosterRaw) ? rosterRaw : null;
|
|
212
|
+
const membersRaw = obligations?.members;
|
|
213
|
+
const myMayWake = Array.isArray(membersRaw) ? membersRaw.find((m) => m.member === cfg.member)?.may_wake : undefined;
|
|
214
|
+
const relayAt = obligations?.now;
|
|
215
|
+
const relayNow = typeof relayAt === "number" && Number.isFinite(relayAt) ? relayAt : Date.now();
|
|
182
216
|
const space = health
|
|
183
217
|
?.spaces?.find((s) => s.name === cfg.space);
|
|
184
218
|
const pending = obligations?.obligations;
|
|
@@ -213,7 +247,10 @@ async function trayStatusOne(cfg) {
|
|
|
213
247
|
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
214
248
|
...waiting.map((o) => {
|
|
215
249
|
const who = o.owner.replace(/^human:/, "");
|
|
216
|
-
const
|
|
250
|
+
const r0 = row(o, "waiting", who, waitingPrompt(who, decisionTitle(o.body), o.goal));
|
|
251
|
+
const r = roster && !o.owner.startsWith("human:") && o.kind !== "DELIVER"
|
|
252
|
+
? { ...r0, pickup: pickupEstimate(roster.find((m) => m.id === who) ?? { id: who, sessions: [] }, { now: relayNow, about: { ref: o.ref ?? null, goal: o.goal ?? null }, ...(myMayWake !== undefined ? { mayWake: myMayWake } : {}) }).says }
|
|
253
|
+
: r0;
|
|
217
254
|
return o.owner.startsWith("human:")
|
|
218
255
|
? { ...r, title: `${who} decides: ${r.title}`, headline: `${who} decides: ${r.headline}`, decider: who }
|
|
219
256
|
: r;
|
|
@@ -235,6 +272,7 @@ async function trayStatusOne(cfg) {
|
|
|
235
272
|
relay_ok: !!space,
|
|
236
273
|
decisions_ok: decisionsOk,
|
|
237
274
|
unavailable_spaces: !space || !decisionsOk ? [cfg.space] : [],
|
|
275
|
+
...(removed || probe.refusedAsRemoved ? { removed_spaces: [cfg.space] } : {}),
|
|
238
276
|
online: space?.online ?? 0,
|
|
239
277
|
members: space?.members ?? 0,
|
|
240
278
|
worker_running: workerRunning,
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export const USAGE_KEEP_DAYS = 400;
|
|
2
|
+
export const USAGE_SHOW_DAYS = 30;
|
|
3
|
+
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
4
|
+
export const emptyUsage = (space) => ({ v: 1, ...(space ? { space } : {}), days: {} });
|
|
5
|
+
const emptyDay = () => ({ acts: {}, by_worker: 0, by_person: 0, goals_opened: 0, goals_closed: 0 });
|
|
6
|
+
export const dayKey = (ts) => new Date(ts).toISOString().slice(0, 10);
|
|
7
|
+
const count = (n) => (typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0);
|
|
8
|
+
const ACT_NAME = /^[A-Z_]{1,32}$/;
|
|
9
|
+
const DAY_NAME = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
|
+
export function parseUsage(raw) {
|
|
11
|
+
const book = emptyUsage();
|
|
12
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
13
|
+
return book;
|
|
14
|
+
const r = raw;
|
|
15
|
+
if (typeof r.space === "string" && r.space)
|
|
16
|
+
book.space = r.space;
|
|
17
|
+
if (!r.days || typeof r.days !== "object" || Array.isArray(r.days))
|
|
18
|
+
return book;
|
|
19
|
+
for (const [day, v] of Object.entries(r.days)) {
|
|
20
|
+
if (!DAY_NAME.test(day) || !v || typeof v !== "object")
|
|
21
|
+
continue;
|
|
22
|
+
const d = v;
|
|
23
|
+
const acts = {};
|
|
24
|
+
if (d.acts && typeof d.acts === "object" && !Array.isArray(d.acts)) {
|
|
25
|
+
for (const [a, n] of Object.entries(d.acts))
|
|
26
|
+
if (ACT_NAME.test(a) && count(n))
|
|
27
|
+
acts[a] = count(n);
|
|
28
|
+
}
|
|
29
|
+
book.days[day] = { acts, by_worker: count(d.by_worker), by_person: count(d.by_person), goals_opened: count(d.goals_opened), goals_closed: count(d.goals_closed) };
|
|
30
|
+
}
|
|
31
|
+
return book;
|
|
32
|
+
}
|
|
33
|
+
export function pruneUsage(book, now = Date.now()) {
|
|
34
|
+
const oldest = dayKey(now - (USAGE_KEEP_DAYS - 1) * DAY_MS);
|
|
35
|
+
for (const day of Object.keys(book.days))
|
|
36
|
+
if (day < oldest)
|
|
37
|
+
delete book.days[day];
|
|
38
|
+
}
|
|
39
|
+
function today(book, now) {
|
|
40
|
+
const k = dayKey(now);
|
|
41
|
+
let d = book.days[k];
|
|
42
|
+
if (!d) {
|
|
43
|
+
d = book.days[k] = emptyDay();
|
|
44
|
+
pruneUsage(book, now);
|
|
45
|
+
}
|
|
46
|
+
return d;
|
|
47
|
+
}
|
|
48
|
+
export function countMessage(book, act, role, now = Date.now()) {
|
|
49
|
+
const d = today(book, now);
|
|
50
|
+
d.acts[act] = (d.acts[act] ?? 0) + 1;
|
|
51
|
+
if (role === "worker")
|
|
52
|
+
d.by_worker++;
|
|
53
|
+
else
|
|
54
|
+
d.by_person++;
|
|
55
|
+
}
|
|
56
|
+
export function countGoal(book, what, now = Date.now()) {
|
|
57
|
+
const d = today(book, now);
|
|
58
|
+
if (what === "opened")
|
|
59
|
+
d.goals_opened++;
|
|
60
|
+
else
|
|
61
|
+
d.goals_closed++;
|
|
62
|
+
}
|
|
63
|
+
export function usageRows(book, days = USAGE_SHOW_DAYS, now = Date.now()) {
|
|
64
|
+
const n = Math.max(1, Math.min(USAGE_KEEP_DAYS, Math.floor(days) || USAGE_SHOW_DAYS));
|
|
65
|
+
const oldest = dayKey(now - (n - 1) * DAY_MS);
|
|
66
|
+
return Object.entries(book.days)
|
|
67
|
+
.filter(([day]) => day >= oldest && day <= dayKey(now))
|
|
68
|
+
.sort(([a], [b]) => (a < b ? 1 : a > b ? -1 : 0))
|
|
69
|
+
.map(([day, d]) => ({ day, ...d, acts: { ...d.acts }, messages: Object.values(d.acts).reduce((s, x) => s + x, 0) }));
|
|
70
|
+
}
|
|
71
|
+
export function usageAnswer(space, book, days = USAGE_SHOW_DAYS, now = Date.now()) {
|
|
72
|
+
return { space, days: usageRows(book, days, now), kept_days: USAGE_KEEP_DAYS };
|
|
73
|
+
}
|
|
74
|
+
export function usageTable(space, rows, shownDays = USAGE_SHOW_DAYS) {
|
|
75
|
+
const out = [];
|
|
76
|
+
out.push(`Usage of "${space}" — the last ${shownDays} days (UTC), counted by the relay. Counts only: no message content, nothing per person.`);
|
|
77
|
+
if (!rows.length) {
|
|
78
|
+
out.push("", "Nothing counted in that time. (Counting started when the relay was upgraded to 0.33.6.)");
|
|
79
|
+
return out.join("\n");
|
|
80
|
+
}
|
|
81
|
+
const head = ["day", "messages", "by runs", "by people", "goals opened", "goals closed"];
|
|
82
|
+
const lines = rows.map((r) => [r.day, String(r.messages), String(r.by_worker), String(r.by_person), String(r.goals_opened), String(r.goals_closed)]);
|
|
83
|
+
const sum = (k) => rows.reduce((s, r) => s + r[k], 0);
|
|
84
|
+
const total = ["total", String(sum("messages")), String(sum("by_worker")), String(sum("by_person")), String(sum("goals_opened")), String(sum("goals_closed"))];
|
|
85
|
+
const width = head.map((h, i) => Math.max(h.length, ...lines.map((l) => l[i].length), total[i].length));
|
|
86
|
+
const fmt = (cells) => cells.map((c, i) => (i === 0 ? c.padEnd(width[i]) : c.padStart(width[i]))).join(" ");
|
|
87
|
+
out.push("", fmt(head), ...lines.map(fmt), fmt(width.map((w) => "-".repeat(w))), fmt(total));
|
|
88
|
+
const byAct = {};
|
|
89
|
+
for (const r of rows)
|
|
90
|
+
for (const [a, n] of Object.entries(r.acts))
|
|
91
|
+
byAct[a] = (byAct[a] ?? 0) + n;
|
|
92
|
+
const kinds = Object.entries(byAct).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
93
|
+
if (kinds.length)
|
|
94
|
+
out.push("", `messages by type: ${kinds.map(([a, n]) => `${a} ${n}`).join(", ")}`);
|
|
95
|
+
return out.join("\n");
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=usage.js.map
|
package/dist/which.js
CHANGED
|
@@ -83,6 +83,7 @@ function posixOnPath(name, env, ok = runnable) {
|
|
|
83
83
|
}
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
|
+
export const loginShellRunner = (timeoutMs) => (script, args) => runLoginShell(script, args, timeoutMs);
|
|
86
87
|
export function loginShellLookup(name, run = runLoginShell) {
|
|
87
88
|
if (!name)
|
|
88
89
|
return null;
|
|
@@ -108,9 +109,9 @@ export function parseLoginAnswer(out, mark) {
|
|
|
108
109
|
const found = said.length === 1 && said[0].startsWith("/") ? said[0] : null;
|
|
109
110
|
return { found, path };
|
|
110
111
|
}
|
|
111
|
-
function runLoginShell(script, args) {
|
|
112
|
+
function runLoginShell(script, args, timeoutMs = 15_000) {
|
|
112
113
|
try {
|
|
113
|
-
return execFileSync("sh", ["-lc", script, "sh", ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout:
|
|
114
|
+
return execFileSync("sh", ["-lc", script, "sh", ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: timeoutMs, killSignal: "SIGKILL" });
|
|
114
115
|
}
|
|
115
116
|
catch (e) {
|
|
116
117
|
const out = e.stdout;
|
package/dist/worker-fleet.js
CHANGED
|
@@ -4,6 +4,7 @@ import { holdsRepo, repoInside } from "./repo-find.js";
|
|
|
4
4
|
import { CONFIG_DIR, configFor, loadAll, repoInfo } from "./config.js";
|
|
5
5
|
import { Worker, workspaceOf } from "./worker.js";
|
|
6
6
|
import { folderReaderLine } from "./agents.js";
|
|
7
|
+
import { removalFor, removalHolds, removalWords, removedLine, removedNotice, takeTold } from "./removed.js";
|
|
7
8
|
export class WorkerFleet {
|
|
8
9
|
opts;
|
|
9
10
|
dupSaid = new Set();
|
|
@@ -57,10 +58,12 @@ export class WorkerFleet {
|
|
|
57
58
|
this.stopKeeper();
|
|
58
59
|
else
|
|
59
60
|
this.startKeeper(f);
|
|
60
|
-
this.alertChecks(f, Object.values(f.spaces ?? {}).filter((m) => (idCount.get(m.id) ?? 0) === 1 && !wanted.has(m.id) && this.keeper?.id !== m.id));
|
|
61
|
+
this.alertChecks(f, Object.values(f.spaces ?? {}).filter((m) => (idCount.get(m.id) ?? 0) === 1 && !wanted.has(m.id) && this.keeper?.id !== m.id && !this.removedFrom(configFor(f, m, null))));
|
|
61
62
|
for (const [id, w] of wanted) {
|
|
62
63
|
if (this.slots.has(id))
|
|
63
64
|
continue;
|
|
65
|
+
if (this.removedFrom(w.cfg))
|
|
66
|
+
continue;
|
|
64
67
|
const home = this.opts.home ?? CONFIG_DIR;
|
|
65
68
|
const { pollMs: _p, readConfig: _r, makeWorker: _m, home: _h, alertsEveryMs: _a, ...rest } = this.opts;
|
|
66
69
|
const wopts = {
|
|
@@ -90,9 +93,36 @@ export class WorkerFleet {
|
|
|
90
93
|
}
|
|
91
94
|
return { started, stopped };
|
|
92
95
|
}
|
|
96
|
+
removedFrom(cfg) {
|
|
97
|
+
let r = null;
|
|
98
|
+
const team = { space: cfg.space, spaceId: cfg.spaceId ?? null, relay: cfg.relay, member: cfg.member };
|
|
99
|
+
try {
|
|
100
|
+
r = removalFor(team);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (!removalHolds(r)) {
|
|
106
|
+
if (r)
|
|
107
|
+
this.removedSaid.delete(r.team);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (!this.removedSaid.has(r.team)) {
|
|
111
|
+
this.removedSaid.add(r.team);
|
|
112
|
+
const words = removalWords(team, r.by);
|
|
113
|
+
this.opts.log(`fleet: [${cfg.space}] ${removedLine(words)}`);
|
|
114
|
+
try {
|
|
115
|
+
if (this.opts.notify && takeTold(team))
|
|
116
|
+
this.opts.notify(removedNotice(words));
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
removedSaid = new Set();
|
|
93
123
|
startKeeper(f) {
|
|
94
|
-
const teams = Object.values(f.spaces ?? {});
|
|
95
|
-
const m = (f.default_space ?
|
|
124
|
+
const teams = Object.values(f.spaces ?? {}).filter((t) => !this.removedFrom(configFor(f, t, null)));
|
|
125
|
+
const m = (f.default_space ? teams.find((t) => t.id === f.default_space) : undefined) ?? teams[0];
|
|
96
126
|
if (!m) {
|
|
97
127
|
this.stopKeeper();
|
|
98
128
|
return;
|
package/dist/worker-policy.js
CHANGED
|
@@ -4,6 +4,53 @@ import { CONFIG_PATH } from "./config.js";
|
|
|
4
4
|
import { trustWrite } from "./home-guard.js";
|
|
5
5
|
import { parseJsonText } from "./text-file.js";
|
|
6
6
|
import { compareVersions, isVersion } from "./installer.js";
|
|
7
|
+
export const IDLE_SECONDS = { default: 120, min: 0, max: 900 };
|
|
8
|
+
export function parseIdleSeconds(raw) {
|
|
9
|
+
if (raw === undefined || raw === null)
|
|
10
|
+
return null;
|
|
11
|
+
const s = String(raw).trim();
|
|
12
|
+
if (!/^\d+$/.test(s))
|
|
13
|
+
return null;
|
|
14
|
+
const n = Number(s);
|
|
15
|
+
return n >= IDLE_SECONDS.min && n <= IDLE_SECONDS.max ? n : null;
|
|
16
|
+
}
|
|
17
|
+
export function idleSecondsProblem(raw) {
|
|
18
|
+
return `"${raw ?? ""}" is not an idle wait — give a whole number of seconds from ${IDLE_SECONDS.min} to ${IDLE_SECONDS.max} (the default is ${IDLE_SECONDS.default}; back to it: pantheon policy set idle-seconds default)`;
|
|
19
|
+
}
|
|
20
|
+
export function effectiveIdleSeconds(o) {
|
|
21
|
+
if (o.flag !== undefined)
|
|
22
|
+
return { seconds: o.flag, from: "flag" };
|
|
23
|
+
if (o.policy?.idleSeconds !== undefined)
|
|
24
|
+
return { seconds: o.policy.idleSeconds, from: "policy" };
|
|
25
|
+
return { seconds: IDLE_SECONDS.default, from: "default" };
|
|
26
|
+
}
|
|
27
|
+
export function describeIdleSeconds(s) {
|
|
28
|
+
return s === 0 ? "at once" : s % 60 === 0 ? `${s / 60} minute${s === 60 ? "" : "s"}` : `${s} seconds`;
|
|
29
|
+
}
|
|
30
|
+
export const RUN_MINUTES = { default: 60, min: 5, max: 180 };
|
|
31
|
+
export function parseRunMinutes(raw) {
|
|
32
|
+
if (raw === undefined || raw === null)
|
|
33
|
+
return null;
|
|
34
|
+
const s = String(raw).trim();
|
|
35
|
+
if (!/^\d+$/.test(s))
|
|
36
|
+
return null;
|
|
37
|
+
const n = Number(s);
|
|
38
|
+
return n >= RUN_MINUTES.min && n <= RUN_MINUTES.max ? n : null;
|
|
39
|
+
}
|
|
40
|
+
export function runMinutesProblem(raw) {
|
|
41
|
+
return `"${raw ?? ""}" is not a run limit — give a whole number of minutes from ${RUN_MINUTES.min} to ${RUN_MINUTES.max} (the default is ${RUN_MINUTES.default}; back to it: pantheon policy set run-minutes default)`;
|
|
42
|
+
}
|
|
43
|
+
export function effectiveRunMinutes(o) {
|
|
44
|
+
const machine = o.flag !== undefined ? { minutes: o.flag, from: "flag" }
|
|
45
|
+
: o.policy?.runMinutes !== undefined ? { minutes: o.policy.runMinutes, from: "policy" }
|
|
46
|
+
: { minutes: RUN_MINUTES.default, from: "default" };
|
|
47
|
+
return o.persona !== undefined && o.persona < machine.minutes ? { minutes: o.persona, from: "persona" } : machine;
|
|
48
|
+
}
|
|
49
|
+
export function describeRunMinutes(r, persona) {
|
|
50
|
+
const why = r.from === "default" ? "the default" : r.from === "policy" ? "set on this machine"
|
|
51
|
+
: r.from === "flag" ? "this worker's --run-minutes" : `lowered by persona ${persona ?? ""}`.trim();
|
|
52
|
+
return `${r.minutes} minutes per run (${why})`;
|
|
53
|
+
}
|
|
7
54
|
export const WAKE_NO_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
8
55
|
export const wakeKey = (team, member) => `${team}|${member}`;
|
|
9
56
|
export function mayAskWake(p, teams, member, now = Date.now()) {
|
|
@@ -41,6 +88,15 @@ export function policyHome(home) {
|
|
|
41
88
|
export function policyPath(home) {
|
|
42
89
|
return join(policyHome(home), "worker-policy.json");
|
|
43
90
|
}
|
|
91
|
+
export function readinessRecheckPath(home) {
|
|
92
|
+
return join(policyHome(home), "readiness-recheck.stamp");
|
|
93
|
+
}
|
|
94
|
+
export function requestReadinessRecheck(home) {
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(readinessRecheckPath(home), `${Date.now()}\n`, { mode: 0o600 });
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
}
|
|
44
100
|
const isRecord = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
45
101
|
function readWakers(x) {
|
|
46
102
|
if (!isRecord(x))
|
|
@@ -71,6 +127,8 @@ export function loadPolicy(home) {
|
|
|
71
127
|
...(isRecord(raw.wakeTeamFlag) ? { wakeTeamFlag: Object.fromEntries(Object.entries(raw.wakeTeamFlag).filter(([, v]) => typeof v === "number")) } : {}),
|
|
72
128
|
...(isRecord(raw.upgradeHeld) && typeof raw.upgradeHeld.version === "string" && typeof raw.upgradeHeld.node === "string"
|
|
73
129
|
? { upgradeHeld: { version: raw.upgradeHeld.version, node: raw.upgradeHeld.node, at: Number(raw.upgradeHeld.at) || 0, why: String(raw.upgradeHeld.why ?? "") } } : {}),
|
|
130
|
+
...(typeof raw.runMinutes === "number" && parseRunMinutes(raw.runMinutes) !== null ? { runMinutes: raw.runMinutes } : {}),
|
|
131
|
+
...(typeof raw.idleSeconds === "number" && parseIdleSeconds(raw.idleSeconds) !== null ? { idleSeconds: raw.idleSeconds } : {}),
|
|
74
132
|
};
|
|
75
133
|
}
|
|
76
134
|
catch {
|