@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/README.md +43 -5
- package/dist/agents.js +155 -14
- package/dist/cli.js +32 -34
- package/dist/config.js +26 -1
- package/dist/decision-ui.js +13 -3
- 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 +5 -2
- 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 +6 -4
- package/dist/worker.js +14 -6
- package/helper/macos/menubar.swift +387 -27
- package/package.json +1 -1
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) {
|
|
@@ -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,
|
|
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
|
@@ -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,
|
|
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;
|