@join-pantheon/cli 0.25.3 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/which.js ADDED
@@ -0,0 +1,73 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, existsSync, statSync } from "node:fs";
3
+ import { win32 } from "node:path";
4
+ import { platform } from "node:os";
5
+ export const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.PS1";
6
+ const isWindows = (os = platform()) => os === "win32";
7
+ function runnable(file) {
8
+ try {
9
+ if (!existsSync(file) || !statSync(file).isFile())
10
+ return false;
11
+ if (isWindows())
12
+ return true;
13
+ accessSync(file, constants.X_OK);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export function whichWindows(name, env = process.env, exists = runnable) {
21
+ if (!name)
22
+ return null;
23
+ const exts = (env.PATHEXT || DEFAULT_PATHEXT).split(";").map((e) => e.trim()).filter(Boolean);
24
+ const ext = win32.extname(name);
25
+ const hasExt = !!ext && exts.some((e) => e.toLowerCase() === ext.toLowerCase());
26
+ const candidates = (base) => hasExt ? [base] : exts.flatMap((e) => (e === e.toLowerCase() ? [base + e] : [base + e.toLowerCase(), base + e]));
27
+ if (win32.isAbsolute(name)) {
28
+ for (const c of candidates(name))
29
+ if (exists(c))
30
+ return c;
31
+ return null;
32
+ }
33
+ for (const dir of (env.PATH || env.Path || "").split(";")) {
34
+ const d = dir.replace(/^"|"$/g, "").trim();
35
+ if (!d)
36
+ continue;
37
+ for (const c of candidates(win32.join(d, name)))
38
+ if (exists(c))
39
+ return c;
40
+ }
41
+ return null;
42
+ }
43
+ export function which(name, os = platform(), env = process.env) {
44
+ if (isWindows(os))
45
+ return whichWindows(name, env);
46
+ try {
47
+ const out = execFileSync("sh", ["-lc", `command -v ${name}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
48
+ if (!out)
49
+ return null;
50
+ return out.startsWith("/") && !runnable(out) ? null : out;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ export const onPathNamed = (name, os = platform(), env = process.env) => which(name, os, env) !== null;
57
+ export function spawnShape(binary, os = platform()) {
58
+ if (!isWindows(os))
59
+ return { command: binary, shell: false };
60
+ return { command: binary, shell: /\.(cmd|bat)$/i.test(binary) };
61
+ }
62
+ const cmdQuote = (a) => (/[\s"&|<>^()]/.test(a) || a === "" ? `"${a.replace(/"/g, '\\"')}"` : a);
63
+ export function agentSpawn(binary, args, os = platform()) {
64
+ if (!isWindows(os) || !/\.(cmd|bat)$/i.test(binary))
65
+ return { command: binary, args: [...args], options: {} };
66
+ const line = [binary, ...args].map(cmdQuote).join(" ");
67
+ return {
68
+ command: process.env.ComSpec || "cmd.exe",
69
+ args: ["/d", "/s", "/c", `"${line}"`],
70
+ options: { windowsVerbatimArguments: true },
71
+ };
72
+ }
73
+ //# sourceMappingURL=which.js.map
package/dist/worker.js CHANGED
@@ -4,7 +4,8 @@ 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
+ import { agentSpawn } from "./which.js";
8
9
  import { VERSION } from "./version.js";
9
10
  import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolicy, useGrant, } from "./worker-policy.js";
10
11
  import { agentHookStates, repairHook } from "./notify.js";
@@ -191,6 +192,7 @@ export function answerWindow(askSeq, hint, fullScanDue) {
191
192
  export class Worker {
192
193
  cfg;
193
194
  opts;
195
+ lastAgentSaid = "";
194
196
  running = false;
195
197
  done = [];
196
198
  stopped = false;
@@ -329,8 +331,7 @@ export class Worker {
329
331
  const agentLabel = this.opts.agentCmd
330
332
  ? this.opts.agentCmd
331
333
  : (() => { try {
332
- const r = resolveAgent(this.opts.agent);
333
- return `${r.backend.label} (${r.binary})`;
334
+ return describeChoice(chooseAgent(this.opts.agent, { prefer: loadProfile(this.cfg).agent.primary_surface }));
334
335
  }
335
336
  catch {
336
337
  return "auto";
@@ -580,10 +581,12 @@ export class Worker {
580
581
  const startedAt = Date.now();
581
582
  writeFileSync(join(worktree, ".pantheon-task.md"), brief);
582
583
  let command;
584
+ let agentId;
583
585
  let args;
584
586
  let env = { ...process.env };
585
587
  try {
586
588
  const runHome = this.runHome();
589
+ agentId = this.opts.agentCmd ? "claude-code" : undefined;
587
590
  if (this.opts.agentCmd) {
588
591
  const plan = headlessSpawn({
589
592
  backend: { id: "claude-code", label: "Claude Code", binaries: ["claude"] },
@@ -594,8 +597,14 @@ export class Worker {
594
597
  ({ command, args, env } = { command: plan.command, args: plan.args, env: { ...env, ...plan.env } });
595
598
  }
596
599
  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 });
600
+ const choice = chooseAgent(policy.agent ?? this.opts.agent, { prefer: loadProfile(this.cfg).agent.primary_surface });
601
+ agentId = choice.backend.id;
602
+ const said = describeChoice(choice);
603
+ if (said !== this.lastAgentSaid) {
604
+ this.opts.log(` agent ${said}`);
605
+ this.lastAgentSaid = said;
606
+ }
607
+ const plan = headlessSpawn({ backend: choice.backend, binary: choice.binary, prompt: brief, worktree, cfg: this.cfg, branch, allowBash, runHome });
599
608
  materializeSpawnFiles(worktree, plan);
600
609
  ({ command, args, env } = { command: plan.command, args: plan.args, env: { ...env, ...plan.env } });
601
610
  this.opts.log(` spawn ${command} ${args.slice(0, 3).join(" ")}…`);
@@ -608,7 +617,8 @@ export class Worker {
608
617
  return { obligation: o, branch, worktree, ok: false, detail };
609
618
  }
610
619
  const detail = await new Promise((res) => {
611
- const child = procSpawn(command, args, { cwd: worktree, stdio: ["ignore", "pipe", "pipe"], env });
620
+ const sp = agentSpawn(command, args);
621
+ const child = procSpawn(sp.command, sp.args, { cwd: worktree, stdio: ["ignore", "pipe", "pipe"], env, ...sp.options });
612
622
  let out = "";
613
623
  child.stdout.on("data", (b) => { out += b.toString(); });
614
624
  child.stderr.on("data", (b) => { out += b.toString(); });
@@ -629,7 +639,7 @@ export class Worker {
629
639
  }, this.opts.home);
630
640
  const sent = client.mineSince(startedAt, o.goal);
631
641
  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);
642
+ const diagnosis = ok ? undefined : diagnoseRun({ exited, replied, allowBash, branch, project: this.opts.project, worktree, detail, runLog, ...(agentId ? { agent: agentId } : {}) }, this.opts.probe ?? gitProbe);
633
643
  if (this.opts.notify && !quiet && (!ok || loadProfile(this.cfg).notifications?.worker_done)) {
634
644
  try {
635
645
  const reply = sent[sent.length - 1] ?? null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.25.3",
4
- "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
3
+ "version": "0.26.1",
4
+ "description": "Let your coding agent collaborate directly with your teammates' agents \u2014 ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pantheon": "bin/pantheon.js"
@@ -10,13 +10,15 @@
10
10
  "node": ">=22"
11
11
  },
12
12
  "scripts": {
13
- "build": "rm -rf dist && tsc -p .",
13
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p .",
14
14
  "dev": "tsc -p . --watch",
15
15
  "relay": "node dist/cli.js relay",
16
16
  "test": "PANTHEON_HOME=\"$(mktemp -d)\" HOME=\"$(mktemp -d)\" node --test --test-force-exit dist/test/*.test.js",
17
17
  "prepare": "npm run build",
18
18
  "prepublishOnly": "npm run build && npm test",
19
- "start": "node dist/cli.js relay"
19
+ "start": "node dist/cli.js relay",
20
+ "test:ci": "node scripts/test.mjs",
21
+ "smoke": "node scripts/smoke.mjs"
20
22
  },
21
23
  "dependencies": {
22
24
  "@modelcontextprotocol/sdk": "^1.0.0",