@join-pantheon/cli 0.20.0 → 0.21.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 +21 -4
- package/dist/accounts.js +136 -9
- package/dist/agents.js +4 -0
- package/dist/cli.js +155 -18
- package/dist/client.js +23 -8
- package/dist/config.js +14 -0
- package/dist/doctor.js +9 -0
- package/dist/helpers-build.js +23 -0
- package/dist/mcp.js +43 -9
- package/dist/notifier.js +29 -6
- package/dist/notify.js +1 -1
- package/dist/pending.js +3 -1
- package/dist/plans.js +46 -0
- package/dist/profile.js +5 -0
- package/dist/protocol.js +13 -1
- package/dist/quickstart.js +2 -1
- package/dist/relay.js +247 -28
- package/dist/roster-file.js +15 -2
- package/dist/service.js +14 -0
- package/dist/setup-guide.js +268 -0
- package/dist/tray.js +3 -0
- package/dist/untrusted.js +16 -3
- package/dist/upgrade.js +11 -1
- package/dist/worker.js +78 -13
- package/helper/macos/menubar.swift +47 -14
- package/package.json +3 -3
package/dist/config.js
CHANGED
|
@@ -153,6 +153,20 @@ export function resolveFor(cwd = process.cwd(), opts = {}) {
|
|
|
153
153
|
}
|
|
154
154
|
return { cfg: null, reason: "unbound", memberships: n, repo };
|
|
155
155
|
}
|
|
156
|
+
export function resolveForHook(cwd = process.cwd()) {
|
|
157
|
+
const r = resolveFor(cwd, { strict: true });
|
|
158
|
+
if (!r.cfg?.space)
|
|
159
|
+
return null;
|
|
160
|
+
if (r.reason !== "single" || !r.repo)
|
|
161
|
+
return r.cfg;
|
|
162
|
+
const f = loadAll();
|
|
163
|
+
const m = Object.values(f?.spaces ?? {})[0];
|
|
164
|
+
const bound = Object.values(f?.bindings ?? {}).filter((b) => b.space === m?.id);
|
|
165
|
+
if (!bound.length)
|
|
166
|
+
return r.cfg;
|
|
167
|
+
const here = r.repo;
|
|
168
|
+
return bound.some((b) => b.path === here.root || (!!b.origin && b.origin === here.origin)) ? r.cfg : null;
|
|
169
|
+
}
|
|
156
170
|
export function load() {
|
|
157
171
|
return resolveFor(process.cwd()).cfg;
|
|
158
172
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { loadProfile, profileInitialized } from "./profile.js";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { PantheonClient } from "./client.js";
|
|
@@ -74,6 +75,14 @@ export async function doctor(projectDir) {
|
|
|
74
75
|
verifyChallenge(cfg.publicKey, cfg.space, cfg.member, nonce, sig)
|
|
75
76
|
? add("pass", "identity", `keypair valid — fingerprint ${fingerprint(cfg.publicKey)}`)
|
|
76
77
|
: add("fail", "identity", "your keypair does not verify against itself", "the config is corrupt — re-run `pantheon join` to mint a new identity, then get re-admitted");
|
|
78
|
+
const prof = loadProfile(cfg);
|
|
79
|
+
if (profileInitialized(prof)) {
|
|
80
|
+
const owns = Object.entries(prof.authority).filter(([, mode]) => mode === "human_required").map(([d]) => d);
|
|
81
|
+
add("pass", "profile", `${prof.human.display_name} — ${prof.human.title}${owns.length ? `; you personally decide: ${owns.join(", ")}` : "; no human-required domains yet"}`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
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>");
|
|
85
|
+
}
|
|
77
86
|
const mcpFile = join(projectDir, ".mcp.json");
|
|
78
87
|
if (!existsSync(mcpFile)) {
|
|
79
88
|
add("warn", "mcp entry", "no .mcp.json in this project", "run `pantheon install`");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
export const helperHashPath = (binary) => `${binary}.source.sha256`;
|
|
4
|
+
export function sourceHash(source) {
|
|
5
|
+
return createHash("sha256").update(readFileSync(source)).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
export function recordHelperBuild(binary, source) {
|
|
8
|
+
try {
|
|
9
|
+
writeFileSync(helperHashPath(binary), sourceHash(source) + "\n");
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
}
|
|
13
|
+
export function helperStale(binary, source) {
|
|
14
|
+
if (!existsSync(binary) || !existsSync(source))
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(helperHashPath(binary), "utf8").trim() !== sourceHash(source);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=helpers-build.js.map
|
package/dist/mcp.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { PantheonClient } from "./client.js";
|
|
5
|
-
import { fenceBlock, fenceRubric } from "./untrusted.js";
|
|
5
|
+
import { fenceBlock, fenceRubric, assess, fenceReminder } from "./untrusted.js";
|
|
6
6
|
import { resolveFor } from "./config.js";
|
|
7
7
|
import { LIMITS, SENDABLE } from "./protocol.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
@@ -92,8 +92,16 @@ RULES
|
|
|
92
92
|
- Answer every ASK addressed to you. An unanswered ASK blocks a peer and eventually stalls the goal.
|
|
93
93
|
- ACCEPT means you actually checked. If something is wrong, OBJECT with the reason.
|
|
94
94
|
- One act per pantheon_send. Keep bodies short and reference file paths instead of pasting code.
|
|
95
|
-
- DELIVERED means pushed, not written: commit, push, then send branch + commit
|
|
96
|
-
see your disk. When a peer DELIVERS to you, fetch their commit
|
|
95
|
+
- DELIVERED means pushed, not written: commit, push, then send branch + commit (+ base: the commit
|
|
96
|
+
you cut from). Your peer cannot see your disk. When a peer DELIVERS to you, fetch their commit,
|
|
97
|
+
check the declared base is an ancestor and nothing unrelated rides along, then ACCEPT or OBJECT.
|
|
98
|
+
- Scope against what the team can see. Grep origin/<branch>, not your working tree: your unpushed
|
|
99
|
+
commits and local edits do not exist for your peer, and naming files from them wastes a round.
|
|
100
|
+
- If your human answers you directly in this session, settle your own ESCALATE yourself: pantheon_send
|
|
101
|
+
ANSWER with ref = the ESCALATE's id. In an interactive session ask your human first; ESCALATE is for
|
|
102
|
+
when they are not here. Never RETRACT an escalation to clear it.
|
|
103
|
+
- Verdict first, evidence after, and short: a peer reads your act in one screen. Say when you edited
|
|
104
|
+
your own human's working tree as a side effect, and leave those edits uncommitted for them.
|
|
97
105
|
- If pantheon_send returns PEER_TURN you have been talking to yourself — go back to pantheon_wait.
|
|
98
106
|
|
|
99
107
|
The relay only moves messages. It has no access to your files, your shell, or your credentials.
|
|
@@ -137,6 +145,8 @@ export async function runMcp() {
|
|
|
137
145
|
const role = process.env.PANTHEON_ROLE === "worker" ? "worker" : "interactive";
|
|
138
146
|
const branch = process.env.PANTHEON_BRANCH;
|
|
139
147
|
const client = new PantheonClient(cfg, undefined, { role });
|
|
148
|
+
let siblingsWarned = false;
|
|
149
|
+
let rubricShown = false;
|
|
140
150
|
const server = new McpServer({ name: "pantheon", version: VERSION }, { instructions: instructions(cfg, role, branch) });
|
|
141
151
|
let connectError = null;
|
|
142
152
|
await client.connect().catch((e) => { connectError = e.message; });
|
|
@@ -211,6 +221,12 @@ export async function runMcp() {
|
|
|
211
221
|
const ref = m?.branch && m?.commit
|
|
212
222
|
? `\n>> fetchable: branch ${m.branch} at ${m.commit} — fetch and check it before you ACCEPT`
|
|
213
223
|
: "";
|
|
224
|
+
const base = m?.base && m?.commit
|
|
225
|
+
? `\n>> declared base ${m.base}: verify it is an ancestor (git merge-base --is-ancestor ${m.base} ${m.commit}) and that nothing unrelated rides along (git log --oneline ${m.base}..${m.commit})`
|
|
226
|
+
: "";
|
|
227
|
+
const unverified = r.act === "HUMAN_REQUIRED" && m?.authority
|
|
228
|
+
? `\n>> authority ${m.authority}: no published profile covers ${(m.unverified_domains ?? []).join(", ") || "these domains"}, so the relay let this reach the named human unverified. Ask them to run pantheon profile set.`
|
|
229
|
+
: "";
|
|
214
230
|
const who = r.session ? `${r.from}[${r.session.slice(-6)}${r.role === "worker" ? "·worker" : ""}]` : r.from;
|
|
215
231
|
const at = new Date(r.ts).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
216
232
|
const safeBody = fenceBlock(who, r.body).block;
|
|
@@ -223,13 +239,25 @@ export async function runMcp() {
|
|
|
223
239
|
.map((o) => `\n!! this ${r.act} dissolves obligation ${o.id}, which you had claimed (${o.body}) — it is no longer owed; stop working on it.`)
|
|
224
240
|
.join("");
|
|
225
241
|
return `[${r.seq}] ${r.act} from ${who}${r.to ? ` to ${r.to.join(",")}` : ""}` +
|
|
226
|
-
`${r.goal ? ` (goal ${r.goal})` : ""} at ${at} id=${r.id}${r.ref ? ` ref=${r.ref}` : ""}${lead}${stale}\n${safeBody}${ref}${dissolved}`;
|
|
242
|
+
`${r.goal ? ` (goal ${r.goal})` : ""} at ${at} id=${r.id}${r.ref ? ` ref=${r.ref}` : ""}${lead}${stale}\n${safeBody}${ref}${base}${unverified}${dissolved}`;
|
|
243
|
+
};
|
|
244
|
+
const fmtAll = (acts) => {
|
|
245
|
+
const flagged = acts.some((a) => assess(a.body).length > 0);
|
|
246
|
+
const rubric = !rubricShown || flagged ? fenceRubric(acts.length) : fenceReminder(acts.length);
|
|
247
|
+
rubricShown = true;
|
|
248
|
+
return acts.map(fmt).join("\n\n---\n\n") + `\n\n${rubric}`;
|
|
227
249
|
};
|
|
228
|
-
const fmtAll = (acts) => acts.map(fmt).join("\n\n---\n\n") + `\n\n${fenceRubric(acts.length)}`;
|
|
229
250
|
const situation = () => {
|
|
230
251
|
const open = [...client.goals.values()].filter((g) => g.state !== "CLOSED");
|
|
231
252
|
const reach = (p) => {
|
|
232
253
|
const head = client.head;
|
|
254
|
+
const w = p.worker;
|
|
255
|
+
const worker = w?.state === "holding"
|
|
256
|
+
? `; 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` : ""}`
|
|
257
|
+
: w?.state === "running" ? `; their worker is running${w.detail ? ` (${w.detail})` : ""}` : "";
|
|
258
|
+
return reachText(p, head) + worker;
|
|
259
|
+
};
|
|
260
|
+
const reachText = (p, head) => {
|
|
233
261
|
if (p.seen_seq === undefined) {
|
|
234
262
|
return p.delivered_seq !== undefined && p.delivered_seq >= head
|
|
235
263
|
? ", delivered but no read-receipt — they have no wake-up hook, so it waits for their human"
|
|
@@ -245,10 +273,13 @@ export async function runMcp() {
|
|
|
245
273
|
const peers = client.roster.filter((m) => m.id !== cfg.member);
|
|
246
274
|
const siblings = (client.roster.find((m) => m.id === cfg.member)?.sessions ?? [])
|
|
247
275
|
.filter((s) => s.id !== client.session);
|
|
276
|
+
const warnSiblings = siblings.length && !siblingsWarned;
|
|
277
|
+
if (siblings.length)
|
|
278
|
+
siblingsWarned = true;
|
|
248
279
|
return [
|
|
249
280
|
`you are: ${cfg.member} (${cfg.agent ?? "unknown agent"}, session ${client.session}, role ${role}) space: ${cfg.space} relay: ${client.connected ? "connected" : "OFFLINE"}`,
|
|
250
281
|
siblings.length
|
|
251
|
-
? `your other sessions: ${siblings.map((s) => `${s.id}[${s.role}]`).join(" ")} — claim work before doing it, or you will duplicate theirs`
|
|
282
|
+
? `your other sessions: ${siblings.map((s) => `${s.id}[${s.role}]`).join(" ")}${warnSiblings ? " — claim work before doing it, or you will duplicate theirs" : ""}`
|
|
252
283
|
: `your other sessions: none`,
|
|
253
284
|
peers.length
|
|
254
285
|
? `peers: ${peers.map((p) => `${p.id} [${p.online ? "online" : "offline"}, ${p.agent}${reach(p)}]`).join(" ")}`
|
|
@@ -319,7 +350,7 @@ export async function runMcp() {
|
|
|
319
350
|
: `ASK ${owner.member}'s agent about ${owner.domains.join(", ")}.`);
|
|
320
351
|
}
|
|
321
352
|
if (route.unmatched_domains.length || !route.domains.length) {
|
|
322
|
-
next.push(`
|
|
353
|
+
next.push(`No published profile owns${route.unmatched_domains.length ? ` ${route.unmatched_domains.join(", ")}` : " these domains"}. If one named peer's human clearly must decide, HUMAN_REQUIRED to them still goes through, marked unverified — and ask them to run pantheon profile set. If it is your own human's call, ESCALATE. Do not guess an owner.`);
|
|
323
354
|
}
|
|
324
355
|
return text(`${JSON.stringify(route, null, 2)}\n\nNEXT\n${next.map((x) => `- ${x}`).join("\n")}`);
|
|
325
356
|
}));
|
|
@@ -362,16 +393,19 @@ export async function runMcp() {
|
|
|
362
393
|
ref: z.string().optional().describe("id of the act you are responding to; required for ANSWER/ACCEPT/OBJECT"),
|
|
363
394
|
branch: z.string().optional().describe("REQUIRED for DELIVERED: the branch you pushed the work to"),
|
|
364
395
|
commit: z.string().optional().describe("REQUIRED for DELIVERED: the commit sha your peer should fetch and check"),
|
|
396
|
+
base: z.string().optional().describe("for DELIVERED: the commit your branch was cut from, so your peer can check nothing unrelated rides along. Worker runs fill this in automatically."),
|
|
365
397
|
remote: z.string().optional().describe("for DELIVERED: the git remote your peer should fetch from (https://, ssh://, or user@host:path). Without it they have to guess that your origin is theirs."),
|
|
366
398
|
defers_to: z.string().optional().describe("on CLOSE: the goal id you are standing down in favour of. The relay refuses this if that goal is closed or is itself deferring, because both sides standing down at once leaves the work with nowhere to live."),
|
|
367
399
|
affects: z.array(z.string()).optional().describe("on PROPOSE: whose agreement this actually needs. Without it EVERY member of the goal owes a verdict — including anyone offline — so a three-person goal stalls on whoever is away."),
|
|
368
400
|
},
|
|
369
|
-
}, async ({ act, body, goal, to, ref, branch, commit, remote, decision_domains, summary, defers_to, affects }) => guard(async () => {
|
|
370
|
-
const
|
|
401
|
+
}, async ({ act, body, goal, to, ref, branch, commit, remote, base, decision_domains, summary, defers_to, affects }) => guard(async () => {
|
|
402
|
+
const baseSha = act === "DELIVERED" ? (base ?? process.env.PANTHEON_BASE) : undefined;
|
|
403
|
+
const meta = branch || commit || remote || baseSha || decision_domains?.length
|
|
371
404
|
? {
|
|
372
405
|
...(branch ? { branch } : {}),
|
|
373
406
|
...(commit ? { commit } : {}),
|
|
374
407
|
...(remote ? { remote } : {}),
|
|
408
|
+
...(baseSha ? { base: baseSha } : {}),
|
|
375
409
|
...(decision_domains?.length ? { decision_domains } : {}),
|
|
376
410
|
}
|
|
377
411
|
: undefined;
|
package/dist/notifier.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { helperStale, recordHelperBuild } from "./helpers-build.js";
|
|
2
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { platform } from "node:os";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -17,10 +18,10 @@ export function dueHumanObligations(obligations, member, state, now, reminderMs)
|
|
|
17
18
|
return last === undefined || now - last >= reminderMs;
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
|
-
export function pruneNotificationState(state, obligations, member) {
|
|
21
|
+
export function pruneNotificationState(state, obligations, member, now = Date.now()) {
|
|
21
22
|
const active = new Set(humanObligations(obligations, member).map((o) => o.id));
|
|
22
23
|
return {
|
|
23
|
-
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id]) => active.has(id))),
|
|
24
|
+
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id, at]) => active.has(id) || (id.startsWith("act:") && now - at < 7 * 86_400_000))),
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
function readState(file) {
|
|
@@ -56,8 +57,10 @@ export function ensureNativeNotifier(home = CONFIG_DIR) {
|
|
|
56
57
|
requireDeveloperTools("notifications", "notifier");
|
|
57
58
|
execFileSync("/usr/bin/swiftc", ["-O", source, "-o", binary], { stdio: "pipe" });
|
|
58
59
|
chmodSync(binary, 0o755);
|
|
60
|
+
recordHelperBuild(binary, source);
|
|
59
61
|
return binary;
|
|
60
62
|
}
|
|
63
|
+
export function notifierStale(home = CONFIG_DIR) { return helperStale(hudBinaryPath(home), hudSourcePath()); }
|
|
61
64
|
export function requireDeveloperTools(what, service) {
|
|
62
65
|
try {
|
|
63
66
|
execFileSync("/usr/bin/xcode-select", ["-p"], { stdio: "pipe" });
|
|
@@ -108,12 +111,15 @@ export class HumanNotifier {
|
|
|
108
111
|
this.opts.log(`notifier online as ${this.cfg.member}`);
|
|
109
112
|
this.opts.log(` reminders every ${Math.round(this.opts.reminderMs / 60_000)} minutes while unresolved`);
|
|
110
113
|
while (!this.stopped) {
|
|
111
|
-
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member);
|
|
114
|
+
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member, now());
|
|
112
115
|
let dirty = JSON.stringify(pruned) !== JSON.stringify(state);
|
|
113
116
|
state = pruned;
|
|
114
|
-
const
|
|
117
|
+
const profile = loadProfile(this.cfg);
|
|
118
|
+
const prefs = profile.notifications ?? { decisions: true, deliveries: false, worker_done: false };
|
|
119
|
+
const due = prefs.decisions
|
|
120
|
+
? dueHumanObligations(client.obligations, this.cfg.member, state, now(), this.opts.reminderMs)
|
|
121
|
+
: [];
|
|
115
122
|
for (const obligation of due) {
|
|
116
|
-
const profile = loadProfile(this.cfg);
|
|
117
123
|
const goal = obligation.goal ? client.goals.get(obligation.goal)?.title : undefined;
|
|
118
124
|
const ok = send("Your input is needed", oneLine(`${this.opts.label ? `${this.opts.label} · ` : ""}${goal ?? "Pantheon decision"}`, 80), decisionTitle(obligation.body, 180), profile, decisionPrompt(obligation.id));
|
|
119
125
|
if (ok) {
|
|
@@ -127,7 +133,24 @@ export class HumanNotifier {
|
|
|
127
133
|
}
|
|
128
134
|
if (dirty || !existsSync(stateFile))
|
|
129
135
|
writeState(stateFile, state);
|
|
130
|
-
await client.next(pollMs);
|
|
136
|
+
const arrived = await client.next(pollMs);
|
|
137
|
+
if (prefs.deliveries) {
|
|
138
|
+
for (const r of arrived) {
|
|
139
|
+
if (r.act !== "DELIVERED" || r.from === this.cfg.member || state.notified_at[`act:${r.id}`])
|
|
140
|
+
continue;
|
|
141
|
+
const goal = r.goal ? client.goals.get(r.goal) : undefined;
|
|
142
|
+
const forMe = (r.to?.includes(this.cfg.member) ?? false) || (goal?.members.includes(this.cfg.member) ?? false);
|
|
143
|
+
if (!forMe)
|
|
144
|
+
continue;
|
|
145
|
+
const m = r.meta;
|
|
146
|
+
const ok = send("Work delivered to you", oneLine(`${this.opts.label ? `${this.opts.label} · ` : ""}${goal?.title ?? "Pantheon delivery"}`, 80), oneLine(`${r.from} pushed ${m?.branch ?? "a branch"}${m?.commit ? ` @ ${m.commit.slice(0, 7)}` : ""} — ${r.body}`, 180), profile, `A teammate delivered work in pantheon: fetch ${m?.branch ?? "their branch"}${m?.commit ? ` at ${m.commit}` : ""} from ${r.from}, check it against the goal "${goal?.title ?? r.goal ?? ""}", then ACCEPT or OBJECT.`, "Delivered");
|
|
147
|
+
if (ok) {
|
|
148
|
+
state.notified_at[`act:${r.id}`] = now();
|
|
149
|
+
writeState(stateFile, state);
|
|
150
|
+
this.opts.log(`> delivery alert for ${r.id} from ${r.from}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
131
154
|
}
|
|
132
155
|
client.close();
|
|
133
156
|
}
|
package/dist/notify.js
CHANGED
|
@@ -290,7 +290,7 @@ export async function decideStop(raw, deps) {
|
|
|
290
290
|
const acts = report.acts ?? [];
|
|
291
291
|
const owed = report.obligations ?? [];
|
|
292
292
|
const closed = new Set(report.closed_goals ?? []);
|
|
293
|
-
const DEMANDS_RESPONSE = new Set(["ASK", "PROPOSE", "OBJECT", "DELIVERED", "CONTRACT"
|
|
293
|
+
const DEMANDS_RESPONSE = new Set(["ASK", "PROPOSE", "OBJECT", "DELIVERED", "CONTRACT"]);
|
|
294
294
|
const work = acts.filter((a) => {
|
|
295
295
|
if (a.goal && closed.has(a.goal))
|
|
296
296
|
return false;
|
package/dist/pending.js
CHANGED
|
@@ -102,10 +102,12 @@ export async function pending(opts) {
|
|
|
102
102
|
const obligations = surfaced.filter((o) => !alreadySeen.has(o.id));
|
|
103
103
|
const head = obligationsBody?.seq;
|
|
104
104
|
const all = (historyBody?.acts ?? []);
|
|
105
|
+
const mine = all.filter((r) => r.from === cfg.member);
|
|
105
106
|
const acts = all
|
|
106
107
|
.filter((r) => r.from !== cfg.member)
|
|
107
108
|
.filter((r) => r.act !== "JOIN" && r.act !== "LEAVE")
|
|
108
|
-
.filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
|
|
109
|
+
.filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
|
|
110
|
+
.filter((r) => !mine.some((m) => m.seq > r.seq && (m.ref === r.id || (!!r.goal && m.goal === r.goal))));
|
|
109
111
|
const seq = first
|
|
110
112
|
? head
|
|
111
113
|
: historyBody === null
|
package/dist/plans.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const UNLIMITED = 1_000_000;
|
|
2
|
+
export const DEFAULT_PLANS = {
|
|
3
|
+
free: { spaces: 1, members: 2, wake: true },
|
|
4
|
+
pro: { spaces: 10, members: 25, wake: true },
|
|
5
|
+
};
|
|
6
|
+
export const PLAN_NAME = /^[a-z][a-z0-9_-]{0,23}$/;
|
|
7
|
+
const int = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? v : null;
|
|
8
|
+
export function parseLimits(raw) {
|
|
9
|
+
if (!raw || typeof raw !== "object")
|
|
10
|
+
return null;
|
|
11
|
+
const o = raw;
|
|
12
|
+
const num = (v) => typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
13
|
+
const spaces = int(num(o["spaces"]), 0, UNLIMITED);
|
|
14
|
+
const members = int(num(o["members"]), 1, UNLIMITED);
|
|
15
|
+
const wake = typeof o["wake"] === "boolean" ? o["wake"] : o["wake"] === "true" || o["wake"] === "on" || o["wake"] === "1" ? true : o["wake"] === "false" || o["wake"] === "off" || o["wake"] === "0" || o["wake"] === undefined ? false : null;
|
|
16
|
+
if (spaces === null || members === null || wake === null)
|
|
17
|
+
return null;
|
|
18
|
+
return { spaces, members, wake };
|
|
19
|
+
}
|
|
20
|
+
export function parsePlans(raw) {
|
|
21
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
22
|
+
return null;
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [name, lim] of Object.entries(raw)) {
|
|
25
|
+
if (!PLAN_NAME.test(name))
|
|
26
|
+
return null;
|
|
27
|
+
const l = parseLimits(lim);
|
|
28
|
+
if (!l)
|
|
29
|
+
return null;
|
|
30
|
+
out[name] = l;
|
|
31
|
+
}
|
|
32
|
+
return out["free"] ? out : null;
|
|
33
|
+
}
|
|
34
|
+
export function planLimits(plans, plan, override) {
|
|
35
|
+
if (override) {
|
|
36
|
+
try {
|
|
37
|
+
const l = parseLimits(JSON.parse(override));
|
|
38
|
+
if (l)
|
|
39
|
+
return l;
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
return plans[plan] ?? plans["free"] ?? DEFAULT_PLANS["free"];
|
|
44
|
+
}
|
|
45
|
+
export const fmtLimit = (n) => n >= UNLIMITED ? "unlimited" : String(n);
|
|
46
|
+
//# sourceMappingURL=plans.js.map
|
package/dist/profile.js
CHANGED
|
@@ -25,6 +25,7 @@ export function defaultProfile(cfg) {
|
|
|
25
25
|
human: { display_name: cfg.display ?? cfg.member, title: "", responsibilities: [] },
|
|
26
26
|
agent: { primary_surface: surfaceFromAgent(cfg.agent), wake_enabled: false },
|
|
27
27
|
attention: { urgent: "primary_surface", decision: "primary_surface", fyi: "inbox" },
|
|
28
|
+
notifications: { decisions: true, deliveries: false, worker_done: false },
|
|
28
29
|
authority: {},
|
|
29
30
|
decision_keywords: {},
|
|
30
31
|
capabilities: [],
|
|
@@ -32,6 +33,9 @@ export function defaultProfile(cfg) {
|
|
|
32
33
|
updated_at: 0,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
36
|
+
export function wakeDefault(profile) {
|
|
37
|
+
return (profile.agent.wake_default ?? "anyone") === "anyone";
|
|
38
|
+
}
|
|
35
39
|
export function loadProfile(cfg, file = profilePath()) {
|
|
36
40
|
if (!existsSync(file))
|
|
37
41
|
return defaultProfile(cfg);
|
|
@@ -53,6 +57,7 @@ export function loadProfile(cfg, file = profilePath()) {
|
|
|
53
57
|
},
|
|
54
58
|
agent: { ...defaultProfile(cfg).agent, ...raw.agent, primary_surface: surface },
|
|
55
59
|
attention: { ...defaultProfile(cfg).attention, ...raw.attention },
|
|
60
|
+
notifications: { ...defaultProfile(cfg).notifications, ...(raw.notifications ?? {}) },
|
|
56
61
|
authority: raw.authority ?? {},
|
|
57
62
|
decision_keywords: cleanKeywords(legacy.decision_keywords),
|
|
58
63
|
capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((x) => typeof x === "string") : [],
|
package/dist/protocol.js
CHANGED
|
@@ -160,6 +160,7 @@ export const BindRepo = z.object({
|
|
|
160
160
|
cid: z.string().max(64).optional(),
|
|
161
161
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/),
|
|
162
162
|
origin: z.string().max(200).nullable().optional(),
|
|
163
|
+
base: z.string().min(1).max(100).optional(),
|
|
163
164
|
});
|
|
164
165
|
export const OpenGoal = z.object({
|
|
165
166
|
t: z.literal("open_goal"),
|
|
@@ -171,7 +172,18 @@ export const OpenGoal = z.object({
|
|
|
171
172
|
deadline: z.number().int().positive().optional(),
|
|
172
173
|
});
|
|
173
174
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
174
|
-
export const
|
|
175
|
+
export const DeleteSpace = z.object({
|
|
176
|
+
t: z.literal("delete_space"),
|
|
177
|
+
cid: z.string().max(64).optional(),
|
|
178
|
+
confirm: z.string().min(1).max(64),
|
|
179
|
+
});
|
|
180
|
+
export const WorkerState = z.object({
|
|
181
|
+
t: z.literal("worker_state"),
|
|
182
|
+
state: z.enum(["idle", "holding", "running"]),
|
|
183
|
+
detail: z.string().max(200).optional(),
|
|
184
|
+
until: z.number().int().positive().optional(),
|
|
185
|
+
});
|
|
186
|
+
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
175
187
|
export function shouldDeferToHuman(sessions, work, opts = {}) {
|
|
176
188
|
const now = opts.now ?? Date.now();
|
|
177
189
|
const idleMs = opts.idleMs ?? LIMITS.workerIdleMs;
|
package/dist/quickstart.js
CHANGED
|
@@ -4,6 +4,7 @@ import { basename } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { PantheonClient } from "./client.js";
|
|
6
6
|
import { load, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
|
|
7
|
+
import { loadProfile, wakeDefault } from "./profile.js";
|
|
7
8
|
import { newKeypair } from "./identity.js";
|
|
8
9
|
import { installForUser } from "./install.js";
|
|
9
10
|
import { writeIntent, installHooks } from "./notify.js";
|
|
@@ -203,7 +204,7 @@ export async function start(opts) {
|
|
|
203
204
|
warnings.push(`this repo was bound to another team (${replaced.space}); it now belongs to "${c.space}"`);
|
|
204
205
|
}
|
|
205
206
|
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
206
|
-
const inv = await client.makeInvite(
|
|
207
|
+
const inv = await client.makeInvite(wakeDefault(loadProfile(c)), opts.ttlMinutes);
|
|
207
208
|
invite = toInvite({ ...c, token: inv.url });
|
|
208
209
|
claimed = true;
|
|
209
210
|
}
|