@join-pantheon/cli 0.25.2 → 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/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 mergeMcpJson(file, entry) {
27
- let doc = {};
28
- if (existsSync(file)) {
29
- try {
30
- doc = JSON.parse(readFileSync(file, "utf8"));
31
- }
32
- catch {
33
- throw new Error(`${file} is not valid JSON — fix or move it, then re-run`);
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 detail = `${head}; ${claude.text}, ${cursor.text}, ${codex}`;
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) {
@@ -95,7 +98,7 @@ export function composeDecision(o, act, goal, peer, ctx) {
95
98
  body: fit(body, BUDGET.body),
96
99
  ttl: 0,
97
100
  sound: true,
98
- primary: primaryFor(ctx.tools, ctx.preferred, decisionPrompt(o.id)),
101
+ primary: primaryFor(ctx.tools, ctx.preferred, decisionPrompt(o.id, decisionTitle(o.body))),
99
102
  details: { decision: o.id, ...(act ? { act: act.id } : {}) },
100
103
  };
101
104
  }
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, or cursor-agent, or open a project that already uses one");
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";
@@ -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",
@@ -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
@@ -86,6 +86,7 @@ export async function trayStatus(cfg = load(), f = loadAll()) {
86
86
  return {
87
87
  ...first,
88
88
  space: ok.map((p) => p.space).join(" · "),
89
+ spaces: ok.map((p) => p.space),
89
90
  relay_ok: ok.every((p) => p.relay_ok),
90
91
  decisions_ok: ok.every((p) => p.decisions_ok),
91
92
  unavailable_spaces: ok.flatMap((p) => p.unavailable_spaces ?? []),
@@ -117,11 +118,11 @@ async function trayStatusOne(cfg) {
117
118
  const mineHuman = all.filter((o) => o.owner === `human:${cfg.member}`);
118
119
  const decisionsOk = ok;
119
120
  const decisions = mineHuman
120
- .map((o) => ({ id: o.id, body: decisionTitle(o.body), detail: o.body, prompt: decisionPrompt(o.id) }));
121
+ .map((o) => ({ id: o.id, body: decisionTitle(o.body), detail: o.body, prompt: decisionPrompt(o.id, decisionTitle(o.body)) }));
121
122
  const senderOf = (o) => stripCeremony(o.body).from;
122
123
  const row = (o, kind, who, prompt) => ({
123
124
  id: o.id, kind, title: decisionTitle(o.body), headline: decisionTitle(o.body, 200), detail: o.body, prompt, who,
124
- when: o.created_at ?? 0,
125
+ when: o.created_at ?? 0, space: cfg.space,
125
126
  ...(o.goal ? { goal: o.goal } : {}),
126
127
  ...(o.ref ? { ref: o.ref } : {}),
127
128
  });
@@ -130,7 +131,7 @@ async function trayStatusOne(cfg) {
130
131
  const items = [
131
132
  ...mineHuman.map((o) => {
132
133
  const from = senderOf(o);
133
- return row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id));
134
+ return row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body)));
134
135
  }),
135
136
  ...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
136
137
  ...waiting.map((o) => {
@@ -143,12 +144,13 @@ async function trayStatusOne(cfg) {
143
144
  ];
144
145
  const profile = loadProfile(cfg);
145
146
  const surface = profile.agent.primary_surface;
146
- const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : "Codex";
147
+ const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : surface === "copilot" ? "Copilot" : "Codex";
147
148
  const rawWorkerState = workerState();
148
149
  return {
149
150
  configured: true,
150
151
  version: VERSION,
151
152
  space: cfg.space,
153
+ spaces: [cfg.space],
152
154
  member: cfg.member,
153
155
  relay: cfg.relay,
154
156
  relay_ok: !!space,
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, resolveAgent } from "./agents.js";
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
- const r = resolveAgent(this.opts.agent);
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 { backend, binary } = resolveAgent(policy.agent ?? this.opts.agent);
598
- const plan = headlessSpawn({ backend, binary, prompt: brief, worktree, cfg: this.cfg, branch, allowBash, runHome });
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;