@join-pantheon/cli 0.25.3 → 0.26.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 +43 -5
- package/bin/pantheon.js +7 -2
- package/dist/agents.js +159 -18
- package/dist/cli.js +39 -27
- package/dist/config.js +26 -1
- package/dist/doctor.js +23 -3
- package/dist/hooks-install.js +60 -2
- package/dist/install.js +94 -30
- package/dist/machine.js +11 -0
- package/dist/measure.js +17 -3
- package/dist/notice.js +4 -1
- package/dist/notifier.js +1 -0
- package/dist/notify.js +21 -5
- package/dist/persona.js +1 -1
- package/dist/profile.js +4 -1
- package/dist/quickstart.js +2 -1
- package/dist/run-failure.js +15 -0
- package/dist/service.js +3 -0
- package/dist/setup-guide.js +1 -1
- package/dist/tray.js +1 -1
- package/dist/update-check.js +2 -0
- package/dist/which.js +73 -0
- package/dist/worker.js +17 -7
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -404,22 +404,60 @@ with no human; the cap and lock governors under two workers per member.
|
|
|
404
404
|
|
|
405
405
|
### Which agent answers
|
|
406
406
|
|
|
407
|
-
The worker runs whichever agent CLI this machine has — `--agent claude-code|codex|cursor|auto
|
|
408
|
-
(default
|
|
409
|
-
|
|
407
|
+
The worker runs whichever agent CLI this machine has — `--agent claude-code|codex|cursor|copilot|auto`.
|
|
408
|
+
With `auto` (the default) the order is: the agent a persona pins, then the one you chose at setup,
|
|
409
|
+
then PATH order — and a candidate that is not signed in is skipped and named in the run log, so a
|
|
410
|
+
run never fails on an expired session when another agent could have answered. If nothing is
|
|
411
|
+
signed in, the first on PATH runs and the alert says exactly that, with the sign-in command.
|
|
412
|
+
`pantheon doctor` shows the same choice (`worker agent`). Each backend is driven the way it
|
|
413
|
+
actually works, with the same "no shell unless you said so" rule spelled in its own dialect:
|
|
410
414
|
|
|
411
415
|
| backend | bash gated (default) | with `--allow-bash` |
|
|
412
416
|
|---|---|---|
|
|
413
417
|
| claude-code | `acceptEdits` + the six pantheon tools | `bypassPermissions` |
|
|
414
418
|
| codex | `--sandbox read-only` (codex has no edits-without-shell mode) | `--sandbox workspace-write` |
|
|
415
419
|
| cursor | proposes edits, never applies (`-p` without `--force`) | `--force` |
|
|
420
|
+
| copilot | `--allow-tool=pantheon-worker --allow-tool=write` (edits without shell, Copilot's `write` kind) | `--allow-all-tools` |
|
|
416
421
|
|
|
417
|
-
|
|
418
|
-
peer's ASK end to end
|
|
422
|
+
Claude Code, Cursor and Codex are verified with **real live woken runs** — an idle machine answering a
|
|
423
|
+
peer's ASK end to end. Codex quirks the tooling handles for
|
|
419
424
|
you: no per-run approval flag exists in `codex exec`, MCP arrives via `-c` overrides (a worktree
|
|
420
425
|
`.codex/` is ignored), and codex silently skips a newly installed hook until you approve it with
|
|
421
426
|
`/hooks`.
|
|
422
427
|
|
|
428
|
+
Copilot runs in a `COPILOT_HOME` of its own for the run, outside the checkout. Copilot loads every
|
|
429
|
+
MCP server and hook it finds in its home, so yours would ride along — your pantheon entry as a second
|
|
430
|
+
server in the interactive role, your hooks, your folder approvals. The per-run home carries only the
|
|
431
|
+
run's own server (named `pantheon-worker`; the repo's committed `pantheon` is disabled by name), trusts
|
|
432
|
+
the worktree so `AGENTS.md` is read, and copies your `config.json` (trusted folders merged) and
|
|
433
|
+
`settings.json` across so your sign-in and model choice still apply. Verified against copilot 1.0.86:
|
|
434
|
+
a fresh home signs in through the system credential store, or through `gh` if that is where you are
|
|
435
|
+
signed in.
|
|
436
|
+
|
|
437
|
+
### Copilot
|
|
438
|
+
|
|
439
|
+
GitHub Copilot is one backend covering two surfaces. **Copilot CLI** reads the repo's `.mcp.json`
|
|
440
|
+
(the same file Claude Code reads) and `~/.copilot/mcp-config.json`; **Copilot in VS Code** reads
|
|
441
|
+
`.vscode/mcp.json` and its own user `mcp.json`, in a different dialect (`servers`, `stdio`), so
|
|
442
|
+
`pantheon install` writes both. The repo-level VS Code file is the one thing here written on
|
|
443
|
+
evidence rather than by default: `.vscode/` is a directory teams curate and commit, so it appears
|
|
444
|
+
only when the repo already has one or the machine running install has VS Code. A VS Code user on a
|
|
445
|
+
repo with neither is covered by `pantheon install --user`, which writes their own profile's
|
|
446
|
+
`mcp.json`. Hooks go in one file both read: `.github/hooks/pantheon.json` in the
|
|
447
|
+
repo, or `~/.copilot/hooks/pantheon.json` for every project — `agentStop` holds a turn open the way
|
|
448
|
+
Codex's Stop does, `userPromptSubmitted` and `sessionEnd` report busy and idle.
|
|
449
|
+
|
|
450
|
+
Two things Copilot does differently. It has no allow-list file for MCP tools: it asks once per tool
|
|
451
|
+
per folder and remembers your answer, so the first session says yes nine times. And it has no status
|
|
452
|
+
command: `pantheon doctor` reports Copilot signed in when a token is in the environment or `gh` is
|
|
453
|
+
signed in, and says "could not tell" otherwise rather than guessing — a run that fails on sign-in is
|
|
454
|
+
named as that, with `copilot login` as the fix.
|
|
455
|
+
|
|
456
|
+
**On a Copilot Business or Enterprise plan**, an organization owner has to switch on the
|
|
457
|
+
"MCP servers in Copilot" policy before any MCP server, pantheon included, loads. Organizations can
|
|
458
|
+
also restrict MCP servers to an approved registry; the server is called `pantheon` and its only job
|
|
459
|
+
is moving messages between your teammates' agents — nothing here reads code or runs commands.
|
|
460
|
+
|
|
423
461
|
### Personas — telling the worker who to be
|
|
424
462
|
|
|
425
463
|
A worker with no instructions escalates too much and trusts too much. A **persona** is one
|
package/bin/pantheon.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { dirname, resolve } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
13
|
|
|
14
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const root = resolve(here, "..");
|
|
@@ -25,4 +25,9 @@ if (!existsSync(entry)) {
|
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// A path is not a URL, and on Windows that is fatal rather than pedantic: an
|
|
29
|
+
// absolute POSIX path happens to work as an import specifier, while `D:\...`
|
|
30
|
+
// is read as the scheme "d:" and Node refuses it — so `pantheon --version`,
|
|
31
|
+
// the first command anyone runs, died with ERR_UNSUPPORTED_ESM_URL_SCHEME on
|
|
32
|
+
// every Windows machine. A real Windows runner is what said so.
|
|
33
|
+
await import(pathToFileURL(entry).href);
|
package/dist/agents.js
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
|
+
import { join, dirname, isAbsolute } from "node:path";
|
|
5
5
|
import { launcher } from "./install.js";
|
|
6
|
+
import { agentSpawn, which } from "./which.js";
|
|
6
7
|
import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
7
8
|
export function launchArgs(agent, prompt, name) {
|
|
8
|
-
|
|
9
|
+
if (agent === "claude-code")
|
|
10
|
+
return ["--name", name, prompt];
|
|
11
|
+
if (agent === "copilot")
|
|
12
|
+
return ["-n", name, "-i", prompt];
|
|
13
|
+
return [prompt];
|
|
9
14
|
}
|
|
10
15
|
export const AGENTS = [
|
|
11
16
|
{ id: "claude-code", label: "Claude Code", binaries: ["claude"] },
|
|
12
17
|
{ id: "codex", label: "Codex", binaries: ["codex"] },
|
|
13
18
|
{ id: "cursor", label: "Cursor", binaries: ["cursor-agent", "agent"] },
|
|
19
|
+
{ id: "copilot", label: "Copilot", binaries: ["copilot"] },
|
|
14
20
|
];
|
|
21
|
+
export function copilotHome(home = homedir(), env = process.env) {
|
|
22
|
+
return env.COPILOT_HOME || join(home, ".copilot");
|
|
23
|
+
}
|
|
24
|
+
export function hasCopilotExtension(home = homedir()) {
|
|
25
|
+
try {
|
|
26
|
+
return readdirSync(join(home, ".vscode", "extensions")).some((d) => d.startsWith("github.copilot"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
const sh = (cmd) => {
|
|
16
33
|
try {
|
|
17
34
|
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -22,7 +39,7 @@ const sh = (cmd) => {
|
|
|
22
39
|
};
|
|
23
40
|
export function cliOnPath(name) {
|
|
24
41
|
if (!binaryCache.has(`path:${name}`))
|
|
25
|
-
binaryCache.set(`path:${name}`,
|
|
42
|
+
binaryCache.set(`path:${name}`, which(name));
|
|
26
43
|
return binaryCache.get(`path:${name}`) ?? null;
|
|
27
44
|
}
|
|
28
45
|
export function agentById(id) {
|
|
@@ -35,6 +52,11 @@ export function detectForHooks(projectDir = process.cwd()) {
|
|
|
35
52
|
out.push(backend);
|
|
36
53
|
continue;
|
|
37
54
|
}
|
|
55
|
+
if (backend.id === "copilot") {
|
|
56
|
+
if (existsSync(copilotHome()) || hasCopilotExtension())
|
|
57
|
+
out.push(backend);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
38
60
|
const userDir = backend.id === "claude-code" ? join(homedir(), ".claude")
|
|
39
61
|
: backend.id === "codex" ? join(homedir(), ".codex")
|
|
40
62
|
: join(homedir(), ".cursor");
|
|
@@ -66,7 +88,8 @@ const binaryCache = new Map();
|
|
|
66
88
|
export function resolveBinary(backend) {
|
|
67
89
|
for (const b of backend.binaries) {
|
|
68
90
|
if (!binaryCache.has(b)) {
|
|
69
|
-
|
|
91
|
+
const hit = which(b);
|
|
92
|
+
let found = hit ? (platform() === "win32" ? hit : b) : null;
|
|
70
93
|
if (found && b === "agent") {
|
|
71
94
|
const v = `${sh("agent --version 2>/dev/null | head -1") ?? ""} ${sh("agent --help 2>/dev/null | head -3") ?? ""}`;
|
|
72
95
|
if (!/cursor/i.test(v))
|
|
@@ -79,21 +102,96 @@ export function resolveBinary(backend) {
|
|
|
79
102
|
}
|
|
80
103
|
return null;
|
|
81
104
|
}
|
|
82
|
-
export
|
|
105
|
+
export const SIGN_IN_CHECK = {
|
|
106
|
+
"claude-code": { args: ["auth", "status"], fix: "run `claude`, then /login" },
|
|
107
|
+
codex: { args: ["login", "status"], fix: "run `codex login`" },
|
|
108
|
+
cursor: { args: ["status"], fix: "run `cursor-agent login`" },
|
|
109
|
+
copilot: { args: [], fix: "run `copilot login` (or `gh auth login` — copilot uses gh's sign-in too)" },
|
|
110
|
+
};
|
|
111
|
+
export const COPILOT_TOKEN_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
112
|
+
export function copilotSignedIn(run = runStatus, env = process.env, gh = () => !!cliOnPath("gh")) {
|
|
113
|
+
if (COPILOT_TOKEN_VARS.some((k) => env[k]))
|
|
114
|
+
return { ok: true, detail: "signed in (token in the environment)" };
|
|
115
|
+
if (!gh())
|
|
116
|
+
return { ok: false, unknown: true, detail: "could not tell (copilot has no status command, and gh is not on PATH)" };
|
|
117
|
+
const r = run("gh", ["auth", "status"]);
|
|
118
|
+
if (r.status === 0)
|
|
119
|
+
return { ok: true, detail: "signed in (via gh)" };
|
|
120
|
+
return { ok: false, unknown: true, detail: "could not tell (gh is not signed in; a `copilot login` is not visible from outside)" };
|
|
121
|
+
}
|
|
122
|
+
const runStatus = (binary, args) => {
|
|
123
|
+
try {
|
|
124
|
+
const sp = agentSpawn(binary, args);
|
|
125
|
+
const out = execFileSync(sp.command, sp.args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 8_000, env: { ...process.env, NO_COLOR: "1" }, ...sp.options });
|
|
126
|
+
return { status: 0, out };
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
const err = e;
|
|
130
|
+
if (err.killed)
|
|
131
|
+
return { status: null, out: "timed out" };
|
|
132
|
+
return { status: err.status ?? null, out: `${err.stdout ?? ""}${err.stderr ?? ""}`.trim() || (err.message ?? "") };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export const AGENT_HOME_DIR = { "claude-code": ".claude", codex: ".codex", cursor: ".cursor", copilot: ".copilot" };
|
|
136
|
+
export function signedIn(backend, binary, run = runStatus, home = homedir()) {
|
|
137
|
+
if (backend.id === "copilot")
|
|
138
|
+
return copilotSignedIn(run);
|
|
139
|
+
if (!existsSync(join(home, AGENT_HOME_DIR[backend.id])))
|
|
140
|
+
return { ok: false, detail: "never signed in on this machine" };
|
|
141
|
+
const check = SIGN_IN_CHECK[backend.id];
|
|
142
|
+
const r = run(binary, check.args);
|
|
143
|
+
if (r.status === null)
|
|
144
|
+
return { ok: false, unknown: true, detail: `could not tell (${(r.out.split("\n")[0] ?? "").slice(0, 60) || "no answer"})` };
|
|
145
|
+
if (backend.id === "claude-code") {
|
|
146
|
+
try {
|
|
147
|
+
const j = JSON.parse(r.out);
|
|
148
|
+
if (typeof j.loggedIn === "boolean")
|
|
149
|
+
return j.loggedIn ? { ok: true, detail: "signed in" } : { ok: false, detail: "not signed in" };
|
|
150
|
+
}
|
|
151
|
+
catch { }
|
|
152
|
+
}
|
|
153
|
+
return r.status === 0 ? { ok: true, detail: "signed in" } : { ok: false, detail: "not signed in" };
|
|
154
|
+
}
|
|
155
|
+
export function chooseAgent(preferred, opts = {}) {
|
|
156
|
+
const available = opts.available ?? detectAvailable();
|
|
157
|
+
const check = opts.check === null ? null : (opts.check ?? ((b, bin) => signedIn(b, bin, undefined, opts.home)));
|
|
158
|
+
const state = (b, bin) => (check ? check(b, bin) : undefined);
|
|
83
159
|
if (preferred && preferred !== "auto") {
|
|
84
160
|
const backend = agentById(preferred);
|
|
85
161
|
if (!backend)
|
|
86
|
-
throw new Error(`unknown agent ${preferred} — use claude-code, codex, cursor, or auto`);
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
162
|
+
throw new Error(`unknown agent ${preferred} — use claude-code, codex, cursor, copilot, or auto`);
|
|
163
|
+
const hit = available.find((a) => a.backend.id === backend.id);
|
|
164
|
+
if (!hit)
|
|
89
165
|
throw new Error(`${backend.label} is not on PATH (looked for ${backend.binaries.join(", ")})`);
|
|
90
|
-
return { backend, binary };
|
|
166
|
+
return { backend, binary: hit.binary, signIn: state(backend, hit.binary), because: "pinned", skipped: [] };
|
|
91
167
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
168
|
+
if (!available.length)
|
|
169
|
+
throw new Error("no agent CLI on PATH — install claude, codex, cursor-agent, or copilot");
|
|
170
|
+
const order = [];
|
|
171
|
+
const wanted = opts.prefer ? agentById(opts.prefer) : undefined;
|
|
172
|
+
const chosen = wanted ? available.find((a) => a.backend.id === wanted.id) : undefined;
|
|
173
|
+
if (chosen)
|
|
174
|
+
order.push({ ...chosen, because: "chosen at setup" });
|
|
175
|
+
for (const a of available)
|
|
176
|
+
if (!order.some((o) => o.backend.id === a.backend.id))
|
|
177
|
+
order.push({ ...a, because: available.length === 1 ? "only one on PATH" : "first signed in on PATH" });
|
|
178
|
+
const skipped = [];
|
|
179
|
+
for (const c of order) {
|
|
180
|
+
const s = state(c.backend, c.binary);
|
|
181
|
+
if (!s || s.ok || s.unknown)
|
|
182
|
+
return { backend: c.backend, binary: c.binary, ...(s ? { signIn: s } : {}), because: c.because, skipped };
|
|
183
|
+
skipped.push({ backend: c.backend, why: `${s.detail} — ${SIGN_IN_CHECK[c.backend.id].fix}` });
|
|
184
|
+
}
|
|
185
|
+
const first = order[0];
|
|
186
|
+
return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
|
|
187
|
+
}
|
|
188
|
+
export function describeChoice(c) {
|
|
189
|
+
const head = `${c.backend.label}${c.signIn ? ` — ${c.signIn.detail}` : ""} (${c.because})`;
|
|
190
|
+
return c.skipped.length ? `${head}; skipped ${c.skipped.map((s) => `${s.backend.label} — ${s.why}`).join("; ")}` : head;
|
|
191
|
+
}
|
|
192
|
+
export function resolveAgent(preferred) {
|
|
193
|
+
const c = chooseAgent(preferred, { check: null });
|
|
194
|
+
return { backend: c.backend, binary: c.binary };
|
|
97
195
|
}
|
|
98
196
|
export { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
99
197
|
export function headlessSpawn(opts) {
|
|
@@ -159,6 +257,32 @@ export function headlessSpawn(opts) {
|
|
|
159
257
|
env: procEnv,
|
|
160
258
|
};
|
|
161
259
|
}
|
|
260
|
+
if (opts.backend.id === "copilot") {
|
|
261
|
+
const home = opts.runHome ? join(opts.runHome, "copilot") : join(opts.worktree, ".pantheon-copilot-home");
|
|
262
|
+
const config = opts.runHome ? readCopilotConfig() : {};
|
|
263
|
+
const trusted = Array.isArray(config.trustedFolders) ? config.trustedFolders.filter((x) => typeof x === "string") : [];
|
|
264
|
+
if (!trusted.includes(opts.worktree))
|
|
265
|
+
trusted.push(opts.worktree);
|
|
266
|
+
const settings = join(copilotHome(), "settings.json");
|
|
267
|
+
return {
|
|
268
|
+
command: opts.binary,
|
|
269
|
+
args: [
|
|
270
|
+
"-p", opts.prompt, "-s",
|
|
271
|
+
"--no-auto-update", "--no-ask-user", "--disable-builtin-mcps",
|
|
272
|
+
"--disable-mcp-server", "pantheon",
|
|
273
|
+
...(opts.allowBash ? ["--allow-all-tools"] : [`--allow-tool=${COPILOT_WORKER_SERVER}`, "--allow-tool=write"]),
|
|
274
|
+
],
|
|
275
|
+
env: { ...procEnv, COPILOT_HOME: home },
|
|
276
|
+
writeFiles: [
|
|
277
|
+
{
|
|
278
|
+
path: join(home, "mcp-config.json"),
|
|
279
|
+
contents: JSON.stringify({ mcpServers: { [COPILOT_WORKER_SERVER]: { type: "local", command: l.command, args: l.args, env: fileEnv, tools: ["*"] } } }, null, 2),
|
|
280
|
+
},
|
|
281
|
+
{ path: join(home, "config.json"), contents: JSON.stringify({ ...config, trustedFolders: trusted }, null, 2) },
|
|
282
|
+
],
|
|
283
|
+
...(opts.runHome && existsSync(settings) ? { copyFiles: [{ from: settings, to: join(home, "settings.json") }] } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
162
286
|
return {
|
|
163
287
|
command: opts.binary,
|
|
164
288
|
args: [
|
|
@@ -185,15 +309,32 @@ export function headlessSpawn(opts) {
|
|
|
185
309
|
],
|
|
186
310
|
};
|
|
187
311
|
}
|
|
312
|
+
export const COPILOT_WORKER_SERVER = "pantheon-worker";
|
|
313
|
+
function readCopilotConfig() {
|
|
314
|
+
try {
|
|
315
|
+
const doc = JSON.parse(readFileSync(join(copilotHome(), "config.json"), "utf8"));
|
|
316
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : {};
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return {};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
188
322
|
export function tomlInlineTable(kv) {
|
|
189
323
|
return `{${Object.entries(kv).map(([k, v]) => `${k} = ${JSON.stringify(v)}`).join(", ")}}`;
|
|
190
324
|
}
|
|
191
325
|
export function materializeSpawnFiles(worktree, spawn) {
|
|
192
326
|
for (const f of spawn.writeFiles ?? []) {
|
|
193
|
-
const p = join(worktree, f.path);
|
|
194
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
327
|
+
const p = isAbsolute(f.path) ? f.path : join(worktree, f.path);
|
|
328
|
+
mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
|
|
195
329
|
writeFileSync(p, f.contents, { mode: 0o600 });
|
|
196
330
|
}
|
|
331
|
+
for (const c of spawn.copyFiles ?? []) {
|
|
332
|
+
try {
|
|
333
|
+
mkdirSync(dirname(c.to), { recursive: true, mode: 0o700 });
|
|
334
|
+
copyFileSync(c.from, c.to);
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
}
|
|
197
338
|
}
|
|
198
339
|
export function backendFromLabel(label) {
|
|
199
340
|
if (!label)
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
|
+
import { agentSpawn } from "./which.js";
|
|
13
14
|
import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
15
|
import { fmtLimit } from "./plans.js";
|
|
15
16
|
import { validEmail, validUsername } from "./accounts.js";
|
|
@@ -427,6 +428,8 @@ function printReady(tools) {
|
|
|
427
428
|
oneTime.set("cursor", `cursor enable the "pantheon" MCP server if it asks`);
|
|
428
429
|
if (/codex/i.test(t.tool))
|
|
429
430
|
oneTime.set("codex", `codex run /hooks inside codex once to approve the wake-up hook`);
|
|
431
|
+
if (/copilot/i.test(t.tool))
|
|
432
|
+
oneTime.set("copilot", `copilot say yes once when it asks to trust this folder; on a Copilot Business or Enterprise plan an org owner must have the "MCP servers in Copilot" policy on`);
|
|
430
433
|
}
|
|
431
434
|
if (oneTime.size) {
|
|
432
435
|
console.error(`\n${B("Your agents may each ask once")} ${D("(their own security, not ours — one yes each):")}`);
|
|
@@ -484,7 +487,7 @@ ${B("control")}
|
|
|
484
487
|
pantheon admit <who> let someone in (--may-wake to allow unattended work here)
|
|
485
488
|
pantheon admit <who> --revoke remove them
|
|
486
489
|
pantheon worker stay answerable while you are away
|
|
487
|
-
(--agent claude-code|codex|cursor|auto, --persona <name>)
|
|
490
|
+
(--agent claude-code|codex|cursor|copilot|auto, --persona <name>)
|
|
488
491
|
pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
|
|
489
492
|
pantheon persona how a woken worker should behave: list / init <name> / show <name>
|
|
490
493
|
pantheon notifier alert you when a task requires your decision
|
|
@@ -497,7 +500,7 @@ ${B("control")}
|
|
|
497
500
|
pantheon approvals decisions waiting on you; show or answer one
|
|
498
501
|
pantheon stop [relay|worker|notifier|menubar|all] stop it
|
|
499
502
|
pantheon hook install your agent keeps working when a peer is waiting on it
|
|
500
|
-
(auto-detects claude / codex / cursor on this machine)
|
|
503
|
+
(auto-detects claude / codex / cursor / copilot on this machine)
|
|
501
504
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
502
505
|
pantheon leave remove what pantheon put on this machine
|
|
503
506
|
pantheon panic cut off all unattended work on this machine, now
|
|
@@ -1314,6 +1317,15 @@ async function main() {
|
|
|
1314
1317
|
case "hook": {
|
|
1315
1318
|
const sub = argv[1] ?? "status";
|
|
1316
1319
|
const hookCfg = resolveForHook();
|
|
1320
|
+
const sessionOf = (raw) => {
|
|
1321
|
+
try {
|
|
1322
|
+
const j = JSON.parse(raw);
|
|
1323
|
+
return j.session_id ?? j.sessionId ?? j.conversation_id ?? "unknown";
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
return "unknown";
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1317
1329
|
const reportIdle = async (session, seen) => {
|
|
1318
1330
|
if (!hookCfg)
|
|
1319
1331
|
return true;
|
|
@@ -1334,11 +1346,7 @@ async function main() {
|
|
|
1334
1346
|
raw += chunk;
|
|
1335
1347
|
}
|
|
1336
1348
|
catch { }
|
|
1337
|
-
|
|
1338
|
-
try {
|
|
1339
|
-
session = JSON.parse(raw).session_id ?? "unknown";
|
|
1340
|
-
}
|
|
1341
|
-
catch { }
|
|
1349
|
+
const session = sessionOf(raw);
|
|
1342
1350
|
try {
|
|
1343
1351
|
if (hookCfg) {
|
|
1344
1352
|
const { reportActivity } = await import("./pending.js");
|
|
@@ -1366,11 +1374,7 @@ async function main() {
|
|
|
1366
1374
|
raw += chunk;
|
|
1367
1375
|
}
|
|
1368
1376
|
catch { }
|
|
1369
|
-
|
|
1370
|
-
try {
|
|
1371
|
-
session = JSON.parse(raw).session_id ?? "unknown";
|
|
1372
|
-
}
|
|
1373
|
-
catch { }
|
|
1377
|
+
const session = sessionOf(raw);
|
|
1374
1378
|
try {
|
|
1375
1379
|
const { cursorSeq } = await import("./pending.js");
|
|
1376
1380
|
await reportIdle(session, cursorSeq(`hook:${session}`));
|
|
@@ -1444,22 +1448,18 @@ async function main() {
|
|
|
1444
1448
|
process.exit(0);
|
|
1445
1449
|
}
|
|
1446
1450
|
}
|
|
1447
|
-
if (sub === "codex-stop") {
|
|
1451
|
+
if (sub === "codex-stop" || sub === "copilot-stop") {
|
|
1448
1452
|
let raw = "";
|
|
1449
1453
|
try {
|
|
1450
1454
|
for await (const chunk of process.stdin)
|
|
1451
1455
|
raw += chunk;
|
|
1452
1456
|
}
|
|
1453
1457
|
catch { }
|
|
1454
|
-
|
|
1455
|
-
try {
|
|
1456
|
-
stopSession = JSON.parse(raw).session_id ?? "unknown";
|
|
1457
|
-
}
|
|
1458
|
-
catch { }
|
|
1458
|
+
const stopSession = sessionOf(raw);
|
|
1459
1459
|
try {
|
|
1460
1460
|
const { pending: pendingFn } = await import("./pending.js");
|
|
1461
1461
|
const { load: loadCfg } = await import("./config.js");
|
|
1462
|
-
const d = await decideStop(
|
|
1462
|
+
const d = await decideStop(JSON.stringify({ session_id: stopSession }), {
|
|
1463
1463
|
pending: async (session) => (hookCfg ? pendingFn({ session, cfg: hookCfg }) : Promise.resolve(null)),
|
|
1464
1464
|
...(loadCfg()?.member ? { me: loadCfg().member } : {}),
|
|
1465
1465
|
});
|
|
@@ -1557,7 +1557,7 @@ async function main() {
|
|
|
1557
1557
|
}
|
|
1558
1558
|
const states = agentHookStates();
|
|
1559
1559
|
if (!states.length) {
|
|
1560
|
-
console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor on PATH)`);
|
|
1560
|
+
console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor / copilot on PATH)`);
|
|
1561
1561
|
return;
|
|
1562
1562
|
}
|
|
1563
1563
|
for (const s of states) {
|
|
@@ -2007,7 +2007,7 @@ async function main() {
|
|
|
2007
2007
|
const publicChange = name !== undefined || title !== undefined || responsibilities !== undefined ||
|
|
2008
2008
|
humanRequired !== undefined || agentCanDecide !== undefined || capabilities !== undefined || keywords !== undefined;
|
|
2009
2009
|
if (!surface && !publicChange) {
|
|
2010
|
-
die("usage: pantheon profile set [--surface codex|claude-code|cursor] [--name name] [--title title] [--responsibilities a,b] [--human-required a,b] [--agent-can-decide a,b] [--keywords 'domain=word|word;domain=word']");
|
|
2010
|
+
die("usage: pantheon profile set [--surface codex|claude-code|cursor|copilot] [--name name] [--title title] [--responsibilities a,b] [--human-required a,b] [--agent-can-decide a,b] [--keywords 'domain=word|word;domain=word']");
|
|
2011
2011
|
}
|
|
2012
2012
|
if (flag("space")) {
|
|
2013
2013
|
const pf = loadAll() ?? die("not configured");
|
|
@@ -2319,7 +2319,10 @@ async function main() {
|
|
|
2319
2319
|
const prompt = o ? dp(o.id, dt(o.body)) : `Read Pantheon act ${a.id}${goalId ? ` and the thread of goal ${goalId}` : ""}. Explain in plain words what happened and whether anything is owed by me, then give me numbered next steps. Do not act until I choose.`;
|
|
2320
2320
|
console.error(D(`opening ${agent.id}…`));
|
|
2321
2321
|
const { spawnSync } = await import("node:child_process");
|
|
2322
|
-
|
|
2322
|
+
{
|
|
2323
|
+
const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
|
|
2324
|
+
spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
|
|
2325
|
+
}
|
|
2323
2326
|
return;
|
|
2324
2327
|
}
|
|
2325
2328
|
if (choice === "2") {
|
|
@@ -2383,16 +2386,22 @@ async function main() {
|
|
|
2383
2386
|
const bin = cliOnPath("grok") ?? die("grok is not on PATH here");
|
|
2384
2387
|
console.error(D("opening grok…"));
|
|
2385
2388
|
const { spawnSync } = await import("node:child_process");
|
|
2386
|
-
|
|
2389
|
+
{
|
|
2390
|
+
const sp = agentSpawn(bin, [hud.prompt]);
|
|
2391
|
+
spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
|
|
2392
|
+
}
|
|
2387
2393
|
return;
|
|
2388
2394
|
}
|
|
2389
2395
|
if (hud.prompt) {
|
|
2390
2396
|
const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
|
|
2391
2397
|
if (!agent)
|
|
2392
|
-
die("no agent CLI found on this machine (claude, cursor-agent, or
|
|
2398
|
+
die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
|
|
2393
2399
|
console.error(D(`opening ${agent.id}…`));
|
|
2394
2400
|
const { spawnSync } = await import("node:child_process");
|
|
2395
|
-
|
|
2401
|
+
{
|
|
2402
|
+
const sp = agentSpawn(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)));
|
|
2403
|
+
spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
|
|
2404
|
+
}
|
|
2396
2405
|
return;
|
|
2397
2406
|
}
|
|
2398
2407
|
if (hud.decision)
|
|
@@ -2412,10 +2421,13 @@ async function main() {
|
|
|
2412
2421
|
}
|
|
2413
2422
|
const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
|
|
2414
2423
|
if (!agent)
|
|
2415
|
-
die("no agent CLI found on this machine (claude, cursor-agent, or
|
|
2424
|
+
die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
|
|
2416
2425
|
console.error(D(`opening ${agent.id} with the question…`));
|
|
2417
2426
|
const { spawnSync } = await import("node:child_process");
|
|
2418
|
-
|
|
2427
|
+
{
|
|
2428
|
+
const sp = agentSpawn(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)));
|
|
2429
|
+
spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
|
|
2430
|
+
}
|
|
2419
2431
|
return;
|
|
2420
2432
|
}
|
|
2421
2433
|
case "approvals": {
|
package/dist/config.js
CHANGED
|
@@ -28,12 +28,37 @@ export function normalizeOrigin(url) {
|
|
|
28
28
|
const p = new URL(u.includes("://") ? u : `ssh://${u}`);
|
|
29
29
|
const host = p.hostname.toLowerCase();
|
|
30
30
|
const path = p.pathname.replace(/^\/+/, "").replace(/\.git\/?$/, "").replace(/\/+$/, "").toLowerCase();
|
|
31
|
-
|
|
31
|
+
if (!host || !path)
|
|
32
|
+
return null;
|
|
33
|
+
const [h, q] = sameRepo(host, path);
|
|
34
|
+
return `${h}/${q}`;
|
|
32
35
|
}
|
|
33
36
|
catch {
|
|
34
37
|
return null;
|
|
35
38
|
}
|
|
36
39
|
}
|
|
40
|
+
const PUBLIC_HOSTS = new Set(["github.com", "gitlab.com", "bitbucket.org"]);
|
|
41
|
+
function sameRepo(host, path) {
|
|
42
|
+
let m;
|
|
43
|
+
if (host === "ssh.dev.azure.com" || host === "vs-ssh.visualstudio.com") {
|
|
44
|
+
if ((m = /^v3\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(path)))
|
|
45
|
+
return ["dev.azure.com", `${m[1]}/${m[2]}/${m[3]}`];
|
|
46
|
+
}
|
|
47
|
+
else if (host === "dev.azure.com") {
|
|
48
|
+
if ((m = /^([^/]+)\/([^/]+)\/_git\/([^/]+)$/.exec(path)))
|
|
49
|
+
return [host, `${m[1]}/${m[2]}/${m[3]}`];
|
|
50
|
+
}
|
|
51
|
+
else if ((m = /^([^./]+)\.visualstudio\.com$/.exec(host))) {
|
|
52
|
+
const org = m[1];
|
|
53
|
+
if ((m = /^(?:defaultcollection\/)?([^/]+)\/_git\/([^/]+)$/.exec(path)))
|
|
54
|
+
return ["dev.azure.com", `${org}/${m[1]}/${m[2]}`];
|
|
55
|
+
}
|
|
56
|
+
else if (!PUBLIC_HOSTS.has(host)) {
|
|
57
|
+
if ((m = /^(?:[^/]+\/)*scm\/([^/]+\/[^/]+)$/.exec(path)))
|
|
58
|
+
return [host, m[1]];
|
|
59
|
+
}
|
|
60
|
+
return [host, path];
|
|
61
|
+
}
|
|
37
62
|
const repoCache = new Map();
|
|
38
63
|
export function repoInfo(dir = process.cwd()) {
|
|
39
64
|
const key = resolve(dir);
|
package/dist/doctor.js
CHANGED
|
@@ -13,6 +13,8 @@ import { approvalSummary, chosenApproval, onPath } from "./install.js";
|
|
|
13
13
|
import { hookState, readIntent, agentHookStates } from "./notify.js";
|
|
14
14
|
import { loadPersonas } from "./persona.js";
|
|
15
15
|
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
16
|
+
import { chooseAgent, describeChoice } from "./agents.js";
|
|
17
|
+
import { onPathNamed } from "./which.js";
|
|
16
18
|
import { VERSION } from "./version.js";
|
|
17
19
|
import { serviceInstalled } from "./service.js";
|
|
18
20
|
import { hostEnv } from "./host-env.js";
|
|
@@ -134,10 +136,28 @@ export async function doctor(projectDir) {
|
|
|
134
136
|
if (branch)
|
|
135
137
|
add("pass", "git branch", branch);
|
|
136
138
|
}
|
|
137
|
-
const agents = ["claude", "codex", "cursor-agent"].filter((a) =>
|
|
139
|
+
const agents = ["claude", "codex", "cursor-agent", "copilot"].filter((a) => onPathNamed(a));
|
|
138
140
|
agents.length
|
|
139
141
|
? add("pass", "agent cli", agents.join(", "), undefined, agents.join(", "))
|
|
140
|
-
: add("warn", "agent cli", "found none of claude / codex / cursor-agent on PATH", "fine if you only drive agents through a GUI (Conductor, Cursor), but `pantheon worker` needs a CLI to spawn", "none of claude / codex / cursor-agent on PATH");
|
|
142
|
+
: add("warn", "agent cli", "found none of claude / codex / cursor-agent / copilot on PATH", "fine if you only drive agents through a GUI (Conductor, Cursor, VS Code), but `pantheon worker` needs a CLI to spawn", "none of claude / codex / cursor-agent / copilot on PATH");
|
|
143
|
+
if (agents.length) {
|
|
144
|
+
try {
|
|
145
|
+
const choice = chooseAgent("auto", { prefer: prof.agent.primary_surface });
|
|
146
|
+
const line = describeChoice(choice);
|
|
147
|
+
const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
|
|
148
|
+
const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
|
|
149
|
+
const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${choice.skipped.length} skipped (not signed in)` : ""}`;
|
|
150
|
+
if (bad)
|
|
151
|
+
add("warn", "worker agent", line, `nothing is signed in, so every woken run here fails until you sign in: ${choice.skipped.map((x) => x.why).join("; ") || "sign in to the agent CLI"}`, shareLine);
|
|
152
|
+
else if (fellBack)
|
|
153
|
+
add("warn", "worker agent", line, `you chose ${prof.agent.primary_surface} at setup; runs use ${choice.backend.label} until it is signed in again`, shareLine);
|
|
154
|
+
else
|
|
155
|
+
add("pass", "worker agent", line, undefined, shareLine);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
add("warn", "worker agent", e.message, "pantheon setup --agent <claude-code|codex|cursor|copilot> names one that is installed", "could not be chosen");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
141
161
|
const budget = instructionBudget(cfg);
|
|
142
162
|
const budgetLine = `${budget.chars} chars (~${budget.tokens} tokens) of session instructions per turn; the ${budget.protocolChars}-char protocol arrives with the first tool call instead`;
|
|
143
163
|
add(budget.chars > 1_200 ? "warn" : "pass", "mcp context", budgetLine, budget.chars > 1_200 ? "this used to be ~3,800 chars on every turn, and it competed with the work people asked for" : undefined, budgetLine);
|
|
@@ -259,7 +279,7 @@ export async function doctor(projectDir) {
|
|
|
259
279
|
}
|
|
260
280
|
}
|
|
261
281
|
if (!hookStates.length && !hook.installed) {
|
|
262
|
-
add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor on PATH)", "not installed");
|
|
282
|
+
add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor / copilot on PATH)", "not installed");
|
|
263
283
|
}
|
|
264
284
|
else if (!reportsActivity) {
|
|
265
285
|
add("warn", "wake-up hook", `installed (${hook.events?.join(", ")}) but predates the activity hook, so a worker cannot tell your idle editor from one you are using`, "re-run `pantheon hook install --user` to add the UserPromptSubmit half", `installed (${hook.events?.join(", ")}), predates the activity hook`);
|