@join-pantheon/cli 0.26.2 → 0.26.4
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 +1 -1
- package/dist/cli.js +1 -1
- package/dist/doctor.js +29 -9
- package/dist/install.js +33 -4
- package/dist/mcp-instructions.js +3 -1
- package/dist/mcp.js +14 -6
- package/dist/notify.js +15 -7
- package/dist/protocol.js +1 -0
- package/dist/relay.js +4 -2
- package/dist/update-check.js +3 -1
- package/dist/upgrade.js +8 -3
- package/dist/which.js +5 -0
- package/dist/worklist.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -499,7 +499,7 @@ All enforced in the relay, not in a prompt — a prompt can be argued out of its
|
|
|
499
499
|
|
|
500
500
|
| Guard | Default | Why |
|
|
501
501
|
|---|---|---|
|
|
502
|
-
| Loop guard | 6 acts in a row | one delegate cannot monologue; only a peer act resets the run |
|
|
502
|
+
| Loop guard | 6 acts in a row | one delegate cannot monologue; only a peer act resets the run. Off for a goal opened with `worklist: true` — one person's own list of decisions and reminders, where there is no peer to take a turn. The act budget still applies there. |
|
|
503
503
|
| Goal budget | 80 acts | a runaway goal stalls and raises a human obligation instead of burning tokens |
|
|
504
504
|
| Rate limit | 40 acts / 10s | per member |
|
|
505
505
|
| Body cap | 32000 chars | over 2000 needs a one-line summary — a peer reading six thousand words misses paragraph nine |
|
package/dist/cli.js
CHANGED
|
@@ -963,7 +963,7 @@ async function main() {
|
|
|
963
963
|
if (!goals.length)
|
|
964
964
|
console.log(` ${D("(none)")}`);
|
|
965
965
|
for (const g of goals) {
|
|
966
|
-
console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title} ${D(g.id)}`);
|
|
966
|
+
console.log(` ${g.state.padEnd(8)} ${String(g.acts).padStart(3)}/${g.budget} ${g.title}${g.kind === "worklist" ? D(" [worklist]") : ""} ${D(g.id)}`);
|
|
967
967
|
}
|
|
968
968
|
const owed = client.obligations;
|
|
969
969
|
const ageOf = (t) => {
|
package/dist/doctor.js
CHANGED
|
@@ -5,7 +5,8 @@ import { grantsSeen, measureMcp, toolsReport, HOST_STARTUP_ALLOWANCE_MS, STARTUP
|
|
|
5
5
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
6
6
|
import { loadProfile, profileInitialized } from "./profile.js";
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
8
|
+
import { homedir, platform } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
9
10
|
import { PantheonClient } from "./client.js";
|
|
10
11
|
import { load, resolveFor, loadAll, CONFIG_PATH } from "./config.js";
|
|
11
12
|
import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
|
|
@@ -19,14 +20,25 @@ import { VERSION } from "./version.js";
|
|
|
19
20
|
import { serviceInstalled, servicesUnavailable } from "./service.js";
|
|
20
21
|
import { hostEnv, platformName } from "./host-env.js";
|
|
21
22
|
const HOSTED_RELAY = "relay.joinpantheon.network";
|
|
22
|
-
const
|
|
23
|
+
const git = (args, cwd) => {
|
|
23
24
|
try {
|
|
24
|
-
return execFileSync("
|
|
25
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
function claudeLocalOverride(projectDir) {
|
|
32
|
+
try {
|
|
33
|
+
const doc = JSON.parse(readFileSync(join(homedir(), ".claude.json"), "utf8"));
|
|
34
|
+
const norm = (x) => resolve(x).replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
35
|
+
const want = norm(projectDir);
|
|
36
|
+
return Object.entries(doc.projects ?? {}).some(([k, v]) => norm(k) === want && !!v?.mcpServers?.["pantheon"]);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
function isPrivate(host) {
|
|
31
43
|
const h = host.replace(/:\d+$/, "");
|
|
32
44
|
if (/^(localhost|127\.0\.0\.1|\[::1\]|::1)$/.test(h))
|
|
@@ -56,10 +68,10 @@ export async function doctor(projectDir) {
|
|
|
56
68
|
: add("fail", "node", `v${process.versions.node} is too old`, "pantheon needs Node 22 or newer (the relay uses node:sqlite) — install it and re-run", `v${process.versions.node}, too old`);
|
|
57
69
|
const host = hostEnv();
|
|
58
70
|
if (host.ephemeral) {
|
|
59
|
-
add("warn", "machine", `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${platformName(host.platform)} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`, `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"}`);
|
|
71
|
+
add("warn", "machine", `${platformName(host.platform)} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${platformName(host.platform)} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`, `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"}`);
|
|
60
72
|
}
|
|
61
73
|
else {
|
|
62
|
-
add("pass", "machine", `${host.platform}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`, undefined, `${host.platform}${host.desktop ? ", desktop" : ""}`);
|
|
74
|
+
add("pass", "machine", `${platformName(host.platform)}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`, undefined, `${host.platform}${host.desktop ? ", desktop" : ""}`);
|
|
63
75
|
}
|
|
64
76
|
let cfg = null;
|
|
65
77
|
try {
|
|
@@ -114,7 +126,15 @@ export async function doctor(projectDir) {
|
|
|
114
126
|
add("fail", "mcp entry", ".mcp.json has no pantheon server", "run `pantheon install`", ".mcp.json has no pantheon server");
|
|
115
127
|
}
|
|
116
128
|
else if (entry.command === "pantheon") {
|
|
117
|
-
|
|
129
|
+
if (platform() !== "win32") {
|
|
130
|
+
add("pass", "mcp entry", "portable (`pantheon mcp`) — safe to commit, works on your teammate's machine", undefined, "portable");
|
|
131
|
+
}
|
|
132
|
+
else if (claudeLocalOverride(projectDir)) {
|
|
133
|
+
add("pass", "mcp entry", "portable (`pantheon mcp`) for your teammates; Claude Code on this Windows machine starts it through a local override for this folder", undefined, "portable, with a local override");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
add("warn", "mcp entry", "portable (`pantheon mcp`) — right for your teammates, but a Windows MCP host cannot start `pantheon` itself (it is pantheon.cmd, which needs a shell), so Claude Code shows pantheon as failed in this folder", "run `pantheon install` in this folder — it adds a local override for Claude Code here and leaves the committed file alone", "portable, no local override on Windows");
|
|
137
|
+
}
|
|
118
138
|
}
|
|
119
139
|
else {
|
|
120
140
|
add("fail", "mcp entry", `hard-codes this machine: ${entry.command}`, "make sure `pantheon` is on your PATH, then run `pantheon install` again — it writes the portable `pantheon mcp` entry. Committed as-is it silently fails on the other machine: the MCP server never starts and no error is shown.", "hard-codes a path on this machine");
|
|
@@ -123,16 +143,16 @@ export async function doctor(projectDir) {
|
|
|
123
143
|
onPath()
|
|
124
144
|
? add("pass", "PATH", "`pantheon` resolves by name", undefined, "pantheon resolves by name")
|
|
125
145
|
: add("warn", "PATH", "`pantheon` is not on PATH", "install it globally (`npm install -g @join-pantheon/cli`) and make sure your npm global bin dir (`npm bin -g`) is on PATH", "pantheon is not on PATH");
|
|
126
|
-
const inRepo =
|
|
146
|
+
const inRepo = git(["rev-parse", "--is-inside-work-tree"], projectDir) === "true";
|
|
127
147
|
if (!inRepo) {
|
|
128
148
|
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");
|
|
129
149
|
}
|
|
130
150
|
else {
|
|
131
|
-
const remote =
|
|
151
|
+
const remote = git(["remote", "get-url", "origin"], projectDir);
|
|
132
152
|
remote
|
|
133
153
|
? add("pass", "git", `repo with origin ${remote}`, undefined, "repo with an origin remote")
|
|
134
154
|
: add("fail", "git", "repo has no `origin` remote", "DELIVERED names a commit your teammate fetches. Without a shared remote they cannot verify anything you deliver.", "repo without an origin remote");
|
|
135
|
-
const branch =
|
|
155
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], projectDir);
|
|
136
156
|
if (branch)
|
|
137
157
|
add("pass", "git branch", branch);
|
|
138
158
|
}
|
package/dist/install.js
CHANGED
|
@@ -8,6 +8,7 @@ import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
|
|
|
8
8
|
import { load } from "./config.js";
|
|
9
9
|
import { loadProfile } from "./profile.js";
|
|
10
10
|
import { agentSpawn, onPathNamed, which } from "./which.js";
|
|
11
|
+
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
11
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
export function onPath() {
|
|
13
14
|
return onPathNamed("pantheon");
|
|
@@ -114,10 +115,11 @@ function upsertBrief(file, cfg) {
|
|
|
114
115
|
writeFileSync(file, cur.trimEnd() + "\n\n" + block + "\n");
|
|
115
116
|
return "updated";
|
|
116
117
|
}
|
|
117
|
-
export function install(projectDir, cfg, force) {
|
|
118
|
+
export function install(projectDir, cfg, force, os = platform(), claudeLocal = claudeLocalScope) {
|
|
118
119
|
const out = [];
|
|
119
|
-
const l = launcher(force);
|
|
120
|
-
const
|
|
120
|
+
const l = launcher(force, os);
|
|
121
|
+
const win = os === "win32";
|
|
122
|
+
const entry = win ? { command: "pantheon", args: ["mcp"] } : { command: l.command, args: l.args };
|
|
121
123
|
out.push(` .mcp.json ${mergeMcpJson(join(projectDir, ".mcp.json"), entry)} (claude code, conductor, copilot cli)`);
|
|
122
124
|
out.push(` .cursor/mcp.json ${mergeMcpJson(join(projectDir, ".cursor", "mcp.json"), entry)} (cursor)`);
|
|
123
125
|
const vscodeDir = join(projectDir, ".vscode");
|
|
@@ -137,7 +139,15 @@ export function install(projectDir, cfg, force) {
|
|
|
137
139
|
for (const line of codexTomlBlock(userEntry()).split("\n"))
|
|
138
140
|
out.push(` ${line}`);
|
|
139
141
|
out.push("");
|
|
140
|
-
if (
|
|
142
|
+
if (win) {
|
|
143
|
+
const local = claudeLocal(userEntry(os), projectDir);
|
|
144
|
+
out.push(" \x1b[36mPortable entry\x1b[0m — `pantheon mcp` is what your teammates' machines start, so this config is safe to commit.");
|
|
145
|
+
out.push(" Windows cannot start it from an MCP host (pantheon is pantheon.cmd here, and hosts use no shell), so this");
|
|
146
|
+
out.push(" machine gets its own way in, outside the repo:");
|
|
147
|
+
out.push(` claude code ${local.status}`);
|
|
148
|
+
out.push(` cursor your user-level entry from \`pantheon join\`; if Pantheon does not appear in Cursor for this folder, say so: ${FEEDBACK_ADDRESS}`);
|
|
149
|
+
}
|
|
150
|
+
else if (l.portable) {
|
|
141
151
|
out.push(" \x1b[36mPortable entry\x1b[0m — `pantheon` resolves on PATH, so this config is safe to commit");
|
|
142
152
|
out.push(" and will work in every Conductor workspace and on your teammate's machine.");
|
|
143
153
|
}
|
|
@@ -198,6 +208,25 @@ function claudeUserScope(entry) {
|
|
|
198
208
|
return { tool, where: "user scope", status: `failed: ${e.message.split("\n")[0]}` };
|
|
199
209
|
}
|
|
200
210
|
}
|
|
211
|
+
function claudeLocalScope(entry, projectDir) {
|
|
212
|
+
const tool = "claude code";
|
|
213
|
+
const claudeBin = which("claude");
|
|
214
|
+
if (!claudeBin)
|
|
215
|
+
return { tool, where: "-", status: "skipped (claude not on PATH) — once Claude Code is installed, run `pantheon install` here again" };
|
|
216
|
+
try {
|
|
217
|
+
const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "local"]);
|
|
218
|
+
const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "local", "--", entry.command, ...entry.args]);
|
|
219
|
+
try {
|
|
220
|
+
execFileSync(remove.command, remove.args, { stdio: "ignore", cwd: projectDir, ...remove.options });
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
execFileSync(add.command, add.args, { stdio: "ignore", cwd: projectDir, ...add.options });
|
|
224
|
+
return { tool, where: "local scope, this folder", status: "local override written — Claude Code starts pantheon here with this machine's node, and nothing in the repo changed" };
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
return { tool, where: "local scope", status: `failed: ${e.message.split("\n")[0]}` };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
201
230
|
function cursorUserScope(entry) {
|
|
202
231
|
const file = join(homedir(), ".cursor", "mcp.json");
|
|
203
232
|
try {
|
package/dist/mcp-instructions.js
CHANGED
|
@@ -8,7 +8,8 @@ export function protocol() {
|
|
|
8
8
|
4. back to 1
|
|
9
9
|
|
|
10
10
|
Keep looping while any goal you are in is OPEN. Stop only when every goal you are in is
|
|
11
|
-
CLOSED, or when you have ESCALATED and are genuinely waiting on your human.
|
|
11
|
+
CLOSED, or when you have ESCALATED and are genuinely waiting on your human. A goal marked
|
|
12
|
+
[worklist] is one person's own list: it does not keep you in the loop.
|
|
12
13
|
|
|
13
14
|
RULES
|
|
14
15
|
- Talk to peers, not to your human. If a peer could answer it, ASK the peer instead of asking your human.
|
|
@@ -33,6 +34,7 @@ RULES
|
|
|
33
34
|
- Verdict first, evidence after, and short: a peer reads your act in one screen. Say when you edited
|
|
34
35
|
your own human's working tree as a side effect, and leave those edits uncommitted for them.
|
|
35
36
|
- If pantheon_send returns PEER_TURN you have been talking to yourself — go back to pantheon_wait.
|
|
37
|
+
Filing reminders and decisions for your OWN human is not that: open the goal with worklist: true.
|
|
36
38
|
|
|
37
39
|
The relay only moves messages. It has no access to your files, your shell, or your credentials.
|
|
38
40
|
|
package/dist/mcp.js
CHANGED
|
@@ -5,6 +5,7 @@ import { PantheonClient } from "./client.js";
|
|
|
5
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
|
+
import { goalKindLabel, openedText } from "./worklist.js";
|
|
8
9
|
import { VERSION } from "./version.js";
|
|
9
10
|
import { installedVersion } from "./mcp-supervisor.js";
|
|
10
11
|
import { backendFromLabel } from "./agents.js";
|
|
@@ -246,7 +247,7 @@ export async function runMcp() {
|
|
|
246
247
|
? `peers: ${peers.map((p) => `${p.id} [${p.online ? "online" : "offline"}, ${p.agent}${reach(p)}]`).join(" ")}`
|
|
247
248
|
: `peers: none yet — you are the only delegate here. Your human's colleague needs to run 'pantheon join'.`,
|
|
248
249
|
open.length
|
|
249
|
-
? `open goals:\n${open.map((g) => ` ${g.id} ${g.state} ${g.acts}/${g.budget} acts "${g.title}" members: ${g.members.join(",")}${deadlineLabel(g.deadline)}`).join("\n")}`
|
|
250
|
+
? `open goals:\n${open.map((g) => ` ${g.id} ${g.state} ${g.acts}/${g.budget} acts "${g.title}" members: ${g.members.join(",")}${deadlineLabel(g.deadline)}${goalKindLabel(g)}`).join("\n")}`
|
|
250
251
|
: `open goals: none`,
|
|
251
252
|
mine.length
|
|
252
253
|
? `YOURS TO HANDLE (claim before working):\n${mine.map((o) => ` ${o.id} ${o.kind}${o.claimed_by ? " [yours]" : ""} (ref ${o.ref}) — ${o.body}`).join("\n")}`
|
|
@@ -415,8 +416,11 @@ export async function runMcp() {
|
|
|
415
416
|
deadline: z.string().optional().describe("when this goal should be closed by, ISO 8601 with timezone (e.g. 2026-08-28T22:00:00+04:00). " +
|
|
416
417
|
"Shown on every status line, and shown OVERDUE when passed — a deadline that lives only in prose is not tracked by anything."),
|
|
417
418
|
distinct_from_open_goals: z.boolean().optional().describe("confirm this goal is NOT the same work as any currently open goal. Required when an open goal already involves the same members."),
|
|
419
|
+
worklist: z.boolean().optional().describe("this goal is your human's OWN list — decisions with deadlines and reminders you file for them, which peers may read but " +
|
|
420
|
+
"nobody owes anything in. Turns off the six-acts-in-a-row guard for this goal (the act budget still applies) and it does not " +
|
|
421
|
+
"keep you in the loop. Use it when your human asks you to set up reminders or a pipeline for themselves; never for shared work."),
|
|
418
422
|
},
|
|
419
|
-
}, async ({ title, acceptance, invite, deadline, distinct_from_open_goals }) => guard(async () => {
|
|
423
|
+
}, async ({ title, acceptance, invite, deadline, distinct_from_open_goals, worklist }) => guard(async () => {
|
|
420
424
|
let deadlineMs;
|
|
421
425
|
if (deadline !== undefined) {
|
|
422
426
|
deadlineMs = Date.parse(deadline);
|
|
@@ -427,7 +431,7 @@ export async function runMcp() {
|
|
|
427
431
|
return fail(`deadline ${new Date(deadlineMs).toISOString()} is already in the past — check the timezone.`);
|
|
428
432
|
}
|
|
429
433
|
}
|
|
430
|
-
if (!distinct_from_open_goals) {
|
|
434
|
+
if (!distinct_from_open_goals && !worklist) {
|
|
431
435
|
const overlapping = overlappingOpenGoals(client.goals.values(), cfg.member, invite, client.roster.map((m) => m.id));
|
|
432
436
|
if (overlapping.length) {
|
|
433
437
|
return fail(`not opened — ${overlapping.length} open goal(s) already involve the same members:\n` +
|
|
@@ -436,9 +440,13 @@ export async function runMcp() {
|
|
|
436
440
|
`If yours is genuinely distinct work, call pantheon_open_goal again with distinct_from_open_goals: true.`);
|
|
437
441
|
}
|
|
438
442
|
}
|
|
439
|
-
const r = await client.openGoal({
|
|
440
|
-
|
|
441
|
-
|
|
443
|
+
const r = await client.openGoal({
|
|
444
|
+
title, acceptance,
|
|
445
|
+
...(invite ? { invite } : {}),
|
|
446
|
+
...(deadlineMs !== undefined ? { deadline: deadlineMs } : {}),
|
|
447
|
+
...(worklist ? { kind: "worklist" } : {}),
|
|
448
|
+
});
|
|
449
|
+
return text(openedText(r.id, title, !!worklist, client.goals.get(r.id), deadlineMs !== undefined ? `\n${deadlineLabel(deadlineMs).trim()}` : ""));
|
|
442
450
|
}));
|
|
443
451
|
server.registerTool("pantheon_claim", {
|
|
444
452
|
title: "Claim a piece of work",
|
package/dist/notify.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { whichPantheon } from "./service.js";
|
|
5
6
|
import { detectForHooks, agentById } from "./agents.js";
|
|
6
7
|
import { codexHookState, cursorHookState, copilotHookState, installCodexHooks, installCursorHooks, installCopilotHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, } from "./hooks-install.js";
|
|
7
8
|
export const HOOK_MARKER = "pantheon hook";
|
|
8
|
-
export const HOOK_RE = /pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s/i;
|
|
9
|
+
export const HOOK_RE = /(?:pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s|cli\.js"?\s+hook\s+(?:stop|prompt|end|codex-stop|copilot-stop|cursor-stop)\b)/i;
|
|
9
10
|
export const isOurHookCommand = (cmd) => !!cmd && (cmd.includes(HOOK_MARKER) || HOOK_RE.test(cmd));
|
|
10
11
|
export const hookBin = (bin) => (/\s/.test(bin) ? `"${bin}"` : bin);
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export function hookLauncher(os = platform(), found = whichPantheon(), entry = resolve(HERE, "cli.js"), entryExists = existsSync) {
|
|
14
|
+
if (os !== "win32")
|
|
15
|
+
return found ? hookBin(found) : null;
|
|
16
|
+
if (entryExists(entry))
|
|
17
|
+
return `node "${entry.replace(/\\/g, "/")}"`;
|
|
18
|
+
return found ? hookBin(found) : null;
|
|
19
|
+
}
|
|
11
20
|
export function settingsPath(scope, projectDir = process.cwd()) {
|
|
12
21
|
return scope === "user"
|
|
13
22
|
? join(homedir(), ".claude", "settings.json")
|
|
@@ -140,10 +149,9 @@ export function repairHook(home) {
|
|
|
140
149
|
function installClaudeHook(scope, projectDir = process.cwd()) {
|
|
141
150
|
const file = settingsPath(scope, projectDir);
|
|
142
151
|
const doc = read(file);
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
152
|
+
const bin = hookLauncher();
|
|
153
|
+
if (!bin)
|
|
145
154
|
throw new Error("pantheon binary not found on PATH");
|
|
146
|
-
const bin = hookBin(found);
|
|
147
155
|
const events = {
|
|
148
156
|
Stop: `${bin} hook stop`,
|
|
149
157
|
UserPromptSubmit: `${bin} hook prompt`,
|
|
@@ -168,7 +176,7 @@ function installClaudeHook(scope, projectDir = process.cwd()) {
|
|
|
168
176
|
];
|
|
169
177
|
}
|
|
170
178
|
export function installHooks(scope, projectDir = process.cwd(), only) {
|
|
171
|
-
const bin =
|
|
179
|
+
const bin = hookLauncher();
|
|
172
180
|
if (!bin)
|
|
173
181
|
throw new Error("pantheon binary not found on PATH");
|
|
174
182
|
const available = detectForHooks(projectDir);
|
package/dist/protocol.js
CHANGED
|
@@ -179,6 +179,7 @@ export const OpenGoal = z.object({
|
|
|
179
179
|
invite: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
180
180
|
budget: z.number().int().min(4).max(1000).optional(),
|
|
181
181
|
deadline: z.number().int().positive().optional(),
|
|
182
|
+
kind: z.enum(["worklist"]).optional(),
|
|
182
183
|
});
|
|
183
184
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
184
185
|
export const DeleteSpace = z.object({
|
package/dist/relay.js
CHANGED
|
@@ -1964,6 +1964,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
1964
1964
|
opened_by: me.id,
|
|
1965
1965
|
opened_at: Date.now(),
|
|
1966
1966
|
...(m.deadline !== undefined ? { deadline: m.deadline } : {}),
|
|
1967
|
+
...(m.kind !== undefined ? { kind: m.kind } : {}),
|
|
1967
1968
|
};
|
|
1968
1969
|
space.goals.set(goal.id, goal);
|
|
1969
1970
|
space.store.save(space.goals, space.obligations, space.profiles);
|
|
@@ -1971,7 +1972,8 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
1971
1972
|
const rec = this.commit(space, {
|
|
1972
1973
|
from: me.id, act: "NOTE", to: null, goal: goal.id, ref: null,
|
|
1973
1974
|
body: `goal opened: ${goal.title}\nacceptance: ${goal.acceptance || "(none stated)"}` +
|
|
1974
|
-
(goal.deadline ? `\ndeadline: ${new Date(goal.deadline).toISOString()}` : "")
|
|
1975
|
+
(goal.deadline ? `\ndeadline: ${new Date(goal.deadline).toISOString()}` : "") +
|
|
1976
|
+
(goal.kind === "worklist" ? `\nkind: worklist — ${me.id}'s own list; peers may read it, nobody owes anything in it` : ""),
|
|
1975
1977
|
});
|
|
1976
1978
|
this.send(ws, { t: "ack", ...(m.cid ? { cid: m.cid } : {}), id: goal.id, seq: rec.seq });
|
|
1977
1979
|
return;
|
|
@@ -2233,7 +2235,7 @@ pantheon signup --email you@work.com --username you</code></pre>
|
|
|
2233
2235
|
return { code: "BUDGET_SPENT", message: `goal "${goal.title}" spent its ${goal.budget}-act budget and needs a human` };
|
|
2234
2236
|
}
|
|
2235
2237
|
const run = sp.lastActor.get(goalId);
|
|
2236
|
-
if (run && run.member === me.id && run.run >= LIMITS.consecutive && act !== "ESCALATE" && act !== "CLOSE") {
|
|
2238
|
+
if (goal.kind !== "worklist" && run && run.member === me.id && run.run >= LIMITS.consecutive && act !== "ESCALATE" && act !== "CLOSE") {
|
|
2237
2239
|
return {
|
|
2238
2240
|
code: "PEER_TURN",
|
|
2239
2241
|
message: `you have sent ${run.run} acts in a row in "${goal.title}". Wait for a peer (pantheon_wait), or ESCALATE if nobody can move it.`,
|
package/dist/update-check.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { CONFIG_DIR } from "./config.js";
|
|
5
5
|
import { repoRoot, PKG_NAME } from "./upgrade.js";
|
|
6
6
|
import { VERSION } from "./version.js";
|
|
7
|
+
import { npmSpawn } from "./which.js";
|
|
7
8
|
export function semverGt(x, y) {
|
|
8
9
|
const p = (s) => s.split(".").map((n) => Number(n) || 0);
|
|
9
10
|
const [a, b] = [p(x), p(y)];
|
|
@@ -80,7 +81,8 @@ export function defaultDeps(home) {
|
|
|
80
81
|
},
|
|
81
82
|
npmLatest: () => {
|
|
82
83
|
try {
|
|
83
|
-
|
|
84
|
+
const s = npmSpawn(["view", PKG_NAME, "version"]);
|
|
85
|
+
return execFileSync(s.command, s.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000, ...s.options }).trim() || null;
|
|
84
86
|
}
|
|
85
87
|
catch {
|
|
86
88
|
return null;
|
package/dist/upgrade.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { installForUser } from "./install.js";
|
|
6
|
+
import { npmSpawn } from "./which.js";
|
|
6
7
|
import { relayProc, workerProc, notifierProc, menubarProc } from "./machine.js";
|
|
7
8
|
export function repoRoot() {
|
|
8
9
|
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -28,6 +29,10 @@ export function rewireVia(root, fallback, run = (file, args) => execFileSync(pro
|
|
|
28
29
|
}
|
|
29
30
|
export function defaultDeps() {
|
|
30
31
|
const git = (root, ...args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
32
|
+
const npm = (args, opts) => {
|
|
33
|
+
const s = npmSpawn(args);
|
|
34
|
+
return execFileSync(s.command, s.args, { encoding: "utf8", ...opts, ...s.options });
|
|
35
|
+
};
|
|
31
36
|
return {
|
|
32
37
|
root: repoRoot(),
|
|
33
38
|
isGit: (root) => existsSync(join(root, ".git")),
|
|
@@ -37,7 +42,7 @@ export function defaultDeps() {
|
|
|
37
42
|
fetch: (root) => { git(root, "fetch", "--quiet"); },
|
|
38
43
|
behind: (root) => Number(git(root, "rev-list", "--count", "HEAD..@{u}")) || 0,
|
|
39
44
|
pull: (root) => git(root, "pull", "--ff-only"),
|
|
40
|
-
build: (root) =>
|
|
45
|
+
build: (root) => npm(["install", "--no-audit", "--no-fund"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] }),
|
|
41
46
|
version: pkgVersion,
|
|
42
47
|
rewire: () => rewireVia(repoRoot(), () => installForUser().map((t) => ({ tool: t.tool, status: t.status }))),
|
|
43
48
|
running: () => {
|
|
@@ -64,10 +69,10 @@ export function defaultDeps() {
|
|
|
64
69
|
catch { }
|
|
65
70
|
return up;
|
|
66
71
|
},
|
|
67
|
-
npmUpgrade: () =>
|
|
72
|
+
npmUpgrade: () => npm(["install", "-g", `${PKG_NAME}@latest`, "--no-audit", "--no-fund"], { stdio: ["ignore", "pipe", "pipe"] }),
|
|
68
73
|
npmLatest: () => {
|
|
69
74
|
try {
|
|
70
|
-
return
|
|
75
|
+
return npm(["view", `${PKG_NAME}`, "version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() || null;
|
|
71
76
|
}
|
|
72
77
|
catch {
|
|
73
78
|
return null;
|
package/dist/which.js
CHANGED
|
@@ -76,6 +76,11 @@ export function spawnShape(binary, os = platform()) {
|
|
|
76
76
|
return { command: binary, shell: false };
|
|
77
77
|
return { command: binary, shell: /\.(cmd|bat)$/i.test(binary) };
|
|
78
78
|
}
|
|
79
|
+
export function npmSpawn(args, os = platform(), resolveNpm = () => which("npm", os)) {
|
|
80
|
+
if (!isWindows(os))
|
|
81
|
+
return { command: "npm", args: [...args], options: {} };
|
|
82
|
+
return agentSpawn(resolveNpm() ?? "npm.cmd", args, os);
|
|
83
|
+
}
|
|
79
84
|
const cmdQuote = (a) => (/[\s"&|<>^()]/.test(a) || a === "" ? `"${a.replace(/"/g, '\\"')}"` : a);
|
|
80
85
|
export function agentSpawn(binary, args, os = platform()) {
|
|
81
86
|
if (!isWindows(os) || !/\.(cmd|bat)$/i.test(binary))
|
package/dist/worklist.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function goalKindLabel(g) {
|
|
2
|
+
return g.kind === "worklist" ? " [worklist — one person's own list; nobody owes anything here, and it does not keep you in the loop]" : "";
|
|
3
|
+
}
|
|
4
|
+
export function openedText(id, title, askedWorklist, seen, deadlineLine = "") {
|
|
5
|
+
const head = `goal ${id} opened: "${title}"${deadlineLine}`;
|
|
6
|
+
if (!askedWorklist)
|
|
7
|
+
return `${head}\n\nNext: PROPOSE how to split it, then wait for peers to ACCEPT or OBJECT.`;
|
|
8
|
+
if (seen?.kind !== "worklist") {
|
|
9
|
+
return `${head}\n\nOpened as an ORDINARY goal: this relay is older than the worklist feature and ignored the flag. ` +
|
|
10
|
+
`The six-acts-in-a-row guard still applies here, so file items in batches of six and expect PEER_TURN between them ` +
|
|
11
|
+
`until the relay is upgraded.`;
|
|
12
|
+
}
|
|
13
|
+
return `${head} [worklist]\n\nThis is your human's own list. File each item as one act: ESCALATE for a decision they must make ` +
|
|
14
|
+
`(with its deadline in the body), NOTE for a reminder. Nobody owes anything here, PEER_TURN never fires, ` +
|
|
15
|
+
`and this goal does not keep you in the loop — the act budget still applies.`;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=worklist.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.4",
|
|
4
4
|
"description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|