@join-pantheon/cli 0.25.3 → 0.26.0
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/dist/agents.js +153 -15
- package/dist/cli.js +22 -23
- package/dist/config.js +26 -1
- package/dist/doctor.js +22 -3
- package/dist/hooks-install.js +58 -0
- package/dist/install.js +78 -13
- 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 +12 -2
- package/dist/persona.js +1 -1
- package/dist/profile.js +4 -1
- package/dist/run-failure.js +15 -0
- package/dist/setup-guide.js +1 -1
- package/dist/tray.js +1 -1
- package/dist/worker.js +14 -6
- package/package.json +1 -1
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/dist/agents.js
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, dirname } from "node:path";
|
|
4
|
+
import { join, dirname, isAbsolute } from "node:path";
|
|
5
5
|
import { launcher } from "./install.js";
|
|
6
6
|
import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
7
7
|
export function launchArgs(agent, prompt, name) {
|
|
8
|
-
|
|
8
|
+
if (agent === "claude-code")
|
|
9
|
+
return ["--name", name, prompt];
|
|
10
|
+
if (agent === "copilot")
|
|
11
|
+
return ["-n", name, "-i", prompt];
|
|
12
|
+
return [prompt];
|
|
9
13
|
}
|
|
10
14
|
export const AGENTS = [
|
|
11
15
|
{ id: "claude-code", label: "Claude Code", binaries: ["claude"] },
|
|
12
16
|
{ id: "codex", label: "Codex", binaries: ["codex"] },
|
|
13
17
|
{ id: "cursor", label: "Cursor", binaries: ["cursor-agent", "agent"] },
|
|
18
|
+
{ id: "copilot", label: "Copilot", binaries: ["copilot"] },
|
|
14
19
|
];
|
|
20
|
+
export function copilotHome(home = homedir(), env = process.env) {
|
|
21
|
+
return env.COPILOT_HOME || join(home, ".copilot");
|
|
22
|
+
}
|
|
23
|
+
export function hasCopilotExtension(home = homedir()) {
|
|
24
|
+
try {
|
|
25
|
+
return readdirSync(join(home, ".vscode", "extensions")).some((d) => d.startsWith("github.copilot"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
15
31
|
const sh = (cmd) => {
|
|
16
32
|
try {
|
|
17
33
|
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -35,6 +51,11 @@ export function detectForHooks(projectDir = process.cwd()) {
|
|
|
35
51
|
out.push(backend);
|
|
36
52
|
continue;
|
|
37
53
|
}
|
|
54
|
+
if (backend.id === "copilot") {
|
|
55
|
+
if (existsSync(copilotHome()) || hasCopilotExtension())
|
|
56
|
+
out.push(backend);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
38
59
|
const userDir = backend.id === "claude-code" ? join(homedir(), ".claude")
|
|
39
60
|
: backend.id === "codex" ? join(homedir(), ".codex")
|
|
40
61
|
: join(homedir(), ".cursor");
|
|
@@ -79,21 +100,95 @@ export function resolveBinary(backend) {
|
|
|
79
100
|
}
|
|
80
101
|
return null;
|
|
81
102
|
}
|
|
82
|
-
export
|
|
103
|
+
export const SIGN_IN_CHECK = {
|
|
104
|
+
"claude-code": { args: ["auth", "status"], fix: "run `claude`, then /login" },
|
|
105
|
+
codex: { args: ["login", "status"], fix: "run `codex login`" },
|
|
106
|
+
cursor: { args: ["status"], fix: "run `cursor-agent login`" },
|
|
107
|
+
copilot: { args: [], fix: "run `copilot login` (or `gh auth login` — copilot uses gh's sign-in too)" },
|
|
108
|
+
};
|
|
109
|
+
export const COPILOT_TOKEN_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
|
|
110
|
+
export function copilotSignedIn(run = runStatus, env = process.env, gh = () => !!cliOnPath("gh")) {
|
|
111
|
+
if (COPILOT_TOKEN_VARS.some((k) => env[k]))
|
|
112
|
+
return { ok: true, detail: "signed in (token in the environment)" };
|
|
113
|
+
if (!gh())
|
|
114
|
+
return { ok: false, unknown: true, detail: "could not tell (copilot has no status command, and gh is not on PATH)" };
|
|
115
|
+
const r = run("gh", ["auth", "status"]);
|
|
116
|
+
if (r.status === 0)
|
|
117
|
+
return { ok: true, detail: "signed in (via gh)" };
|
|
118
|
+
return { ok: false, unknown: true, detail: "could not tell (gh is not signed in; a `copilot login` is not visible from outside)" };
|
|
119
|
+
}
|
|
120
|
+
const runStatus = (binary, args) => {
|
|
121
|
+
try {
|
|
122
|
+
const out = execFileSync(binary, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 8_000, env: { ...process.env, NO_COLOR: "1" } });
|
|
123
|
+
return { status: 0, out };
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
const err = e;
|
|
127
|
+
if (err.killed)
|
|
128
|
+
return { status: null, out: "timed out" };
|
|
129
|
+
return { status: err.status ?? null, out: `${err.stdout ?? ""}${err.stderr ?? ""}`.trim() || (err.message ?? "") };
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
export const AGENT_HOME_DIR = { "claude-code": ".claude", codex: ".codex", cursor: ".cursor", copilot: ".copilot" };
|
|
133
|
+
export function signedIn(backend, binary, run = runStatus, home = homedir()) {
|
|
134
|
+
if (backend.id === "copilot")
|
|
135
|
+
return copilotSignedIn(run);
|
|
136
|
+
if (!existsSync(join(home, AGENT_HOME_DIR[backend.id])))
|
|
137
|
+
return { ok: false, detail: "never signed in on this machine" };
|
|
138
|
+
const check = SIGN_IN_CHECK[backend.id];
|
|
139
|
+
const r = run(binary, check.args);
|
|
140
|
+
if (r.status === null)
|
|
141
|
+
return { ok: false, unknown: true, detail: `could not tell (${(r.out.split("\n")[0] ?? "").slice(0, 60) || "no answer"})` };
|
|
142
|
+
if (backend.id === "claude-code") {
|
|
143
|
+
try {
|
|
144
|
+
const j = JSON.parse(r.out);
|
|
145
|
+
if (typeof j.loggedIn === "boolean")
|
|
146
|
+
return j.loggedIn ? { ok: true, detail: "signed in" } : { ok: false, detail: "not signed in" };
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
}
|
|
150
|
+
return r.status === 0 ? { ok: true, detail: "signed in" } : { ok: false, detail: "not signed in" };
|
|
151
|
+
}
|
|
152
|
+
export function chooseAgent(preferred, opts = {}) {
|
|
153
|
+
const available = opts.available ?? detectAvailable();
|
|
154
|
+
const check = opts.check === null ? null : (opts.check ?? ((b, bin) => signedIn(b, bin, undefined, opts.home)));
|
|
155
|
+
const state = (b, bin) => (check ? check(b, bin) : undefined);
|
|
83
156
|
if (preferred && preferred !== "auto") {
|
|
84
157
|
const backend = agentById(preferred);
|
|
85
158
|
if (!backend)
|
|
86
|
-
throw new Error(`unknown agent ${preferred} — use claude-code, codex, cursor, or auto`);
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
159
|
+
throw new Error(`unknown agent ${preferred} — use claude-code, codex, cursor, copilot, or auto`);
|
|
160
|
+
const hit = available.find((a) => a.backend.id === backend.id);
|
|
161
|
+
if (!hit)
|
|
89
162
|
throw new Error(`${backend.label} is not on PATH (looked for ${backend.binaries.join(", ")})`);
|
|
90
|
-
return { backend, binary };
|
|
163
|
+
return { backend, binary: hit.binary, signIn: state(backend, hit.binary), because: "pinned", skipped: [] };
|
|
91
164
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
165
|
+
if (!available.length)
|
|
166
|
+
throw new Error("no agent CLI on PATH — install claude, codex, cursor-agent, or copilot");
|
|
167
|
+
const order = [];
|
|
168
|
+
const wanted = opts.prefer ? agentById(opts.prefer) : undefined;
|
|
169
|
+
const chosen = wanted ? available.find((a) => a.backend.id === wanted.id) : undefined;
|
|
170
|
+
if (chosen)
|
|
171
|
+
order.push({ ...chosen, because: "chosen at setup" });
|
|
172
|
+
for (const a of available)
|
|
173
|
+
if (!order.some((o) => o.backend.id === a.backend.id))
|
|
174
|
+
order.push({ ...a, because: available.length === 1 ? "only one on PATH" : "first signed in on PATH" });
|
|
175
|
+
const skipped = [];
|
|
176
|
+
for (const c of order) {
|
|
177
|
+
const s = state(c.backend, c.binary);
|
|
178
|
+
if (!s || s.ok || s.unknown)
|
|
179
|
+
return { backend: c.backend, binary: c.binary, ...(s ? { signIn: s } : {}), because: c.because, skipped };
|
|
180
|
+
skipped.push({ backend: c.backend, why: `${s.detail} — ${SIGN_IN_CHECK[c.backend.id].fix}` });
|
|
181
|
+
}
|
|
182
|
+
const first = order[0];
|
|
183
|
+
return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
|
|
184
|
+
}
|
|
185
|
+
export function describeChoice(c) {
|
|
186
|
+
const head = `${c.backend.label}${c.signIn ? ` — ${c.signIn.detail}` : ""} (${c.because})`;
|
|
187
|
+
return c.skipped.length ? `${head}; skipped ${c.skipped.map((s) => `${s.backend.label} — ${s.why}`).join("; ")}` : head;
|
|
188
|
+
}
|
|
189
|
+
export function resolveAgent(preferred) {
|
|
190
|
+
const c = chooseAgent(preferred, { check: null });
|
|
191
|
+
return { backend: c.backend, binary: c.binary };
|
|
97
192
|
}
|
|
98
193
|
export { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
99
194
|
export function headlessSpawn(opts) {
|
|
@@ -159,6 +254,32 @@ export function headlessSpawn(opts) {
|
|
|
159
254
|
env: procEnv,
|
|
160
255
|
};
|
|
161
256
|
}
|
|
257
|
+
if (opts.backend.id === "copilot") {
|
|
258
|
+
const home = opts.runHome ? join(opts.runHome, "copilot") : join(opts.worktree, ".pantheon-copilot-home");
|
|
259
|
+
const config = opts.runHome ? readCopilotConfig() : {};
|
|
260
|
+
const trusted = Array.isArray(config.trustedFolders) ? config.trustedFolders.filter((x) => typeof x === "string") : [];
|
|
261
|
+
if (!trusted.includes(opts.worktree))
|
|
262
|
+
trusted.push(opts.worktree);
|
|
263
|
+
const settings = join(copilotHome(), "settings.json");
|
|
264
|
+
return {
|
|
265
|
+
command: opts.binary,
|
|
266
|
+
args: [
|
|
267
|
+
"-p", opts.prompt, "-s",
|
|
268
|
+
"--no-auto-update", "--no-ask-user", "--disable-builtin-mcps",
|
|
269
|
+
"--disable-mcp-server", "pantheon",
|
|
270
|
+
...(opts.allowBash ? ["--allow-all-tools"] : [`--allow-tool=${COPILOT_WORKER_SERVER}`, "--allow-tool=write"]),
|
|
271
|
+
],
|
|
272
|
+
env: { ...procEnv, COPILOT_HOME: home },
|
|
273
|
+
writeFiles: [
|
|
274
|
+
{
|
|
275
|
+
path: join(home, "mcp-config.json"),
|
|
276
|
+
contents: JSON.stringify({ mcpServers: { [COPILOT_WORKER_SERVER]: { type: "local", command: l.command, args: l.args, env: fileEnv, tools: ["*"] } } }, null, 2),
|
|
277
|
+
},
|
|
278
|
+
{ path: join(home, "config.json"), contents: JSON.stringify({ ...config, trustedFolders: trusted }, null, 2) },
|
|
279
|
+
],
|
|
280
|
+
...(opts.runHome && existsSync(settings) ? { copyFiles: [{ from: settings, to: join(home, "settings.json") }] } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
162
283
|
return {
|
|
163
284
|
command: opts.binary,
|
|
164
285
|
args: [
|
|
@@ -185,15 +306,32 @@ export function headlessSpawn(opts) {
|
|
|
185
306
|
],
|
|
186
307
|
};
|
|
187
308
|
}
|
|
309
|
+
export const COPILOT_WORKER_SERVER = "pantheon-worker";
|
|
310
|
+
function readCopilotConfig() {
|
|
311
|
+
try {
|
|
312
|
+
const doc = JSON.parse(readFileSync(join(copilotHome(), "config.json"), "utf8"));
|
|
313
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : {};
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
188
319
|
export function tomlInlineTable(kv) {
|
|
189
320
|
return `{${Object.entries(kv).map(([k, v]) => `${k} = ${JSON.stringify(v)}`).join(", ")}}`;
|
|
190
321
|
}
|
|
191
322
|
export function materializeSpawnFiles(worktree, spawn) {
|
|
192
323
|
for (const f of spawn.writeFiles ?? []) {
|
|
193
|
-
const p = join(worktree, f.path);
|
|
194
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
324
|
+
const p = isAbsolute(f.path) ? f.path : join(worktree, f.path);
|
|
325
|
+
mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
|
|
195
326
|
writeFileSync(p, f.contents, { mode: 0o600 });
|
|
196
327
|
}
|
|
328
|
+
for (const c of spawn.copyFiles ?? []) {
|
|
329
|
+
try {
|
|
330
|
+
mkdirSync(dirname(c.to), { recursive: true, mode: 0o700 });
|
|
331
|
+
copyFileSync(c.from, c.to);
|
|
332
|
+
}
|
|
333
|
+
catch { }
|
|
334
|
+
}
|
|
197
335
|
}
|
|
198
336
|
export function backendFromLabel(label) {
|
|
199
337
|
if (!label)
|
package/dist/cli.js
CHANGED
|
@@ -427,6 +427,8 @@ function printReady(tools) {
|
|
|
427
427
|
oneTime.set("cursor", `cursor enable the "pantheon" MCP server if it asks`);
|
|
428
428
|
if (/codex/i.test(t.tool))
|
|
429
429
|
oneTime.set("codex", `codex run /hooks inside codex once to approve the wake-up hook`);
|
|
430
|
+
if (/copilot/i.test(t.tool))
|
|
431
|
+
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
432
|
}
|
|
431
433
|
if (oneTime.size) {
|
|
432
434
|
console.error(`\n${B("Your agents may each ask once")} ${D("(their own security, not ours — one yes each):")}`);
|
|
@@ -484,7 +486,7 @@ ${B("control")}
|
|
|
484
486
|
pantheon admit <who> let someone in (--may-wake to allow unattended work here)
|
|
485
487
|
pantheon admit <who> --revoke remove them
|
|
486
488
|
pantheon worker stay answerable while you are away
|
|
487
|
-
(--agent claude-code|codex|cursor|auto, --persona <name>)
|
|
489
|
+
(--agent claude-code|codex|cursor|copilot|auto, --persona <name>)
|
|
488
490
|
pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
|
|
489
491
|
pantheon persona how a woken worker should behave: list / init <name> / show <name>
|
|
490
492
|
pantheon notifier alert you when a task requires your decision
|
|
@@ -497,7 +499,7 @@ ${B("control")}
|
|
|
497
499
|
pantheon approvals decisions waiting on you; show or answer one
|
|
498
500
|
pantheon stop [relay|worker|notifier|menubar|all] stop it
|
|
499
501
|
pantheon hook install your agent keeps working when a peer is waiting on it
|
|
500
|
-
(auto-detects claude / codex / cursor on this machine)
|
|
502
|
+
(auto-detects claude / codex / cursor / copilot on this machine)
|
|
501
503
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
502
504
|
pantheon leave remove what pantheon put on this machine
|
|
503
505
|
pantheon panic cut off all unattended work on this machine, now
|
|
@@ -1314,6 +1316,15 @@ async function main() {
|
|
|
1314
1316
|
case "hook": {
|
|
1315
1317
|
const sub = argv[1] ?? "status";
|
|
1316
1318
|
const hookCfg = resolveForHook();
|
|
1319
|
+
const sessionOf = (raw) => {
|
|
1320
|
+
try {
|
|
1321
|
+
const j = JSON.parse(raw);
|
|
1322
|
+
return j.session_id ?? j.sessionId ?? j.conversation_id ?? "unknown";
|
|
1323
|
+
}
|
|
1324
|
+
catch {
|
|
1325
|
+
return "unknown";
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1317
1328
|
const reportIdle = async (session, seen) => {
|
|
1318
1329
|
if (!hookCfg)
|
|
1319
1330
|
return true;
|
|
@@ -1334,11 +1345,7 @@ async function main() {
|
|
|
1334
1345
|
raw += chunk;
|
|
1335
1346
|
}
|
|
1336
1347
|
catch { }
|
|
1337
|
-
|
|
1338
|
-
try {
|
|
1339
|
-
session = JSON.parse(raw).session_id ?? "unknown";
|
|
1340
|
-
}
|
|
1341
|
-
catch { }
|
|
1348
|
+
const session = sessionOf(raw);
|
|
1342
1349
|
try {
|
|
1343
1350
|
if (hookCfg) {
|
|
1344
1351
|
const { reportActivity } = await import("./pending.js");
|
|
@@ -1366,11 +1373,7 @@ async function main() {
|
|
|
1366
1373
|
raw += chunk;
|
|
1367
1374
|
}
|
|
1368
1375
|
catch { }
|
|
1369
|
-
|
|
1370
|
-
try {
|
|
1371
|
-
session = JSON.parse(raw).session_id ?? "unknown";
|
|
1372
|
-
}
|
|
1373
|
-
catch { }
|
|
1376
|
+
const session = sessionOf(raw);
|
|
1374
1377
|
try {
|
|
1375
1378
|
const { cursorSeq } = await import("./pending.js");
|
|
1376
1379
|
await reportIdle(session, cursorSeq(`hook:${session}`));
|
|
@@ -1444,22 +1447,18 @@ async function main() {
|
|
|
1444
1447
|
process.exit(0);
|
|
1445
1448
|
}
|
|
1446
1449
|
}
|
|
1447
|
-
if (sub === "codex-stop") {
|
|
1450
|
+
if (sub === "codex-stop" || sub === "copilot-stop") {
|
|
1448
1451
|
let raw = "";
|
|
1449
1452
|
try {
|
|
1450
1453
|
for await (const chunk of process.stdin)
|
|
1451
1454
|
raw += chunk;
|
|
1452
1455
|
}
|
|
1453
1456
|
catch { }
|
|
1454
|
-
|
|
1455
|
-
try {
|
|
1456
|
-
stopSession = JSON.parse(raw).session_id ?? "unknown";
|
|
1457
|
-
}
|
|
1458
|
-
catch { }
|
|
1457
|
+
const stopSession = sessionOf(raw);
|
|
1459
1458
|
try {
|
|
1460
1459
|
const { pending: pendingFn } = await import("./pending.js");
|
|
1461
1460
|
const { load: loadCfg } = await import("./config.js");
|
|
1462
|
-
const d = await decideStop(
|
|
1461
|
+
const d = await decideStop(JSON.stringify({ session_id: stopSession }), {
|
|
1463
1462
|
pending: async (session) => (hookCfg ? pendingFn({ session, cfg: hookCfg }) : Promise.resolve(null)),
|
|
1464
1463
|
...(loadCfg()?.member ? { me: loadCfg().member } : {}),
|
|
1465
1464
|
});
|
|
@@ -1557,7 +1556,7 @@ async function main() {
|
|
|
1557
1556
|
}
|
|
1558
1557
|
const states = agentHookStates();
|
|
1559
1558
|
if (!states.length) {
|
|
1560
|
-
console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor on PATH)`);
|
|
1559
|
+
console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor / copilot on PATH)`);
|
|
1561
1560
|
return;
|
|
1562
1561
|
}
|
|
1563
1562
|
for (const s of states) {
|
|
@@ -2007,7 +2006,7 @@ async function main() {
|
|
|
2007
2006
|
const publicChange = name !== undefined || title !== undefined || responsibilities !== undefined ||
|
|
2008
2007
|
humanRequired !== undefined || agentCanDecide !== undefined || capabilities !== undefined || keywords !== undefined;
|
|
2009
2008
|
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']");
|
|
2009
|
+
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
2010
|
}
|
|
2012
2011
|
if (flag("space")) {
|
|
2013
2012
|
const pf = loadAll() ?? die("not configured");
|
|
@@ -2389,7 +2388,7 @@ async function main() {
|
|
|
2389
2388
|
if (hud.prompt) {
|
|
2390
2389
|
const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
|
|
2391
2390
|
if (!agent)
|
|
2392
|
-
die("no agent CLI found on this machine (claude, cursor-agent, or
|
|
2391
|
+
die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
|
|
2393
2392
|
console.error(D(`opening ${agent.id}…`));
|
|
2394
2393
|
const { spawnSync } = await import("node:child_process");
|
|
2395
2394
|
spawnSync(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)), { stdio: "inherit" });
|
|
@@ -2412,7 +2411,7 @@ async function main() {
|
|
|
2412
2411
|
}
|
|
2413
2412
|
const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
|
|
2414
2413
|
if (!agent)
|
|
2415
|
-
die("no agent CLI found on this machine (claude, cursor-agent, or
|
|
2414
|
+
die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
|
|
2416
2415
|
console.error(D(`opening ${agent.id} with the question…`));
|
|
2417
2416
|
const { spawnSync } = await import("node:child_process");
|
|
2418
2417
|
spawnSync(agent.binary, launchArgs(agent.id, prompt, sessionName(prompt)), { stdio: "inherit" });
|
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,7 @@ 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";
|
|
16
17
|
import { VERSION } from "./version.js";
|
|
17
18
|
import { serviceInstalled } from "./service.js";
|
|
18
19
|
import { hostEnv } from "./host-env.js";
|
|
@@ -134,10 +135,28 @@ export async function doctor(projectDir) {
|
|
|
134
135
|
if (branch)
|
|
135
136
|
add("pass", "git branch", branch);
|
|
136
137
|
}
|
|
137
|
-
const agents = ["claude", "codex", "cursor-agent"].filter((a) => sh(`command -v ${a}`));
|
|
138
|
+
const agents = ["claude", "codex", "cursor-agent", "copilot"].filter((a) => sh(`command -v ${a}`));
|
|
138
139
|
agents.length
|
|
139
140
|
? 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");
|
|
141
|
+
: 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");
|
|
142
|
+
if (agents.length) {
|
|
143
|
+
try {
|
|
144
|
+
const choice = chooseAgent("auto", { prefer: prof.agent.primary_surface });
|
|
145
|
+
const line = describeChoice(choice);
|
|
146
|
+
const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
|
|
147
|
+
const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
|
|
148
|
+
const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${choice.skipped.length} skipped (not signed in)` : ""}`;
|
|
149
|
+
if (bad)
|
|
150
|
+
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);
|
|
151
|
+
else if (fellBack)
|
|
152
|
+
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);
|
|
153
|
+
else
|
|
154
|
+
add("pass", "worker agent", line, undefined, shareLine);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
add("warn", "worker agent", e.message, "pantheon setup --agent <claude-code|codex|cursor|copilot> names one that is installed", "could not be chosen");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
141
160
|
const budget = instructionBudget(cfg);
|
|
142
161
|
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
162
|
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 +278,7 @@ export async function doctor(projectDir) {
|
|
|
259
278
|
}
|
|
260
279
|
}
|
|
261
280
|
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");
|
|
281
|
+
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
282
|
}
|
|
264
283
|
else if (!reportsActivity) {
|
|
265
284
|
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`);
|
package/dist/hooks-install.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { HOOK_MARKER, MAX_CONSECUTIVE_BLOCKS } from "./notify.js";
|
|
5
|
+
import { copilotHome } from "./agents.js";
|
|
5
6
|
function ours(cmd) {
|
|
6
7
|
return !!cmd?.includes(HOOK_MARKER);
|
|
7
8
|
}
|
|
@@ -189,4 +190,61 @@ export function removeCursorHooks(projectDir = process.cwd()) {
|
|
|
189
190
|
}
|
|
190
191
|
return out;
|
|
191
192
|
}
|
|
193
|
+
export function copilotHooksPath(scope, projectDir = process.cwd()) {
|
|
194
|
+
return scope === "user"
|
|
195
|
+
? join(copilotHome(), "hooks", "pantheon.json")
|
|
196
|
+
: join(projectDir, ".github", "hooks", "pantheon.json");
|
|
197
|
+
}
|
|
198
|
+
function copilotEvents(bin) {
|
|
199
|
+
return {
|
|
200
|
+
agentStop: `${bin} hook copilot-stop`,
|
|
201
|
+
userPromptSubmitted: `${bin} hook prompt`,
|
|
202
|
+
sessionEnd: `${bin} hook end`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export function copilotHookState(scope, projectDir = process.cwd()) {
|
|
206
|
+
const file = copilotHooksPath(scope, projectDir);
|
|
207
|
+
const doc = readJson(file);
|
|
208
|
+
if (!doc?.hooks)
|
|
209
|
+
return { agent: "copilot", label: "Copilot", installed: false };
|
|
210
|
+
const events = [];
|
|
211
|
+
for (const [event, entries] of Object.entries(doc.hooks)) {
|
|
212
|
+
if ((entries ?? []).some((h) => ours(h.command) || ours(h.bash)))
|
|
213
|
+
events.push(event);
|
|
214
|
+
}
|
|
215
|
+
return events.length
|
|
216
|
+
? { agent: "copilot", label: "Copilot", installed: true, scope, file, events }
|
|
217
|
+
: { agent: "copilot", label: "Copilot", installed: false };
|
|
218
|
+
}
|
|
219
|
+
export function installCopilotHooks(bin, scope, projectDir = process.cwd()) {
|
|
220
|
+
const file = copilotHooksPath(scope, projectDir);
|
|
221
|
+
const existed = existsSync(file);
|
|
222
|
+
const events = copilotEvents(bin);
|
|
223
|
+
const doc = { version: 1, hooks: {} };
|
|
224
|
+
for (const [event, cmd] of Object.entries(events)) {
|
|
225
|
+
doc.hooks[event] = [{ type: "command", command: cmd, timeoutSec: 10, timeout: 10 }];
|
|
226
|
+
}
|
|
227
|
+
writeJson(file, doc);
|
|
228
|
+
return [
|
|
229
|
+
` ${existed ? "updated" : "installed"} ${file}`,
|
|
230
|
+
` runs ${events.agentStop}`,
|
|
231
|
+
` ${events.userPromptSubmitted}`,
|
|
232
|
+
` ${events.sessionEnd}`,
|
|
233
|
+
` ${""}(read by Copilot CLI and by Copilot in VS Code${scope === "project" ? "; repo-level hooks run once the folder is trusted" : ""})`,
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
export function removeCopilotHooks(projectDir = process.cwd()) {
|
|
237
|
+
const out = [];
|
|
238
|
+
for (const scope of ["project", "user"]) {
|
|
239
|
+
const file = copilotHooksPath(scope, projectDir);
|
|
240
|
+
if (!copilotHookState(scope, projectDir).installed)
|
|
241
|
+
continue;
|
|
242
|
+
try {
|
|
243
|
+
rmSync(file, { force: true });
|
|
244
|
+
out.push(` removed ${file}`);
|
|
245
|
+
}
|
|
246
|
+
catch { }
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
192
250
|
//# sourceMappingURL=hooks-install.js.map
|
package/dist/install.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { homedir } from "node:os";
|
|
3
|
+
import { homedir, platform } from "node:os";
|
|
4
4
|
import { join, dirname, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { grantToolPermissions, claudeSettingsFile, cursorCliConfigFile } from "./grants.js";
|
|
@@ -23,26 +23,52 @@ export function launcher(force) {
|
|
|
23
23
|
? { command: "pantheon", args: ["mcp"], portable: true }
|
|
24
24
|
: { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
25
25
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
function readConfigJson(file) {
|
|
27
|
+
if (!existsSync(file))
|
|
28
|
+
return {};
|
|
29
|
+
const text = readFileSync(file, "utf8");
|
|
30
|
+
if (!text.trim())
|
|
31
|
+
return {};
|
|
32
|
+
try {
|
|
33
|
+
const doc = JSON.parse(text);
|
|
34
|
+
if (doc && typeof doc === "object" && !Array.isArray(doc))
|
|
35
|
+
return doc;
|
|
35
36
|
}
|
|
37
|
+
catch { }
|
|
38
|
+
throw new Error(`${file} is not valid JSON — fix or move it, then re-run`);
|
|
39
|
+
}
|
|
40
|
+
function mergeMcpJson(file, entry, shape = {}) {
|
|
41
|
+
const doc = readConfigJson(file);
|
|
36
42
|
const existed = existsSync(file);
|
|
37
43
|
doc.mcpServers ??= {};
|
|
38
44
|
const before = JSON.stringify(doc.mcpServers["pantheon"] ?? null);
|
|
39
|
-
doc.mcpServers["pantheon"] = { ...entry, env: {} };
|
|
45
|
+
doc.mcpServers["pantheon"] = { ...shape, ...entry, env: {} };
|
|
40
46
|
if (before === JSON.stringify(doc.mcpServers["pantheon"]))
|
|
41
47
|
return "unchanged";
|
|
42
48
|
mkdirSync(dirname(file), { recursive: true });
|
|
43
49
|
writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
44
50
|
return existed ? "updated" : "created";
|
|
45
51
|
}
|
|
52
|
+
export const COPILOT_ENTRY_SHAPE = { type: "local", tools: ["*"] };
|
|
53
|
+
export function mergeVsCodeMcpJson(file, entry) {
|
|
54
|
+
const doc = readConfigJson(file);
|
|
55
|
+
const existed = existsSync(file);
|
|
56
|
+
doc.servers ??= {};
|
|
57
|
+
const before = JSON.stringify(doc.servers["pantheon"] ?? null);
|
|
58
|
+
doc.servers["pantheon"] = { type: "stdio", command: entry.command, args: entry.args };
|
|
59
|
+
if (before === JSON.stringify(doc.servers["pantheon"]))
|
|
60
|
+
return "unchanged";
|
|
61
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
62
|
+
writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
|
|
63
|
+
return existed ? "updated" : "created";
|
|
64
|
+
}
|
|
65
|
+
export const copilotMcpConfigFile = (home = homedir(), env = process.env) => join(env.COPILOT_HOME || join(home, ".copilot"), "mcp-config.json");
|
|
66
|
+
export function vsCodeUserDir(home = homedir(), os = platform(), env = process.env) {
|
|
67
|
+
const dir = os === "darwin" ? join(home, "Library", "Application Support", "Code", "User")
|
|
68
|
+
: os === "win32" ? join(env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User")
|
|
69
|
+
: join(env.XDG_CONFIG_HOME || join(home, ".config"), "Code", "User");
|
|
70
|
+
return existsSync(dir) ? dir : null;
|
|
71
|
+
}
|
|
46
72
|
const BRIEF_MARK = "<!-- pantheon:begin -->";
|
|
47
73
|
function brief(cfg) {
|
|
48
74
|
return `${BRIEF_MARK}
|
|
@@ -95,14 +121,19 @@ export function install(projectDir, cfg, force) {
|
|
|
95
121
|
const out = [];
|
|
96
122
|
const l = launcher(force);
|
|
97
123
|
const entry = { command: l.command, args: l.args };
|
|
98
|
-
out.push(` .mcp.json ${mergeMcpJson(join(projectDir, ".mcp.json"), entry)} (claude code, conductor)`);
|
|
124
|
+
out.push(` .mcp.json ${mergeMcpJson(join(projectDir, ".mcp.json"), entry)} (claude code, conductor, copilot cli)`);
|
|
99
125
|
out.push(` .cursor/mcp.json ${mergeMcpJson(join(projectDir, ".cursor", "mcp.json"), entry)} (cursor)`);
|
|
126
|
+
const vscodeDir = join(projectDir, ".vscode");
|
|
127
|
+
out.push(existsSync(vscodeDir) || vsCodeUserDir()
|
|
128
|
+
? ` .vscode/mcp.json ${mergeVsCodeMcpJson(join(vscodeDir, "mcp.json"), entry)} (copilot in vs code)`
|
|
129
|
+
: ` .vscode/mcp.json ${"skipped".padEnd(9)} no .vscode/ here and no VS Code on this machine (a VS Code user runs pantheon install --user)`);
|
|
100
130
|
for (const f of ["CLAUDE.md", "AGENTS.md"]) {
|
|
101
131
|
const p = join(projectDir, f);
|
|
102
132
|
if (existsSync(p) || f === "AGENTS.md")
|
|
103
133
|
out.push(` ${f.padEnd(25)} ${upsertBrief(p, cfg)} (the collaboration brief)`);
|
|
104
134
|
}
|
|
105
135
|
out.push("");
|
|
136
|
+
out.push(" Copilot asks once to trust this folder before it reads the repo's MCP servers and hooks.");
|
|
106
137
|
out.push(" Codex reads ~/.codex/config.toml, which `pantheon join` already wrote at user scope.");
|
|
107
138
|
out.push(" If you need it by hand, this is the block (absolute path so a GUI launch finds it):");
|
|
108
139
|
out.push("");
|
|
@@ -172,11 +203,44 @@ function cursorUserScope(entry) {
|
|
|
172
203
|
return { tool: "cursor", where: file, status: `failed: ${e.message}` };
|
|
173
204
|
}
|
|
174
205
|
}
|
|
206
|
+
function copilotUserScope(entry) {
|
|
207
|
+
const tool = "copilot";
|
|
208
|
+
const file = copilotMcpConfigFile();
|
|
209
|
+
let present = existsSync(dirname(file));
|
|
210
|
+
if (!present) {
|
|
211
|
+
try {
|
|
212
|
+
execFileSync("sh", ["-lc", "command -v copilot"], { stdio: "ignore" });
|
|
213
|
+
present = true;
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
}
|
|
217
|
+
if (!present)
|
|
218
|
+
return { tool, where: "-", status: "skipped (copilot not on PATH)" };
|
|
219
|
+
try {
|
|
220
|
+
return { tool, where: file, status: mergeMcpJson(file, entry, COPILOT_ENTRY_SHAPE) };
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
return { tool, where: file, status: `failed: ${e.message}` };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function vsCodeUserScope(entry) {
|
|
227
|
+
const tool = "vs code";
|
|
228
|
+
const dir = vsCodeUserDir();
|
|
229
|
+
if (!dir)
|
|
230
|
+
return { tool, where: "-", status: "skipped (VS Code not found)" };
|
|
231
|
+
const file = join(dir, "mcp.json");
|
|
232
|
+
try {
|
|
233
|
+
return { tool, where: file, status: mergeVsCodeMcpJson(file, entry) };
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
return { tool, where: file, status: `failed: ${e.message}` };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
175
239
|
export const CODEX_APPROVAL_LINE = `default_tools_approval_mode = "approve"`;
|
|
176
240
|
export function approvalSummary(approval) {
|
|
177
241
|
return approval === "ask-before-sending"
|
|
178
242
|
? `your agent asks you before each send, goal or claim in your name; reading and waiting run without a prompt. Unattended runs keep their own per-run approvals — this governs the agent you sit in front of`
|
|
179
|
-
: `pantheon's ${PANTHEON_TOOL_NAMES.length} tools run without a per-call prompt in Claude Code, Cursor and Codex — your agent coordinates for you without stopping to ask`;
|
|
243
|
+
: `pantheon's ${PANTHEON_TOOL_NAMES.length} tools run without a per-call prompt in Claude Code, Cursor and Codex — your agent coordinates for you without stopping to ask. Copilot has no allow-list file: it asks once per tool per folder and remembers your answer`;
|
|
180
244
|
}
|
|
181
245
|
export function codexTomlBlock(entry, env = { PANTHEON_AGENT: "codex" }, approval = "approve") {
|
|
182
246
|
const envToml = Object.entries(env)
|
|
@@ -374,6 +438,7 @@ export function installForUser(approval = chosenApproval()) {
|
|
|
374
438
|
const entry = userEntry();
|
|
375
439
|
return [
|
|
376
440
|
claudeUserScope(entry), cursorUserScope(entry), codexUserScope(entry, approval),
|
|
441
|
+
copilotUserScope(entry), vsCodeUserScope(entry),
|
|
377
442
|
...grantToolPermissions(approval).map((g) => ({
|
|
378
443
|
tool: g.tool,
|
|
379
444
|
where: g.tool.startsWith("claude") ? claudeSettingsFile() : cursorCliConfigFile(),
|
package/dist/machine.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir, platform } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
6
6
|
import { plistPath } from "./service.js";
|
|
7
|
+
import { copilotMcpConfigFile, vsCodeUserDir } from "./install.js";
|
|
7
8
|
const sh = (cmd) => {
|
|
8
9
|
try {
|
|
9
10
|
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -109,6 +110,16 @@ export function wiring() {
|
|
|
109
110
|
tool: "codex",
|
|
110
111
|
state: existsSync(codex) && /\[mcp_servers\.pantheon\]/.test(readFileSync(codex, "utf8")) ? "wired" : "not wired",
|
|
111
112
|
});
|
|
113
|
+
const copilot = copilotMcpConfigFile();
|
|
114
|
+
out.push({
|
|
115
|
+
tool: "copilot",
|
|
116
|
+
state: existsSync(copilot) && /"pantheon"/.test(readFileSync(copilot, "utf8")) ? "wired" : "not wired",
|
|
117
|
+
});
|
|
118
|
+
const vscodeDir = vsCodeUserDir();
|
|
119
|
+
if (vscodeDir) {
|
|
120
|
+
const f = join(vscodeDir, "mcp.json");
|
|
121
|
+
out.push({ tool: "vs code", state: existsSync(f) && /"pantheon"/.test(readFileSync(f, "utf8")) ? "wired" : "not wired" });
|
|
122
|
+
}
|
|
112
123
|
return out;
|
|
113
124
|
}
|
|
114
125
|
export function logFileFor(kind, proc) {
|
package/dist/measure.js
CHANGED
|
@@ -3,7 +3,9 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { resolve, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { claudeGrantedEffective, cursorGrantedEffective, claudeAllow, cursorAllow, isServerWide } from "./grants.js";
|
|
6
|
-
import { codexApprovalState } from "./install.js";
|
|
6
|
+
import { codexApprovalState, copilotMcpConfigFile } from "./install.js";
|
|
7
|
+
import { existsSync as fileExists, readFileSync } from "node:fs";
|
|
8
|
+
import { dirname as dirOf } from "node:path";
|
|
7
9
|
import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
|
|
8
10
|
const ROOT = process.env.PANTHEON_MCP_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
11
|
export const HOST_STARTUP_ALLOWANCE_MS = 10_000;
|
|
@@ -157,12 +159,24 @@ export function toolsReport(registered, seen, approval, opts = {}) {
|
|
|
157
159
|
problems.push("Codex's per-tool overrides do not match your choice");
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
|
-
const
|
|
162
|
+
const copilot = seen.copilot === undefined || seen.copilot === null ? null
|
|
163
|
+
: seen.copilot ? "Copilot wired (it asks once per tool per folder and remembers)" : "Copilot has no pantheon entry";
|
|
164
|
+
const detail = `${head}; ${claude.text}, ${cursor.text}, ${codex}${copilot ? `, ${copilot}` : ""}`;
|
|
161
165
|
if (!problems.length)
|
|
162
166
|
return { level: "pass", detail };
|
|
163
167
|
return { level: "warn", detail, fix: `${problems.join("; ")} — pantheon install --user rewrites every grant in your own settings to match this build and your approval choice; a rule in this repo's .claude/settings*.json or .cursor/cli.json is yours to edit by hand` };
|
|
164
168
|
}
|
|
165
169
|
export function grantsSeen(projectDir = null) {
|
|
166
|
-
return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState() };
|
|
170
|
+
return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState(), copilot: copilotWired() };
|
|
171
|
+
}
|
|
172
|
+
export function copilotWired(file = copilotMcpConfigFile()) {
|
|
173
|
+
if (!fileExists(dirOf(file)))
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
return fileExists(file) && /"pantheon"/.test(readFileSync(file, "utf8"));
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
167
181
|
}
|
|
168
182
|
//# sourceMappingURL=measure.js.map
|
package/dist/notice.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { decisionPrompt, decisionTitle } from "./decision-ui.js";
|
|
2
|
-
export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor", grok: "Grok" };
|
|
2
|
+
export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor", copilot: "Copilot", grok: "Grok" };
|
|
3
3
|
export const APP_TOOLS = [
|
|
4
4
|
{ id: "cursor-app", label: "Cursor app", kind: "app", bundle: "com.todesktop.230313mzl4w4u92", deeplink: "cursor://anysphere.cursor-deeplink/prompt?text={prompt}" },
|
|
5
5
|
{ id: "claude-app", label: "Claude app · question copied", kind: "app", bundle: "com.anthropic.claudefordesktop" },
|
|
6
6
|
{ id: "chatgpt-app", label: "ChatGPT app · question copied", kind: "app", bundle: "com.openai.chat" },
|
|
7
7
|
{ id: "codex-app", label: "Codex app · question copied", kind: "app", bundle: "com.openai.codex" },
|
|
8
|
+
{ id: "vscode-app", label: "VS Code · question copied", kind: "app", bundle: "com.microsoft.VSCode" },
|
|
8
9
|
];
|
|
9
10
|
export const BUDGET = { who: 60, heading: 40, context: 70, body: 190 };
|
|
10
11
|
const oneLine = (s) => s.replace(/\s+/g, " ").trim();
|
|
@@ -64,6 +65,8 @@ const agentLabel = (agent) => {
|
|
|
64
65
|
return "Codex";
|
|
65
66
|
if (a.includes("cursor"))
|
|
66
67
|
return "Cursor";
|
|
68
|
+
if (a.includes("copilot"))
|
|
69
|
+
return "Copilot";
|
|
67
70
|
return null;
|
|
68
71
|
};
|
|
69
72
|
function whoLine(from, agent, ts, now) {
|
package/dist/notifier.js
CHANGED
|
@@ -99,6 +99,7 @@ export function installedTools() {
|
|
|
99
99
|
"claude-app": ["/Applications/Claude.app", join(homedir(), "Applications", "Claude.app")],
|
|
100
100
|
"chatgpt-app": ["/Applications/ChatGPT.app", join(homedir(), "Applications", "ChatGPT.app")],
|
|
101
101
|
"codex-app": ["/Applications/Codex.app", join(homedir(), "Applications", "Codex.app")],
|
|
102
|
+
"vscode-app": ["/Applications/Visual Studio Code.app", join(homedir(), "Applications", "Visual Studio Code.app")],
|
|
102
103
|
};
|
|
103
104
|
for (const t of APP_TOOLS)
|
|
104
105
|
if ((APP_PATHS[t.id] ?? []).some((p) => existsSync(p)))
|
package/dist/notify.js
CHANGED
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { whichPantheon } from "./service.js";
|
|
5
5
|
import { detectForHooks, agentById } from "./agents.js";
|
|
6
|
-
import { codexHookState, cursorHookState, installCodexHooks, installCursorHooks, removeCodexHooks, removeCursorHooks, } from "./hooks-install.js";
|
|
6
|
+
import { codexHookState, cursorHookState, copilotHookState, installCodexHooks, installCursorHooks, installCopilotHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, } from "./hooks-install.js";
|
|
7
7
|
export const HOOK_MARKER = "pantheon hook";
|
|
8
8
|
export function settingsPath(scope, projectDir = process.cwd()) {
|
|
9
9
|
return scope === "user"
|
|
@@ -93,6 +93,9 @@ export function agentHookStates(projectDir = process.cwd(), only) {
|
|
|
93
93
|
const cursor = cursorHookState(scope, projectDir);
|
|
94
94
|
if (cursor.installed)
|
|
95
95
|
out.push(cursor);
|
|
96
|
+
const copilot = copilotHookState(scope, projectDir);
|
|
97
|
+
if (copilot.installed)
|
|
98
|
+
out.push(copilot);
|
|
96
99
|
}
|
|
97
100
|
const byAgent = new Map();
|
|
98
101
|
for (const s of out) {
|
|
@@ -116,6 +119,8 @@ export function repairHook(home) {
|
|
|
116
119
|
missing.push(id);
|
|
117
120
|
if (id === "cursor" && !cursorHookState(intent.scope, projectDir).installed)
|
|
118
121
|
missing.push(id);
|
|
122
|
+
if (id === "copilot" && !copilotHookState(intent.scope, projectDir).installed)
|
|
123
|
+
missing.push(id);
|
|
119
124
|
}
|
|
120
125
|
if (!missing.length)
|
|
121
126
|
return null;
|
|
@@ -165,7 +170,7 @@ export function installHooks(scope, projectDir = process.cwd(), only) {
|
|
|
165
170
|
? only.map((id) => agentById(id)).filter((b) => !!b)
|
|
166
171
|
: available;
|
|
167
172
|
if (!pick.length) {
|
|
168
|
-
throw new Error("no agent found — install claude, codex,
|
|
173
|
+
throw new Error("no agent found — install claude, codex, cursor-agent, or copilot, or open a project that already uses one");
|
|
169
174
|
}
|
|
170
175
|
const lines = [];
|
|
171
176
|
const agents = [];
|
|
@@ -183,6 +188,10 @@ export function installHooks(scope, projectDir = process.cwd(), only) {
|
|
|
183
188
|
lines.push(...installCursorHooks(bin, scope, projectDir));
|
|
184
189
|
agents.push("cursor");
|
|
185
190
|
}
|
|
191
|
+
else if (backend.id === "copilot") {
|
|
192
|
+
lines.push(...installCopilotHooks(bin, scope, projectDir));
|
|
193
|
+
agents.push("copilot");
|
|
194
|
+
}
|
|
186
195
|
}
|
|
187
196
|
lines.push(` effect when your agent tries to finish, it checks whether a peer is waiting`, ` on you — and keeps going instead of leaving them hanging.`, ` it also reports when a turn starts and ends, so a worker can tell`, ` an idle editor from one you are actually using.`);
|
|
188
197
|
return { lines, agents };
|
|
@@ -195,6 +204,7 @@ export function removeHook(projectDir = process.cwd()) {
|
|
|
195
204
|
out.push(...removeClaudeHook(projectDir));
|
|
196
205
|
out.push(...removeCodexHooks(projectDir));
|
|
197
206
|
out.push(...removeCursorHooks(projectDir));
|
|
207
|
+
out.push(...removeCopilotHooks(projectDir));
|
|
198
208
|
return out.length ? out : [" nothing installed"];
|
|
199
209
|
}
|
|
200
210
|
function removeClaudeHook(projectDir = process.cwd()) {
|
package/dist/persona.js
CHANGED
|
@@ -170,7 +170,7 @@ export const PERSONA_TEMPLATE = `---
|
|
|
170
170
|
# wake_by: alice, bob # only these peers' requests are handled by this persona
|
|
171
171
|
# kinds: ANSWER, ACCEPT # ANSWER (asks), ACCEPT (verdicts), REVISE (objections), CONTINUE (replies/deliveries)
|
|
172
172
|
# goals: launch-* # only goals whose id or title matches
|
|
173
|
-
# agent: auto # claude-code | codex | cursor | auto
|
|
173
|
+
# agent: auto # claude-code | codex | cursor | copilot | auto
|
|
174
174
|
---
|
|
175
175
|
Say how this worker should behave when it is woken while you are away — what it
|
|
176
176
|
should take on, what it should refuse, and where its judgment ends and yours
|
package/dist/profile.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CONFIG_DIR } from "./config.js";
|
|
4
|
-
export const AGENT_SURFACES = ["codex", "claude-code", "cursor"];
|
|
4
|
+
export const AGENT_SURFACES = ["codex", "claude-code", "cursor", "copilot"];
|
|
5
5
|
export const SURFACE_BUNDLE_IDS = {
|
|
6
6
|
codex: "com.openai.codex",
|
|
7
7
|
"claude-code": "com.anthropic.claudefordesktop",
|
|
8
8
|
cursor: "com.todesktop.230313mzl4w4u92",
|
|
9
|
+
copilot: "com.microsoft.VSCode",
|
|
9
10
|
};
|
|
10
11
|
export const profilePath = (home = CONFIG_DIR) => join(home, "profile.json");
|
|
11
12
|
export function isAgentSurface(value) {
|
|
@@ -14,6 +15,8 @@ export function isAgentSurface(value) {
|
|
|
14
15
|
export function surfaceFromAgent(agent) {
|
|
15
16
|
if (agent && /cursor/i.test(agent))
|
|
16
17
|
return "cursor";
|
|
18
|
+
if (agent && /copilot/i.test(agent))
|
|
19
|
+
return "copilot";
|
|
17
20
|
if (agent && /claude/i.test(agent))
|
|
18
21
|
return "claude-code";
|
|
19
22
|
return "codex";
|
package/dist/run-failure.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
export const AUTH_RE = /not (?:logged|signed) in|not authenticated|no (?:github )?(?:authentication|credentials?|token) (?:found|available)|login required|please (?:log|sign) in|run .*login|invalid api key|unauthori[sz]ed|authentication (?:failed|required)|\b401\b|expired (?:token|session)/i;
|
|
3
|
+
export const SIGN_IN_FIX = {
|
|
4
|
+
"claude-code": "run `claude`, then /login",
|
|
5
|
+
codex: "run `codex login`",
|
|
6
|
+
cursor: "run `cursor-agent login`",
|
|
7
|
+
copilot: "run `copilot login` (or `gh auth login` — copilot uses gh's sign-in too)",
|
|
8
|
+
};
|
|
2
9
|
const sh = (dir, ...args) => execFileSync("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 }).trim();
|
|
3
10
|
export const gitProbe = {
|
|
4
11
|
isRepo: (dir) => { try {
|
|
@@ -42,6 +49,14 @@ catch {
|
|
|
42
49
|
return fallback;
|
|
43
50
|
} };
|
|
44
51
|
export function diagnoseRun(f, probe = gitProbe) {
|
|
52
|
+
if (!f.exited && AUTH_RE.test(f.detail)) {
|
|
53
|
+
const fix = SIGN_IN_FIX[f.agent ?? ""] ?? "sign in to the agent CLI on this machine";
|
|
54
|
+
return {
|
|
55
|
+
cause: "not_signed_in",
|
|
56
|
+
says: `The agent could not start because nobody is signed in to it${f.agent ? ` (${f.agent})` : ""} — ${exitLine(f.detail)}.`,
|
|
57
|
+
fix: `Sign in once and the next run works: ${fix}. Until then every woken run on this machine fails the same way.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
45
60
|
if (!f.exited) {
|
|
46
61
|
return {
|
|
47
62
|
cause: "crashed",
|
package/dist/setup-guide.js
CHANGED
|
@@ -195,7 +195,7 @@ export async function runGuide(io, ctx, deps = {}, preset = {}) {
|
|
|
195
195
|
: "only people you approve by hand (pantheon admit <name> --may-wake)"}`);
|
|
196
196
|
io.say(` • Alerts: ${shortAlert(a.notifications).toLowerCase()}`);
|
|
197
197
|
io.say(` • Approval: ${a.approval === "approve"
|
|
198
|
-
? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor and Codex)"
|
|
198
|
+
? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor and Codex; Copilot asks once per tool per folder)"
|
|
199
199
|
: "your agent asks you before each send, goal and claim; reading and waiting never prompt"}`);
|
|
200
200
|
io.say(` • Decisions: ${a.decides.length ? `you personally decide ${a.decides.join(", ")}` : "none reserved for you — your agent may decide; add an area when one comes up"}`);
|
|
201
201
|
if (a.wake !== "no" && ctx.project)
|
package/dist/tray.js
CHANGED
|
@@ -144,7 +144,7 @@ async function trayStatusOne(cfg) {
|
|
|
144
144
|
];
|
|
145
145
|
const profile = loadProfile(cfg);
|
|
146
146
|
const surface = profile.agent.primary_surface;
|
|
147
|
-
const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : "Codex";
|
|
147
|
+
const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : surface === "copilot" ? "Copilot" : "Codex";
|
|
148
148
|
const rawWorkerState = workerState();
|
|
149
149
|
return {
|
|
150
150
|
configured: true,
|
package/dist/worker.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { PantheonClient } from "./client.js";
|
|
7
|
-
import { headlessSpawn, materializeSpawnFiles,
|
|
7
|
+
import { headlessSpawn, materializeSpawnFiles, chooseAgent, describeChoice } from "./agents.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
9
|
import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolicy, useGrant, } from "./worker-policy.js";
|
|
10
10
|
import { agentHookStates, repairHook } from "./notify.js";
|
|
@@ -191,6 +191,7 @@ export function answerWindow(askSeq, hint, fullScanDue) {
|
|
|
191
191
|
export class Worker {
|
|
192
192
|
cfg;
|
|
193
193
|
opts;
|
|
194
|
+
lastAgentSaid = "";
|
|
194
195
|
running = false;
|
|
195
196
|
done = [];
|
|
196
197
|
stopped = false;
|
|
@@ -329,8 +330,7 @@ export class Worker {
|
|
|
329
330
|
const agentLabel = this.opts.agentCmd
|
|
330
331
|
? this.opts.agentCmd
|
|
331
332
|
: (() => { try {
|
|
332
|
-
|
|
333
|
-
return `${r.backend.label} (${r.binary})`;
|
|
333
|
+
return describeChoice(chooseAgent(this.opts.agent, { prefer: loadProfile(this.cfg).agent.primary_surface }));
|
|
334
334
|
}
|
|
335
335
|
catch {
|
|
336
336
|
return "auto";
|
|
@@ -580,10 +580,12 @@ export class Worker {
|
|
|
580
580
|
const startedAt = Date.now();
|
|
581
581
|
writeFileSync(join(worktree, ".pantheon-task.md"), brief);
|
|
582
582
|
let command;
|
|
583
|
+
let agentId;
|
|
583
584
|
let args;
|
|
584
585
|
let env = { ...process.env };
|
|
585
586
|
try {
|
|
586
587
|
const runHome = this.runHome();
|
|
588
|
+
agentId = this.opts.agentCmd ? "claude-code" : undefined;
|
|
587
589
|
if (this.opts.agentCmd) {
|
|
588
590
|
const plan = headlessSpawn({
|
|
589
591
|
backend: { id: "claude-code", label: "Claude Code", binaries: ["claude"] },
|
|
@@ -594,8 +596,14 @@ export class Worker {
|
|
|
594
596
|
({ command, args, env } = { command: plan.command, args: plan.args, env: { ...env, ...plan.env } });
|
|
595
597
|
}
|
|
596
598
|
else {
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
+
const choice = chooseAgent(policy.agent ?? this.opts.agent, { prefer: loadProfile(this.cfg).agent.primary_surface });
|
|
600
|
+
agentId = choice.backend.id;
|
|
601
|
+
const said = describeChoice(choice);
|
|
602
|
+
if (said !== this.lastAgentSaid) {
|
|
603
|
+
this.opts.log(` agent ${said}`);
|
|
604
|
+
this.lastAgentSaid = said;
|
|
605
|
+
}
|
|
606
|
+
const plan = headlessSpawn({ backend: choice.backend, binary: choice.binary, prompt: brief, worktree, cfg: this.cfg, branch, allowBash, runHome });
|
|
599
607
|
materializeSpawnFiles(worktree, plan);
|
|
600
608
|
({ command, args, env } = { command: plan.command, args: plan.args, env: { ...env, ...plan.env } });
|
|
601
609
|
this.opts.log(` spawn ${command} ${args.slice(0, 3).join(" ")}…`);
|
|
@@ -629,7 +637,7 @@ export class Worker {
|
|
|
629
637
|
}, this.opts.home);
|
|
630
638
|
const sent = client.mineSince(startedAt, o.goal);
|
|
631
639
|
const quiet = ok && !requireReply && sent.length === 0;
|
|
632
|
-
const diagnosis = ok ? undefined : diagnoseRun({ exited, replied, allowBash, branch, project: this.opts.project, worktree, detail, runLog }, this.opts.probe ?? gitProbe);
|
|
640
|
+
const diagnosis = ok ? undefined : diagnoseRun({ exited, replied, allowBash, branch, project: this.opts.project, worktree, detail, runLog, ...(agentId ? { agent: agentId } : {}) }, this.opts.probe ?? gitProbe);
|
|
633
641
|
if (this.opts.notify && !quiet && (!ok || loadProfile(this.cfg).notifications?.worker_done)) {
|
|
634
642
|
try {
|
|
635
643
|
const reply = sent[sent.length - 1] ?? null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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": {
|