@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/notifier.js
CHANGED
|
@@ -14,6 +14,7 @@ import { loadPolicy } from "./worker-policy.js";
|
|
|
14
14
|
import { wakeAlerted } from "./continued.js";
|
|
15
15
|
import { cliOnPath, detectAvailable } from "./agents.js";
|
|
16
16
|
import { whichPantheon } from "./service.js";
|
|
17
|
+
import { removalFor, removalHolds, removalRefusal, removalWords, removedLine, removedNotice, takeTold } from "./removed.js";
|
|
17
18
|
export const DEFAULT_REMINDER_MS = 4 * 60 * 60 * 1_000;
|
|
18
19
|
export const PASSED_OVER_EVERY_MS = 3_600_000;
|
|
19
20
|
export const MAX_REMINDERS = 4;
|
|
@@ -145,7 +146,37 @@ export class HumanNotifier {
|
|
|
145
146
|
stop() {
|
|
146
147
|
this.stopped = true;
|
|
147
148
|
}
|
|
149
|
+
async waitWhileRemoved() {
|
|
150
|
+
const team = { space: this.cfg.space, spaceId: this.cfg.spaceId ?? null, relay: this.cfg.relay, member: this.cfg.member };
|
|
151
|
+
const now = this.opts.now ?? Date.now;
|
|
152
|
+
let said = false;
|
|
153
|
+
while (!this.stopped) {
|
|
154
|
+
let r = null;
|
|
155
|
+
try {
|
|
156
|
+
r = removalFor(team);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!removalHolds(r, now()))
|
|
162
|
+
return;
|
|
163
|
+
if (!said) {
|
|
164
|
+
said = true;
|
|
165
|
+
const words = removalWords(team, r.by);
|
|
166
|
+
this.opts.log(`! ${removedLine(words)}`);
|
|
167
|
+
try {
|
|
168
|
+
if (takeTold(team))
|
|
169
|
+
await (this.opts.send ?? sendDesktopNotification)(removedNotice(words));
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
}
|
|
173
|
+
await new Promise((res) => setTimeout(res, this.opts.removedPollMs ?? 60_000));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
148
176
|
async run() {
|
|
177
|
+
await this.waitWhileRemoved();
|
|
178
|
+
if (this.stopped)
|
|
179
|
+
return;
|
|
149
180
|
const client = new PantheonClient(this.cfg, undefined, { role: "cli" });
|
|
150
181
|
await client.connect();
|
|
151
182
|
const stateFile = this.opts.stateFile ?? join(CONFIG_DIR, "notifier-state.json");
|
|
@@ -155,7 +186,21 @@ export class HumanNotifier {
|
|
|
155
186
|
let state = readState(stateFile);
|
|
156
187
|
this.opts.log(`notifier online as ${this.cfg.member}`);
|
|
157
188
|
this.opts.log(` reminders after ${Math.round(this.opts.reminderMs / 60_000)} minutes, then doubling to daily, ${MAX_REMINDERS} at most — after that a decision waits on pantheon brief and the menu bar`);
|
|
189
|
+
let removedSaid = false;
|
|
158
190
|
while (!this.stopped) {
|
|
191
|
+
if (client.removedFrom && !removedSaid) {
|
|
192
|
+
removedSaid = true;
|
|
193
|
+
const team = { space: this.cfg.space, spaceId: this.cfg.spaceId ?? null, relay: this.cfg.relay, member: this.cfg.member };
|
|
194
|
+
const r = removalWords(team, client.removedFrom.by);
|
|
195
|
+
this.opts.log(`! ${removedLine(r)}`);
|
|
196
|
+
try {
|
|
197
|
+
if (takeTold(team))
|
|
198
|
+
await send(removedNotice(r));
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
}
|
|
202
|
+
else if (!client.removedFrom)
|
|
203
|
+
removedSaid = false;
|
|
159
204
|
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member, now());
|
|
160
205
|
let dirty = JSON.stringify(pruned) !== JSON.stringify(state);
|
|
161
206
|
state = pruned;
|
|
@@ -268,22 +313,27 @@ export async function runNotifiers(teams, o) {
|
|
|
268
313
|
let wait = o.retryMs ?? 30_000;
|
|
269
314
|
for (;;) {
|
|
270
315
|
failing.delete(i);
|
|
316
|
+
let removed = false;
|
|
271
317
|
try {
|
|
272
318
|
await t.run();
|
|
273
319
|
return;
|
|
274
320
|
}
|
|
275
321
|
catch (e) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
322
|
+
removed = !!removalRefusal(e.message);
|
|
323
|
+
if (!removed) {
|
|
324
|
+
failing.add(i);
|
|
325
|
+
o.log(`[${t.label}] ! not connecting: ${e.message.split("\n")[0]}`);
|
|
326
|
+
if (failing.size === teams.length) {
|
|
327
|
+
gaveUp = true;
|
|
328
|
+
giveUp();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
333
|
if (gaveUp || t.stopped?.())
|
|
285
334
|
return;
|
|
286
|
-
|
|
335
|
+
if (!removed)
|
|
336
|
+
o.log(`[${t.label}] the other teams' alerts keep running; trying this one again in ${Math.round(wait / 1000)}s`);
|
|
287
337
|
await sleep(wait);
|
|
288
338
|
if (gaveUp || t.stopped?.())
|
|
289
339
|
return;
|
package/dist/persona.js
CHANGED
|
@@ -2,9 +2,10 @@ import { lstatSync, readFileSync, readdirSync, realpathSync, writeFileSync, exis
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { CONFIG_DIR } from "./config.js";
|
|
5
|
+
import { parseRunMinutes, RUN_MINUTES } from "./worker-policy.js";
|
|
5
6
|
export const PERSONA_INSTRUCTIONS_MAX = 8_000;
|
|
6
7
|
const KNOWN_KEYS = new Set([
|
|
7
|
-
"allow_bash", "max_runs_per_hour", "max_runs_per_day",
|
|
8
|
+
"allow_bash", "max_runs_per_hour", "max_runs_per_day", "max_run_minutes",
|
|
8
9
|
"wake_by", "kinds", "goals", "agent", "name", "spaces",
|
|
9
10
|
]);
|
|
10
11
|
const list = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -55,6 +56,14 @@ export function parsePersona(name, scope, file, raw) {
|
|
|
55
56
|
p.warnings.push(`${key} must be a number, got "${value}"`);
|
|
56
57
|
break;
|
|
57
58
|
}
|
|
59
|
+
case "max_run_minutes": {
|
|
60
|
+
const n = parseRunMinutes(value);
|
|
61
|
+
if (n !== null)
|
|
62
|
+
p.maxRunMinutes = n;
|
|
63
|
+
else
|
|
64
|
+
p.warnings.push(`max_run_minutes must be a whole number of minutes from ${RUN_MINUTES.min} to ${RUN_MINUTES.max}, got "${value}"`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
58
67
|
case "wake_by":
|
|
59
68
|
p.wakeBy = list(value);
|
|
60
69
|
break;
|
|
@@ -212,6 +221,7 @@ export const PERSONA_TEMPLATE = `---
|
|
|
212
221
|
# allow_bash: false # runs handled here never get shell, even with --allow-bash
|
|
213
222
|
# max_runs_per_hour: 6
|
|
214
223
|
# max_runs_per_day: 20
|
|
224
|
+
# max_run_minutes: 30 # a run it handles is stopped after this long (only lowers the machine's limit)
|
|
215
225
|
# wake_by: alice, bob # only these peers' requests are handled by this persona
|
|
216
226
|
# kinds: ANSWER, ACCEPT # ANSWER (asks), ACCEPT (verdicts), REVISE (objections), CONTINUE (replies/deliveries)
|
|
217
227
|
# goals: launch-* # only goals whose id or title matches
|
|
@@ -239,24 +249,38 @@ export function initPersona(name, scope, projectDir = process.cwd(), home) {
|
|
|
239
249
|
writeFileSync(file, PERSONA_TEMPLATE);
|
|
240
250
|
return file;
|
|
241
251
|
}
|
|
252
|
+
export function olderTeammatesNote(p) {
|
|
253
|
+
if (p.scope !== "project" || p.broken)
|
|
254
|
+
return null;
|
|
255
|
+
const newer = p.maxRunMinutes !== undefined ? ["max_run_minutes"] : [];
|
|
256
|
+
if (!newer.length)
|
|
257
|
+
return null;
|
|
258
|
+
return `uses ${newer.join(", ")}, which Pantheon before 0.33.6 does not know: a teammate's worker on an older version reads this committed persona as broken and skips it until they upgrade (pantheon upgrade). Keep it in your own ~/.pantheon/personas instead, or wait until the team is on 0.33.6`;
|
|
259
|
+
}
|
|
242
260
|
export function describePersona(p) {
|
|
243
|
-
const
|
|
261
|
+
const filters = [];
|
|
262
|
+
if (p.spaces?.length)
|
|
263
|
+
filters.push(`spaces ${p.spaces.join(" / ")}`);
|
|
244
264
|
if (p.wakeBy?.length)
|
|
245
|
-
|
|
265
|
+
filters.push(`wake_by ${p.wakeBy.join(",")}`);
|
|
246
266
|
if (p.kinds?.length)
|
|
247
|
-
|
|
267
|
+
filters.push(`kinds ${p.kinds.join(",")}`);
|
|
248
268
|
if (p.goals?.length)
|
|
249
|
-
|
|
269
|
+
filters.push(`goals ${p.goals.join(",")}`);
|
|
270
|
+
const limits = [];
|
|
250
271
|
if (p.allowBash === false)
|
|
251
|
-
|
|
272
|
+
limits.push("bash off");
|
|
252
273
|
if (p.maxRunsPerHour !== undefined)
|
|
253
|
-
|
|
274
|
+
limits.push(`≤${p.maxRunsPerHour}/h`);
|
|
254
275
|
if (p.maxRunsPerDay !== undefined)
|
|
255
|
-
|
|
276
|
+
limits.push(`≤${p.maxRunsPerDay}/d`);
|
|
277
|
+
if (p.maxRunMinutes !== undefined)
|
|
278
|
+
limits.push(`≤${p.maxRunMinutes}min/run`);
|
|
256
279
|
if (p.agent)
|
|
257
|
-
|
|
280
|
+
limits.push(`agent ${p.agent}`);
|
|
258
281
|
if (p.broken)
|
|
259
282
|
return `${p.name} (${p.scope}) BROKEN — takes no work until fixed: ${p.broken}`;
|
|
260
|
-
|
|
283
|
+
const rules = filters.length ? [...filters, ...limits].join(", ") : `matches everything${limits.length ? `; ${limits.join(", ")}` : ""}`;
|
|
284
|
+
return `${p.name} (${p.scope}) ${rules}`;
|
|
261
285
|
}
|
|
262
286
|
//# sourceMappingURL=persona.js.map
|
package/dist/pickup.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { LIMITS, WORKER_REASONS } from "./protocol.js";
|
|
2
|
+
export function inWords(ms) {
|
|
3
|
+
if (ms < 60_000)
|
|
4
|
+
return "under a minute";
|
|
5
|
+
const m = Math.round(ms / 60_000);
|
|
6
|
+
if (m < 60)
|
|
7
|
+
return `about ${m} minute${m === 1 ? "" : "s"}`;
|
|
8
|
+
const h = Math.round(m / 60);
|
|
9
|
+
return `about ${h} hour${h === 1 ? "" : "s"}`;
|
|
10
|
+
}
|
|
11
|
+
const UNAVAILABLE = {
|
|
12
|
+
no_agent: "Claude Code (or another agent CLI) isn't installed there",
|
|
13
|
+
kiro_only: "only Kiro is installed there, and Kiro answers in live sessions only",
|
|
14
|
+
not_signed_in: "its agent CLI isn't signed in",
|
|
15
|
+
unsandboxed_off: "its only agent has no sandbox, and it is set to leave requests for its person",
|
|
16
|
+
no_agent_choice: "no agent there can be chosen for a run",
|
|
17
|
+
sandbox_unavailable: "its Linux sandbox can't start",
|
|
18
|
+
};
|
|
19
|
+
function reasonOf(w) {
|
|
20
|
+
if (w.reason && WORKER_REASONS.includes(w.reason))
|
|
21
|
+
return w.reason;
|
|
22
|
+
const m = /\[([a-z_]{3,40})\]\s*$/.exec(w.detail ?? "");
|
|
23
|
+
return m && WORKER_REASONS.includes(m[1]) ? m[1] : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function pickupEstimate(peer, o = {}) {
|
|
26
|
+
const now = o.now ?? Date.now();
|
|
27
|
+
const who = peer.id;
|
|
28
|
+
const live = (peer.sessions ?? []).filter((s) => s.role === "interactive").length > 0;
|
|
29
|
+
const workerOn = (peer.sessions ?? []).some((s) => s.role === "worker");
|
|
30
|
+
if (o.mayWake === false) {
|
|
31
|
+
return { kind: "no-wake", says: `${who}'s worker does not take your requests (the team does not let you wake it), so this waits for ${who}${live ? ", who has a live session open" : ""}` };
|
|
32
|
+
}
|
|
33
|
+
if (!workerOn) {
|
|
34
|
+
return { kind: "offline", says: live
|
|
35
|
+
? `${who}'s worker is offline; ${who} has a live session open, which sees this when their agent next looks`
|
|
36
|
+
: `${who}'s worker is offline, so this waits until ${who} is back` };
|
|
37
|
+
}
|
|
38
|
+
const w = peer.worker && now - peer.worker.at < 15 * 60_000 ? peer.worker : undefined;
|
|
39
|
+
const reason = w ? reasonOf(w) : undefined;
|
|
40
|
+
if (w && (w.state === "unavailable" || (reason && UNAVAILABLE[reason]))) {
|
|
41
|
+
return { kind: "unavailable", says: `${who}'s machine can't take requests yet${reason && UNAVAILABLE[reason] ? `: ${UNAVAILABLE[reason]}` : ""}` };
|
|
42
|
+
}
|
|
43
|
+
if (w?.state === "running") {
|
|
44
|
+
const mine = !!o.about && ((!!o.about.ref && w.ref === o.about.ref) || (!!o.about.goal && !w.ref && w.goal === o.about.goal));
|
|
45
|
+
return mine
|
|
46
|
+
? { kind: "working", says: `${who}'s agent is working on this now` }
|
|
47
|
+
: { kind: "queued", says: `${who}'s worker is busy with another request; this one is next` };
|
|
48
|
+
}
|
|
49
|
+
const idleMs = typeof w?.idle_ms === "number" ? w.idle_ms : undefined;
|
|
50
|
+
const wait = (ms) => (idleMs !== undefined ? inWords(ms) : `${inWords(ms)} (if they kept the default wait)`);
|
|
51
|
+
const lifts = w?.state === "holding" && w.until && w.until > now ? w.until - now : null;
|
|
52
|
+
if (w?.state === "holding" && reason === "capped") {
|
|
53
|
+
return { kind: "holding", says: `${who}'s worker has used its runs for now (its hourly or daily cap)${lifts ? `; it can take this in ${inWords(lifts)}` : ", so this waits"}` };
|
|
54
|
+
}
|
|
55
|
+
if (w?.state === "holding" && reason === "outage") {
|
|
56
|
+
return { kind: "holding", says: `${who}'s agent is out for now (a usage limit or an outage)${lifts ? `; it should be back in ${inWords(lifts)}` : ""}` };
|
|
57
|
+
}
|
|
58
|
+
if (w?.state === "holding" && reason === "awaiting_wake_approval") {
|
|
59
|
+
return { kind: "holding", says: `${who}'s machine may be waiting for ${who} to approve a teammate before it starts their work` };
|
|
60
|
+
}
|
|
61
|
+
if (w?.state === "holding" && reason === "busy_elsewhere") {
|
|
62
|
+
return { kind: "queued", says: `${who}'s machine is busy with another request (possibly for another team); this one waits until it finishes` };
|
|
63
|
+
}
|
|
64
|
+
if (w?.state === "holding" && reason === "unknown") {
|
|
65
|
+
return { kind: "holding", says: `${who}'s worker is holding requests for now` };
|
|
66
|
+
}
|
|
67
|
+
if (lifts)
|
|
68
|
+
return { kind: "holding", says: `${who}'s agent will pick this up in ${inWords(lifts)}` };
|
|
69
|
+
if (live && (peer.work?.busy || reason === "busy_turn")) {
|
|
70
|
+
return { kind: "keyboard", says: `${who} is at their keyboard; their agent picks this up ${wait(idleMs ?? LIMITS.workerIdleMs)} after they go quiet` };
|
|
71
|
+
}
|
|
72
|
+
if (live && peer.work?.idle_since) {
|
|
73
|
+
const left = peer.work.idle_since + (idleMs ?? LIMITS.workerIdleMs) - now;
|
|
74
|
+
if (left > 0)
|
|
75
|
+
return { kind: "keyboard", says: `${who} was at their keyboard moments ago; their agent picks this up in ${wait(left)} if they stay quiet` };
|
|
76
|
+
}
|
|
77
|
+
if (live && (!peer.work || reason === "no_signal")) {
|
|
78
|
+
return { kind: "keyboard", says: `${who} has a live session open (with no activity signal), so their agent sees this there; their worker takes it once that session closes` };
|
|
79
|
+
}
|
|
80
|
+
return { kind: "soon", says: `${who}'s agent should pick this up within a minute, if nothing on their machine holds it` };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=pickup.js.map
|
package/dist/profile.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
import { isCatchAllDomain } from "./protocol.js";
|
|
4
5
|
export const AGENT_SURFACES = ["codex", "claude-code", "cursor", "copilot", "kiro"];
|
|
5
6
|
export const SURFACE_BUNDLE_IDS = {
|
|
6
7
|
codex: "com.openai.codex",
|
|
@@ -176,6 +177,28 @@ export function teamProfile(profile, ...forSpace) {
|
|
|
176
177
|
export function profileForTeam(cfg, file = profilePath()) {
|
|
177
178
|
return teamProfile(loadProfile(cfg, file), cfg.spaceId, cfg.space);
|
|
178
179
|
}
|
|
180
|
+
export function dropCatchAllClaims(profile) {
|
|
181
|
+
const dropped = new Set();
|
|
182
|
+
const clean = (a) => {
|
|
183
|
+
if (!a)
|
|
184
|
+
return a;
|
|
185
|
+
const out = {};
|
|
186
|
+
for (const [d, m] of Object.entries(a)) {
|
|
187
|
+
if (isCatchAllDomain(d))
|
|
188
|
+
dropped.add(d);
|
|
189
|
+
else
|
|
190
|
+
out[d] = m;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
};
|
|
194
|
+
const authority = clean(profile.authority);
|
|
195
|
+
const spaces = profile.spaces
|
|
196
|
+
? Object.fromEntries(Object.entries(profile.spaces).map(([k, v]) => [k, v.authority ? { ...v, authority: clean(v.authority) } : v]))
|
|
197
|
+
: undefined;
|
|
198
|
+
if (!dropped.size)
|
|
199
|
+
return { profile, dropped: [] };
|
|
200
|
+
return { profile: { ...profile, authority, ...(spaces ? { spaces } : {}) }, dropped: [...dropped].sort() };
|
|
201
|
+
}
|
|
179
202
|
export function organizationProfile(profile, ...forSpace) {
|
|
180
203
|
const team = teamProfile(profile, ...forSpace);
|
|
181
204
|
return {
|
|
@@ -185,7 +208,7 @@ export function organizationProfile(profile, ...forSpace) {
|
|
|
185
208
|
title: profile.human.title,
|
|
186
209
|
responsibilities: cleanList(team.human.responsibilities),
|
|
187
210
|
authority: Object.fromEntries(Object.entries(team.authority)
|
|
188
|
-
.map(([domain, mode]) => [normalize(domain), mode]).filter(([domain]) => !!domain)),
|
|
211
|
+
.map(([domain, mode]) => [normalize(domain), mode]).filter(([domain]) => !!domain && !isCatchAllDomain(domain))),
|
|
189
212
|
decision_keywords: cleanKeywords(team.decision_keywords),
|
|
190
213
|
capabilities: cleanList(profile.capabilities),
|
|
191
214
|
delegations: cleanDelegations(profile.delegations),
|
package/dist/protocol.js
CHANGED
|
@@ -24,6 +24,15 @@ export const UPKEEP_AGENT = "pantheon-upkeep";
|
|
|
24
24
|
export function siblingSessions(roster, member, session) {
|
|
25
25
|
return (roster.find((m) => m.id === member)?.sessions ?? []).filter((s) => s.id !== session && s.agent !== UPKEEP_AGENT);
|
|
26
26
|
}
|
|
27
|
+
export const CATCH_ALL_DOMAINS = [
|
|
28
|
+
"all", "*", "everything", "every", "any", "anything", "everyone", "anyone", "whatever",
|
|
29
|
+
"none", "nothing", "n/a", "all-domains", "all_domains",
|
|
30
|
+
];
|
|
31
|
+
export const normalizeDomainName = (d) => d.normalize("NFKC").trim().toLowerCase();
|
|
32
|
+
export function isCatchAllDomain(domain) {
|
|
33
|
+
const d = normalizeDomainName(domain);
|
|
34
|
+
return !d || CATCH_ALL_DOMAINS.includes(d) || /[*?]/.test(d);
|
|
35
|
+
}
|
|
27
36
|
export const LIMITS = {
|
|
28
37
|
bodyChars: 32000,
|
|
29
38
|
summaryRequiredOver: 2000,
|
|
@@ -116,7 +125,7 @@ export const GoalSigSchema = z.object({
|
|
|
116
125
|
kind: z.string().max(32).nullable().optional(),
|
|
117
126
|
cert: DelegateCertSchema.optional(),
|
|
118
127
|
});
|
|
119
|
-
export const RELAY_FEATURES = ["act-sig", "delegate", "named-decider"];
|
|
128
|
+
export const RELAY_FEATURES = ["act-sig", "delegate", "named-decider", "worker-state-2"];
|
|
120
129
|
export const Hello = z.object({
|
|
121
130
|
t: z.literal("hello"),
|
|
122
131
|
v: z.number().int().optional(),
|
|
@@ -236,11 +245,19 @@ export const DeleteSpace = z.object({
|
|
|
236
245
|
cid: z.string().max(64).optional(),
|
|
237
246
|
confirm: z.string().min(1).max(64),
|
|
238
247
|
});
|
|
248
|
+
export const WORKER_REASONS = ["no_agent", "kiro_only", "not_signed_in", "unsandboxed_off", "no_agent_choice",
|
|
249
|
+
"busy_turn", "recently_active", "no_signal", "capped", "outage", "awaiting_wake_approval", "busy_elsewhere", "unknown", "sandbox_unavailable"];
|
|
250
|
+
const WORK_ID = /^(cont:)?[a-z]{1,3}_[A-Za-z0-9_-]{4,64}$/;
|
|
239
251
|
export const WorkerState = z.object({
|
|
240
252
|
t: z.literal("worker_state"),
|
|
241
|
-
state: z.enum(["idle", "holding", "running"]),
|
|
253
|
+
state: z.enum(["idle", "holding", "running", "unavailable"]),
|
|
242
254
|
detail: z.string().max(200).optional(),
|
|
243
255
|
until: z.number().int().positive().optional(),
|
|
256
|
+
idle_ms: z.number().int().min(0).max(3_600_000).optional(),
|
|
257
|
+
reason: z.string().regex(/^[a-z_]{1,40}$/).optional(),
|
|
258
|
+
ref: z.string().regex(WORK_ID).optional(),
|
|
259
|
+
goal: z.string().regex(WORK_ID).optional(),
|
|
260
|
+
until_in: z.number().int().min(0).max(7 * 24 * 3600_000).optional(),
|
|
244
261
|
});
|
|
245
262
|
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, ListInvites, RevokeInvite, PublishProfile, ClearAuthority, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
246
263
|
export function shouldDeferToHuman(sessions, work, opts = {}) {
|