@join-pantheon/cli 0.26.5 → 0.28.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 CHANGED
@@ -404,7 +404,7 @@ 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|copilot|auto`.
407
+ The worker runs whichever agent CLI this machine has — `--agent claude-code|codex|cursor|copilot|kiro|auto`.
408
408
  With `auto` (the default) the order is: the agent a persona pins, then the one you chose at setup,
409
409
  then PATH order — and a candidate that is not signed in is skipped and named in the run log, so a
410
410
  run never fails on an expired session when another agent could have answered. If nothing is
@@ -418,6 +418,7 @@ actually works, with the same "no shell unless you said so" rule spelled in its
418
418
  | codex | `--sandbox read-only` (codex has no edits-without-shell mode) | `--sandbox workspace-write` |
419
419
  | cursor | proposes edits, never applies (`-p` without `--force`) | `--force` |
420
420
  | copilot | `--allow-tool=pantheon-worker --allow-tool=write` (edits without shell, Copilot's `write` kind) | `--allow-all-tools` |
421
+ | kiro | an agent whose tools are pantheon's plus `read`, `write`, `grep`, `glob` — all pre-approved | the same, plus `shell` |
421
422
 
422
423
  Claude Code, Cursor and Codex are verified with **real live woken runs** — an idle machine answering a
423
424
  peer's ASK end to end. Codex quirks the tooling handles for
@@ -458,6 +459,58 @@ named as that, with `copilot login` as the fix.
458
459
  also restrict MCP servers to an approved registry; the server is called `pantheon` and its only job
459
460
  is moving messages between your teammates' agents — nothing here reads code or runs commands.
460
461
 
462
+ ### Kiro
463
+
464
+ Kiro is one backend covering the **Kiro CLI** (`kiro-cli`) and the **Kiro IDE**, both of which read
465
+ `~/.kiro/settings/mcp.json`; `pantheon install --user` writes pantheon there. Everything below about
466
+ the CLI was checked live against kiro-cli 2.23.1.
467
+
468
+ **Sessions report presence on their own.** The entry names its agent (`PANTHEON_AGENT=kiro`), so a Kiro
469
+ session is labelled Kiro to your teammates and reports busy and idle through its own MCP server. Without
470
+ that label it would take the machine's main agent as its own, and on a Claude Code machine report nothing.
471
+ There is deliberately no repo-level Kiro file: Kiro uses a workspace-level server of the same name whole,
472
+ so a committed copy would replace each teammate's own entry.
473
+
474
+ **Approval.** The entry's `autoApprove` list carries your approval choice, and the Kiro IDE honours it.
475
+ The Kiro CLI ignores it — its default agent asks before each Pantheon call, and has no setting that
476
+ changes that.
477
+
478
+ **The `pantheon` agent, for the CLI.** In kiro-cli the only hooks that fire are the ones inside an agent
479
+ file; neither of its engines reads the standalone `.kiro/hooks/` files, and the built-in default agent
480
+ cannot carry any. So `pantheon hook install` adds an agent called `pantheon`: Kiro's default — the same
481
+ tools (`tools: ["*"]`) and your own MCP servers (`includeMcpJson: true`) — plus Pantheon's tools
482
+ pre-approved per your choice, a `userPromptSubmit` hook that reports a turn as started and prints
483
+ nothing, and a `stop` hook that holds the turn open while a teammate waits. It is opt-in and nothing
484
+ makes it your default:
485
+
486
+ ```
487
+ kiro-cli chat --agent pantheon # this session
488
+ kiro-cli agent set-default pantheon # every session
489
+ ```
490
+
491
+ Checked live: with a teammate's question waiting and a prompt that said only "reply HELLO", the agent
492
+ replied, the stop hook held the turn, and it read, claimed and answered the question through Pantheon
493
+ without one approval prompt. An agent called `pantheon` that you wrote yourself is never overwritten.
494
+
495
+ **Woken runs.** A woken Kiro run is its own agent, `pantheon-worker`, in a `KIRO_HOME` of its own outside
496
+ the checkout. That matters: Kiro starts every MCP server in its config whatever the agent says, so your own
497
+ pantheon entry would otherwise join every run as a second server reporting you present. The run's home
498
+ holds only its agent and your Kiro settings; your sign-in, which lives in Kiro's data store, still applies.
499
+ The agent's `tools` and `allowedTools` are one list — Pantheon's server and `read`, `write`, `grep`,
500
+ `glob`, with `shell` only under `--allow-bash` — so nothing it can reach needs approval, and no trust flag
501
+ is passed. A repo's own committed Kiro MCP servers still start, as the repo's tooling; their tools are not
502
+ in the run's list, and one that fails does not stop the run.
503
+
504
+ **Sign in once with `kiro-cli login`, or set `KIRO_API_KEY`.** Signed out, Kiro does not fail: it opens a
505
+ browser to log in and waits. So a woken run is never started with Kiro signed out, even when a persona pins
506
+ Kiro or it is the only agent on the machine; the run is refused with the reason, and `pantheon doctor` says
507
+ so first. Pantheon reads the sign-in with `kiro-cli whoami --format json`, and only once Kiro has run on the
508
+ machine, because on a fresh install that one call creates `~/.kiro` and Kiro's data store.
509
+
510
+ Not supported: Kiro Crew, which is itself an always-on agent runner and would be a second answerer beside
511
+ Pantheon's worker, and Kiro Mobile, which has nowhere to load an MCP server. The Kiro IDE's own hooks are
512
+ not wired, because they could not be checked here; IDE sessions get the MCP entry and presence.
513
+
461
514
  ### Personas — telling the worker who to be
462
515
 
463
516
  A worker with no instructions escalates too much and trusts too much. A **persona** is one
package/dist/agents.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
2
3
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
4
  import { homedir, platform } from "node:os";
4
5
  import { join, dirname, isAbsolute } from "node:path";
5
- import { launcher } from "./install.js";
6
+ import { launcher, kiroDir } from "./install.js";
6
7
  import { agentSpawn, which } from "./which.js";
7
8
  import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
8
9
  export function launchArgs(agent, prompt, name) {
@@ -10,6 +11,8 @@ export function launchArgs(agent, prompt, name) {
10
11
  return ["--name", name, prompt];
11
12
  if (agent === "copilot")
12
13
  return ["-n", name, "-i", prompt];
14
+ if (agent === "kiro")
15
+ return ["chat", prompt];
13
16
  return [prompt];
14
17
  }
15
18
  export const AGENTS = [
@@ -17,7 +20,11 @@ export const AGENTS = [
17
20
  { id: "codex", label: "Codex", binaries: ["codex"] },
18
21
  { id: "cursor", label: "Cursor", binaries: ["cursor-agent", "agent"] },
19
22
  { id: "copilot", label: "Copilot", binaries: ["copilot"] },
23
+ { id: "kiro", label: "Kiro", binaries: ["kiro-cli"] },
20
24
  ];
25
+ export function kiroHome(home = homedir(), env = process.env) {
26
+ return kiroDir(home, env);
27
+ }
21
28
  export function copilotHome(home = homedir(), env = process.env) {
22
29
  return env.COPILOT_HOME || join(home, ".copilot");
23
30
  }
@@ -57,6 +64,11 @@ export function detectForHooks(projectDir = process.cwd()) {
57
64
  out.push(backend);
58
65
  continue;
59
66
  }
67
+ if (backend.id === "kiro") {
68
+ if (existsSync(kiroHome()) || existsSync(join(projectDir, ".kiro")))
69
+ out.push(backend);
70
+ continue;
71
+ }
60
72
  const userDir = backend.id === "claude-code" ? join(homedir(), ".claude")
61
73
  : backend.id === "codex" ? join(homedir(), ".codex")
62
74
  : join(homedir(), ".cursor");
@@ -107,7 +119,28 @@ export const SIGN_IN_CHECK = {
107
119
  codex: { args: ["login", "status"], fix: "run `codex login`" },
108
120
  cursor: { args: ["status"], fix: "run `cursor-agent login`" },
109
121
  copilot: { args: [], fix: "run `copilot login` (or `gh auth login` — copilot uses gh's sign-in too)" },
122
+ kiro: { args: ["whoami", "--format", "json"], fix: "run `kiro-cli login` (or set KIRO_API_KEY)" },
110
123
  };
124
+ export const NEEDS_SIGN_IN_BEFORE_SPAWN = new Set(["kiro"]);
125
+ export function kiroSignedIn(binary, run = runStatus, env = process.env, home = homedir(), os = platform()) {
126
+ if (env.KIRO_API_KEY)
127
+ return { ok: true, detail: "signed in (KIRO_API_KEY in the environment)" };
128
+ if (!kiroHasRunHere(home, env, os))
129
+ return { ok: false, detail: "never signed in on this machine" };
130
+ const r = run(binary, SIGN_IN_CHECK.kiro.args);
131
+ if (r.status === null)
132
+ return { ok: false, detail: `could not tell (${(r.out.split("\n")[0] ?? "").slice(0, 60) || "no answer"}), so it is not run unattended` };
133
+ const line = r.out.split("\n").map((l) => l.trim()).reverse().find((l) => l.startsWith("{"));
134
+ try {
135
+ const j = JSON.parse(line ?? "");
136
+ if (typeof j.accountType === "string" && j.accountType)
137
+ return { ok: true, detail: "signed in" };
138
+ if ("account" in j)
139
+ return j.account ? { ok: true, detail: "signed in" } : { ok: false, detail: "not signed in" };
140
+ }
141
+ catch { }
142
+ return { ok: false, detail: "could not tell (kiro-cli whoami answered in a shape this build does not know), so it is not run unattended" };
143
+ }
111
144
  export const COPILOT_TOKEN_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
112
145
  export function copilotSignedIn(run = runStatus, env = process.env, gh = () => !!cliOnPath("gh")) {
113
146
  if (COPILOT_TOKEN_VARS.some((k) => env[k]))
@@ -132,10 +165,12 @@ const runStatus = (binary, args) => {
132
165
  return { status: err.status ?? null, out: `${err.stdout ?? ""}${err.stderr ?? ""}`.trim() || (err.message ?? "") };
133
166
  }
134
167
  };
135
- export const AGENT_HOME_DIR = { "claude-code": ".claude", codex: ".codex", cursor: ".cursor", copilot: ".copilot" };
168
+ export const AGENT_HOME_DIR = { "claude-code": ".claude", codex: ".codex", cursor: ".cursor", copilot: ".copilot", kiro: ".kiro" };
136
169
  export function signedIn(backend, binary, run = runStatus, home = homedir()) {
137
170
  if (backend.id === "copilot")
138
171
  return copilotSignedIn(run);
172
+ if (backend.id === "kiro")
173
+ return kiroSignedIn(binary, run, process.env, home);
139
174
  if (!existsSync(join(home, AGENT_HOME_DIR[backend.id])))
140
175
  return { ok: false, detail: "never signed in on this machine" };
141
176
  const check = SIGN_IN_CHECK[backend.id];
@@ -159,14 +194,14 @@ export function chooseAgent(preferred, opts = {}) {
159
194
  if (preferred && preferred !== "auto") {
160
195
  const backend = agentById(preferred);
161
196
  if (!backend)
162
- throw new Error(`unknown agent ${preferred} — use claude-code, codex, cursor, copilot, or auto`);
197
+ throw new Error(`unknown agent ${preferred} — use claude-code, codex, cursor, copilot, kiro, or auto`);
163
198
  const hit = available.find((a) => a.backend.id === backend.id);
164
199
  if (!hit)
165
200
  throw new Error(`${backend.label} is not on PATH (looked for ${backend.binaries.join(", ")})`);
166
201
  return { backend, binary: hit.binary, signIn: state(backend, hit.binary), because: "pinned", skipped: [] };
167
202
  }
168
203
  if (!available.length)
169
- throw new Error("no agent CLI on PATH — install claude, codex, cursor-agent, or copilot");
204
+ throw new Error("no agent CLI on PATH — install claude, codex, cursor-agent, copilot, or kiro-cli");
170
205
  const order = [];
171
206
  const wanted = opts.prefer ? agentById(opts.prefer) : undefined;
172
207
  const chosen = wanted ? available.find((a) => a.backend.id === wanted.id) : undefined;
@@ -182,9 +217,14 @@ export function chooseAgent(preferred, opts = {}) {
182
217
  return { backend: c.backend, binary: c.binary, ...(s ? { signIn: s } : {}), because: c.because, skipped };
183
218
  skipped.push({ backend: c.backend, why: `${s.detail} — ${SIGN_IN_CHECK[c.backend.id].fix}` });
184
219
  }
185
- const first = order[0];
220
+ const first = order.find((o) => !NEEDS_SIGN_IN_BEFORE_SPAWN.has(o.backend.id)) ?? order[0];
186
221
  return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
187
222
  }
223
+ export function unattendedRefusal(c) {
224
+ if (!NEEDS_SIGN_IN_BEFORE_SPAWN.has(c.backend.id) || c.signIn?.ok)
225
+ return null;
226
+ return `${c.backend.label} is not signed in (${c.signIn?.detail ?? "not checked"}), and signed out it opens a browser to log in, so it is not started while you are away — ${SIGN_IN_CHECK[c.backend.id].fix}`;
227
+ }
188
228
  export function describeChoice(c) {
189
229
  const head = `${c.backend.label}${c.signIn ? ` — ${c.signIn.detail}` : ""} (${c.because})`;
190
230
  return c.skipped.length ? `${head}; skipped ${c.skipped.map((s) => `${s.backend.label} — ${s.why}`).join("; ")}` : head;
@@ -283,6 +323,30 @@ export function headlessSpawn(opts) {
283
323
  ...(opts.runHome && existsSync(settings) ? { copyFiles: [{ from: settings, to: join(home, "settings.json") }] } : {}),
284
324
  };
285
325
  }
326
+ if (opts.backend.id === "kiro") {
327
+ const home = opts.runHome ? join(opts.runHome, "kiro") : join(opts.worktree, ".pantheon", "kiro-home");
328
+ const tools = [...KIRO_WORKER_TOOLS, ...(opts.allowBash ? ["shell"] : []), `@${KIRO_WORKER_SERVER}`];
329
+ const agentName = `${KIRO_WORKER_AGENT}-${randomBytes(8).toString("hex")}`;
330
+ const settings = join(kiroHome(), "settings", "cli.json");
331
+ return {
332
+ command: opts.binary,
333
+ args: ["chat", "--no-interactive", "--agent", agentName, "--require-mcp-startup", "--wrap", "never", opts.prompt],
334
+ env: { ...procEnv, KIRO_HOME: home, KIRO_NO_AUTO_UPDATE: "1" },
335
+ writeFiles: [{
336
+ path: join(home, "agents", `${KIRO_WORKER_AGENT}.json`),
337
+ contents: JSON.stringify({
338
+ name: agentName,
339
+ description: "A Pantheon run: answers a teammate's request on an isolated branch, while its human is away",
340
+ tools,
341
+ allowedTools: tools,
342
+ includeMcpJson: false,
343
+ mcpServers: { [KIRO_WORKER_SERVER]: { command: l.command, args: l.args, env: fileEnv } },
344
+ resources: ["file://.kiro/steering/**/*.md"],
345
+ }, null, 2),
346
+ }],
347
+ ...(opts.runHome && existsSync(settings) ? { copyFiles: [{ from: settings, to: join(home, "settings", "cli.json") }] } : {}),
348
+ };
349
+ }
286
350
  return {
287
351
  command: opts.binary,
288
352
  args: [
@@ -310,6 +374,20 @@ export function headlessSpawn(opts) {
310
374
  };
311
375
  }
312
376
  export const COPILOT_WORKER_SERVER = "pantheon-worker";
377
+ export function kiroDataDir(home = homedir(), env = process.env, os = platform()) {
378
+ if (env.KIRO_DATA_DIR)
379
+ return env.KIRO_DATA_DIR;
380
+ if (os === "darwin")
381
+ return join(home, "Library", "Application Support", "kiro-cli");
382
+ return null;
383
+ }
384
+ export function kiroHasRunHere(home = homedir(), env = process.env, os = platform()) {
385
+ const data = kiroDataDir(home, env, os);
386
+ return data ? existsSync(join(data, "data.sqlite3")) : existsSync(kiroDir(home, env));
387
+ }
388
+ export const KIRO_WORKER_AGENT = "pantheon-worker";
389
+ export const KIRO_WORKER_SERVER = "pantheon";
390
+ export const KIRO_WORKER_TOOLS = ["read", "write", "grep", "glob"];
313
391
  function readCopilotConfig() {
314
392
  try {
315
393
  const doc = JSON.parse(readFileSync(join(copilotHome(), "config.json"), "utf8"));
package/dist/cli.js CHANGED
@@ -29,7 +29,8 @@ import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, d
29
29
  import { assess } from "./untrusted.js";
30
30
  import { report as spendReport, recentRuns } from "./spend.js";
31
31
  import { renderBrief, renderTeam } from "./brief.js";
32
- import { resolveDataDir } from "./config.js";
32
+ import { resolveDataDir, saveAll } from "./config.js";
33
+ import { currentPlan, installNetwork, redactProxy, normalizeProxy, usedSystemFallback } from "./network.js";
33
34
  import { createInterface } from "node:readline";
34
35
  import { homedir } from "node:os";
35
36
  import { removeHook, decideStop, writeIntent, clearIntent, readIntent, installHooks, agentHookStates } from "./notify.js";
@@ -442,6 +443,8 @@ function printReady(tools) {
442
443
  oneTime.set("cursor", `cursor enable the "pantheon" MCP server if it asks`);
443
444
  if (/codex/i.test(t.tool))
444
445
  oneTime.set("codex", `codex run /hooks inside codex once to approve the wake-up hook`);
446
+ if (/kiro/i.test(t.tool))
447
+ oneTime.set("kiro", `kiro run kiro-cli chat --agent pantheon (or kiro-cli agent set-default pantheon) to keep working while a teammate waits; stay signed in — a woken run never starts Kiro signed out, since Kiro would open a browser`);
445
448
  if (/copilot/i.test(t.tool))
446
449
  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`);
447
450
  }
@@ -501,7 +504,7 @@ ${B("control")}
501
504
  pantheon admit <who> let someone in (--may-wake to allow unattended work here)
502
505
  pantheon admit <who> --revoke remove them
503
506
  pantheon worker stay answerable while you are away
504
- (--agent claude-code|codex|cursor|copilot|auto, --persona <name>)
507
+ (--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
505
508
  pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
506
509
  pantheon persona how a woken worker should behave: list / init <name> / show <name>
507
510
  pantheon notifier alert you when a task requires your decision
@@ -514,11 +517,12 @@ ${B("control")}
514
517
  pantheon approvals decisions waiting on you; show or answer one
515
518
  pantheon stop [relay|worker|notifier|menubar|all] stop it
516
519
  pantheon hook install your agent keeps working when a peer is waiting on it
517
- (auto-detects claude / codex / cursor / copilot on this machine)
520
+ (auto-detects claude / codex / cursor / copilot / kiro on this machine)
518
521
  pantheon spend woken runs on this machine, and what pantheon cannot see
519
522
  pantheon leave remove what pantheon put on this machine
520
523
  pantheon panic cut off all unattended work on this machine, now
521
524
  pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
525
+ pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
522
526
  pantheon setup three questions — your name, your role, may a woken run execute commands —
523
527
  then one screen of defaults to keep or change: who may wake this machine,
524
528
  alerts, what you personally decide, playbook, base branch. Re-run any time.
@@ -547,7 +551,26 @@ ${B("less common")}
547
551
  pantheon version the pantheon version this machine is running
548
552
  `;
549
553
  const QUIET = new Set(["mcp", "hook", "pending", "version", "tray-status"]);
554
+ function rememberWorkingProxy() {
555
+ try {
556
+ const plan = currentPlan();
557
+ const via = plan.https ?? plan.http;
558
+ if (!via || (plan.source !== "env" && !(plan.fallbackOnly && usedSystemFallback())))
559
+ return;
560
+ const all = loadAll();
561
+ if (!all || all.network?.source === "pantheon network" || all.network?.proxy === via)
562
+ return;
563
+ const custom = plan.noProxy.filter((h) => !["localhost", "127.0.0.1", "::1", "[::1]"].includes(h));
564
+ saveAll({ ...all, network: { proxy: via, ...(custom.length ? { no_proxy: custom.join(",") } : {}), source: plan.source, saved_at: Date.now() } });
565
+ console.error(D(` saved the proxy ${redactProxy(via)} for this machine's other Pantheon processes (worker, editors) — change it with pantheon network`));
566
+ }
567
+ catch { }
568
+ }
550
569
  async function main() {
570
+ try {
571
+ await installNetwork();
572
+ }
573
+ catch { }
551
574
  if (!QUIET.has(cmd)) {
552
575
  try {
553
576
  console.error(D(updateBanner(VERSION)));
@@ -563,6 +586,35 @@ async function main() {
563
586
  return interactiveOnboard();
564
587
  }
565
588
  switch (cmd) {
589
+ case "network": {
590
+ const all = loadAll() ?? die("not configured — sign up or join a team first");
591
+ const set = flag("proxy");
592
+ if (set !== undefined) {
593
+ const p = normalizeProxy(set) ?? die(`"${set}" is not a proxy address — use http://host:port (add user:password@ if it needs a login)`);
594
+ saveAll({ ...all, network: { proxy: p, ...(flag("no-proxy") ? { no_proxy: flag("no-proxy") } : {}), source: "pantheon network", saved_at: Date.now() } });
595
+ console.error(`${B("saved")} every Pantheon process on this machine now reaches the relay through ${redactProxy(p)}`);
596
+ return;
597
+ }
598
+ if (argv.includes("--direct")) {
599
+ saveAll({ ...all, network: { proxy: "", source: "pantheon network --direct", saved_at: Date.now() } });
600
+ console.error(`${B("saved")} direct — set PANTHEON_PROXY=none in a shell to do the same for one command`);
601
+ return;
602
+ }
603
+ if (argv.includes("--clear")) {
604
+ const { network: _drop, ...rest } = all;
605
+ saveAll(rest);
606
+ console.error(`${B("cleared")} — Pantheon follows HTTPS_PROXY / NO_PROXY and the system settings again`);
607
+ return;
608
+ }
609
+ const plan = currentPlan();
610
+ const via = plan.https ?? plan.http;
611
+ console.log(`${B("network")} ${via ? `through ${redactProxy(via)}` : "direct"}${plan.source !== "none" ? D(` from ${plan.source}${plan.fallbackOnly ? ", used only if a direct connection fails" : ""}`) : ""}`);
612
+ if (via)
613
+ console.log(D(` not for: ${plan.noProxy.join(", ")}`));
614
+ if (plan.pac)
615
+ console.log(D(` this machine uses an auto-config script (${plan.pac}), which Pantheon cannot read — if the relay is unreachable, save the proxy it points to: pantheon network --proxy http://host:port`));
616
+ return;
617
+ }
566
618
  case "start": {
567
619
  const port = Number(flag("port", "8787"));
568
620
  const setupFlags = parseSetupFlags(argv);
@@ -593,6 +645,7 @@ async function main() {
593
645
  for (const w of r.warnings)
594
646
  console.error(`\x1b[33m ! ${w}\x1b[0m`);
595
647
  printGranted(r.granted);
648
+ rememberWorkingProxy();
596
649
  if (!r.claimed) {
597
650
  console.error(`\n\x1b[31m No team was created.\x1b[0m ${D("Fix the warning above, then run `pantheon start` again.")}`);
598
651
  if (r.relay)
@@ -792,6 +845,7 @@ async function main() {
792
845
  for (const w of r.warnings)
793
846
  console.error(`\x1b[33m ! ${w}\x1b[0m`);
794
847
  printGranted(r.granted);
848
+ rememberWorkingProxy();
795
849
  printReady(r.tools);
796
850
  {
797
851
  const joined = load();
@@ -1332,13 +1386,13 @@ async function main() {
1332
1386
  const sub = argv[1] ?? "status";
1333
1387
  const hookCfg = resolveForHook();
1334
1388
  const sessionOf = (raw) => {
1389
+ let fromJson;
1335
1390
  try {
1336
1391
  const j = JSON.parse(raw);
1337
- return j.session_id ?? j.sessionId ?? j.conversation_id ?? "unknown";
1338
- }
1339
- catch {
1340
- return "unknown";
1392
+ fromJson = j.session_id ?? j.sessionId ?? j.conversation_id;
1341
1393
  }
1394
+ catch { }
1395
+ return fromJson ?? process.env.KIRO_SESSION_ID ?? "unknown";
1342
1396
  };
1343
1397
  const reportIdle = async (session, seen) => {
1344
1398
  if (!hookCfg)
@@ -1353,6 +1407,22 @@ async function main() {
1353
1407
  }
1354
1408
  return false;
1355
1409
  };
1410
+ if (sub === "kiro-prompt") {
1411
+ let raw = "";
1412
+ try {
1413
+ for await (const chunk of process.stdin)
1414
+ raw += chunk;
1415
+ }
1416
+ catch { }
1417
+ try {
1418
+ if (hookCfg) {
1419
+ const { reportActivity } = await import("./pending.js");
1420
+ await reportActivity({ cfg: hookCfg, session: `hook:${sessionOf(raw)}`, state: "busy" });
1421
+ }
1422
+ }
1423
+ catch { }
1424
+ process.exit(0);
1425
+ }
1356
1426
  if (sub === "prompt") {
1357
1427
  let raw = "";
1358
1428
  try {
@@ -1462,7 +1532,7 @@ async function main() {
1462
1532
  process.exit(0);
1463
1533
  }
1464
1534
  }
1465
- if (sub === "codex-stop" || sub === "copilot-stop") {
1535
+ if (sub === "codex-stop" || sub === "copilot-stop" || sub === "kiro-stop") {
1466
1536
  let raw = "";
1467
1537
  try {
1468
1538
  for await (const chunk of process.stdin)
@@ -1571,7 +1641,7 @@ async function main() {
1571
1641
  }
1572
1642
  const states = agentHookStates();
1573
1643
  if (!states.length) {
1574
- console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor / copilot on PATH)`);
1644
+ console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor / copilot / kiro on PATH)`);
1575
1645
  return;
1576
1646
  }
1577
1647
  for (const s of states) {
@@ -1654,7 +1724,7 @@ async function main() {
1654
1724
  else {
1655
1725
  console.error(` config: none to remove`);
1656
1726
  }
1657
- console.error(`\n ${D("Not touched: the MCP entries in Claude Code and Cursor (pantheon install wrote them; remove them there),")}`);
1727
+ console.error(`\n ${D("Not touched: the MCP entries in Claude Code, Cursor, Copilot, VS Code and Kiro (pantheon install wrote them; remove them there),")}`);
1658
1728
  console.error(` ${D("the relay's ledger on disk, and anything you pushed to git. Codex's entry and approval line were removed above.")}`);
1659
1729
  if (cfg) {
1660
1730
  console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
@@ -2021,7 +2091,7 @@ async function main() {
2021
2091
  const publicChange = name !== undefined || title !== undefined || responsibilities !== undefined ||
2022
2092
  humanRequired !== undefined || agentCanDecide !== undefined || capabilities !== undefined || keywords !== undefined;
2023
2093
  if (!surface && !publicChange) {
2024
- 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']");
2094
+ die("usage: pantheon profile set [--surface codex|claude-code|cursor|copilot|kiro] [--name name] [--title title] [--responsibilities a,b] [--human-required a,b] [--agent-can-decide a,b] [--keywords 'domain=word|word;domain=word']");
2025
2095
  }
2026
2096
  if (flag("space")) {
2027
2097
  const pf = loadAll() ?? die("not configured");
@@ -2462,7 +2532,7 @@ async function main() {
2462
2532
  if (hud.prompt) {
2463
2533
  const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
2464
2534
  if (!agent)
2465
- die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
2535
+ die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2466
2536
  console.error(D(`opening ${agent.id}…`));
2467
2537
  const { spawnSync } = await import("node:child_process");
2468
2538
  {
@@ -2494,7 +2564,7 @@ async function main() {
2494
2564
  }
2495
2565
  const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
2496
2566
  if (!agent)
2497
- die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
2567
+ die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2498
2568
  console.error(D(`opening ${agent.id} with the question…`));
2499
2569
  const { spawnSync } = await import("node:child_process");
2500
2570
  {
package/dist/client.js CHANGED
@@ -2,6 +2,7 @@ import WebSocket from "ws";
2
2
  import { signChallenge } from "./identity.js";
3
3
  import { newId } from "./protocol.js";
4
4
  import { VERSION } from "./version.js";
5
+ import { describeNetError, explainNetError, trySystemFallback, wsOptions } from "./network.js";
5
6
  export class PantheonClient {
6
7
  cfg;
7
8
  onEvent;
@@ -74,7 +75,28 @@ export class PantheonClient {
74
75
  connect() {
75
76
  if (!this.everConnected)
76
77
  this.closed = false;
77
- return this.attempt(this.localOverride ?? this.cfg.relay).catch(async (e) => {
78
+ const first = this.localOverride ?? this.cfg.relay;
79
+ return this.attempt(first).catch(async (e) => {
80
+ if (!this.localOverride && trySystemFallback(first, e)) {
81
+ this.closed = false;
82
+ try {
83
+ return await this.attempt(first);
84
+ }
85
+ catch (e2) {
86
+ e = e2;
87
+ }
88
+ }
89
+ return this.afterFailure(e);
90
+ }).catch((e) => {
91
+ if (!explainNetError(e))
92
+ throw e;
93
+ const out = new Error(describeNetError(e));
94
+ out.code = e.code;
95
+ throw out;
96
+ });
97
+ }
98
+ async afterFailure(e) {
99
+ {
78
100
  if (this.localOverride)
79
101
  throw e;
80
102
  const transport = /ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|handshake has timed out|socket hang up/i
@@ -88,13 +110,13 @@ export class PantheonClient {
88
110
  this.localOverride = local;
89
111
  this.usingLoopback = true;
90
112
  return this.attempt(local);
91
- });
113
+ }
92
114
  }
93
115
  attempt(address) {
94
116
  return new Promise((resolve, reject) => {
95
117
  let settled = false;
96
118
  this.ws?.terminate();
97
- const ws = new WebSocket(address, { handshakeTimeout: 6000 });
119
+ const ws = new WebSocket(address, { handshakeTimeout: 6000, ...wsOptions(address) });
98
120
  this.ws = ws;
99
121
  ws.on("open", () => { });
100
122
  ws.on("message", (raw) => {
package/dist/doctor.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { kiroDefaultIsPantheon, readKiroAgent, kiroAgentFile } from "./kiro-files.js";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { instructionBudget } from "./mcp-instructions.js";
3
4
  import { describeWindows, scanMcpWindows } from "./mcp-windows.js";
@@ -14,11 +15,12 @@ import { approvalSummary, chosenApproval, onPath } from "./install.js";
14
15
  import { hookState, readIntent, agentHookStates } from "./notify.js";
15
16
  import { loadPersonas } from "./persona.js";
16
17
  import { workerProc, menubarProc, notifierProc } from "./machine.js";
17
- import { chooseAgent, describeChoice } from "./agents.js";
18
+ import { chooseAgent, describeChoice, unattendedRefusal } from "./agents.js";
18
19
  import { onPathNamed } from "./which.js";
19
20
  import { VERSION } from "./version.js";
20
21
  import { serviceInstalled, servicesUnavailable } from "./service.js";
21
22
  import { hostEnv, platformName } from "./host-env.js";
23
+ import { currentPlan, explainNetError, proxyFor, redactProxy } from "./network.js";
22
24
  const HOSTED_RELAY = "relay.joinpantheon.network";
23
25
  const git = (args, cwd) => {
24
26
  try {
@@ -156,10 +158,10 @@ export async function doctor(projectDir) {
156
158
  if (branch)
157
159
  add("pass", "git branch", branch);
158
160
  }
159
- const agents = ["claude", "codex", "cursor-agent", "copilot"].filter((a) => onPathNamed(a));
161
+ const agents = ["claude", "codex", "cursor-agent", "copilot", "kiro-cli"].filter((a) => onPathNamed(a));
160
162
  agents.length
161
163
  ? add("pass", "agent cli", agents.join(", "), undefined, agents.join(", "))
162
- : 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");
164
+ : add("warn", "agent cli", "found none of claude / codex / cursor-agent / copilot / kiro-cli on PATH", "fine if you only drive agents through a GUI (Conductor, Cursor, VS Code, the Kiro IDE), but `pantheon worker` needs a CLI to spawn", "none of claude / codex / cursor-agent / copilot / kiro-cli on PATH");
163
165
  if (agents.length) {
164
166
  try {
165
167
  const choice = chooseAgent("auto", { prefer: prof.agent.primary_surface });
@@ -167,7 +169,10 @@ export async function doctor(projectDir) {
167
169
  const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
168
170
  const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
169
171
  const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${choice.skipped.length} skipped (not signed in)` : ""}`;
170
- if (bad)
172
+ const refused = unattendedRefusal(choice);
173
+ if (refused)
174
+ add("warn", "worker agent", line, `woken runs are refused here, not tried: ${refused}`, shareLine);
175
+ else if (bad)
171
176
  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);
172
177
  else if (fellBack)
173
178
  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);
@@ -175,7 +180,13 @@ export async function doctor(projectDir) {
175
180
  add("pass", "worker agent", line, undefined, shareLine);
176
181
  }
177
182
  catch (e) {
178
- add("warn", "worker agent", e.message, "pantheon setup --agent <claude-code|codex|cursor|copilot> names one that is installed", "could not be chosen");
183
+ add("warn", "worker agent", e.message, "pantheon setup --agent <claude-code|codex|cursor|copilot|kiro> names one that is installed", "could not be chosen");
184
+ }
185
+ }
186
+ {
187
+ const here = readKiroAgent(kiroAgentFile(join(projectDir, ".kiro")));
188
+ if (here && !here.ours) {
189
+ add("warn", "kiro agent", "this repo defines its own agent called pantheon — Kiro uses it instead of yours in this repo", "workspace agents win over your own in Kiro; read .kiro/agents/pantheon.json before running kiro-cli chat --agent pantheon here", "this repo defines its own pantheon agent");
179
190
  }
180
191
  }
181
192
  const budget = instructionBudget(cfg);
@@ -212,6 +223,20 @@ export async function doctor(projectDir) {
212
223
  else {
213
224
  add("pass", "transport", url.protocol === "wss:" ? "wss:// (TLS)" : `ws:// on a private address (${url.host})`, undefined, url.protocol === "wss:" ? "wss:// (TLS)" : "ws:// on a private address");
214
225
  }
226
+ const plan = currentPlan();
227
+ const via = proxyFor(cfg.relay, plan);
228
+ if (via) {
229
+ add("pass", "network", `through the proxy ${redactProxy(via)} (from ${plan.source})`, undefined, `through a proxy (${plan.source})`);
230
+ }
231
+ else if (plan.fallbackOnly && (plan.https || plan.http)) {
232
+ add("pass", "network", `direct; the ${plan.source === "macos" ? "macOS" : "Windows"} proxy ${redactProxy(plan.https ?? plan.http)} is used only if a direct connection fails`, undefined, `direct, system proxy as fallback`);
233
+ }
234
+ else if (plan.pac) {
235
+ add("warn", "network", `direct, but this machine uses a proxy auto-config script, which Pantheon cannot read`, "if the relay turns out unreachable below, save the proxy that script points to: pantheon network --proxy http://host:port", "direct; PAC script present");
236
+ }
237
+ else {
238
+ add("pass", "network", "direct — no proxy set", undefined, "direct");
239
+ }
215
240
  const health = `${cfg.relay.replace(/^ws/, "http")}/health`;
216
241
  const where = hosted ? "hosted" : "self-hosted";
217
242
  try {
@@ -235,6 +260,11 @@ export async function doctor(projectDir) {
235
260
  }
236
261
  catch (e) {
237
262
  const msg = e.message;
263
+ const nx = explainNetError(e);
264
+ if (nx) {
265
+ add("fail", "relay", `cannot reach ${health} — ${nx.why}`, nx.fix, `unreachable (${where}): ${nx.share}`);
266
+ return out;
267
+ }
238
268
  add("fail", "relay", `cannot reach ${health} — ${msg}`, "start it with `pantheon relay`, or check the tunnel/tailnet. Your teammate must be able to reach the SAME address.", `unreachable (${where}): ${/abort|timeout/i.test(msg) ? "timed out" : /ECONNREFUSED/.test(msg) ? "connection refused" : /ENOTFOUND|EAI_AGAIN/.test(msg) ? "name did not resolve" : "fetch failed"}`);
239
269
  return out;
240
270
  }
@@ -242,6 +272,12 @@ export async function doctor(projectDir) {
242
272
  let joined = false;
243
273
  await client.connect().then(() => { joined = true; }).catch(() => { });
244
274
  if (!joined) {
275
+ const nx = client.pending ? null : explainNetError(new Error(client.lastError ?? ""));
276
+ if (nx) {
277
+ add("fail", "live link", `the health check got through but the live connection did not — ${nx.why}`, nx.fix, `live connection failed: ${nx.share}`);
278
+ client.close();
279
+ return out;
280
+ }
245
281
  client.pending
246
282
  ? add("fail", "membership", "registered but not admitted", `ask an admitted member to run: pantheon admit ${cfg.member} --may-wake (confirm fingerprint ${fingerprint(cfg.publicKey)} with them by voice first)`, "registered, not admitted")
247
283
  : add("fail", "membership", `refused: ${client.lastError ?? "unknown"}`, "if this is KEY_MISMATCH, someone else already registered your member name — pick another with --me, or have them revoke it", `refused: ${String(client.lastError ?? "unknown").split(/[\s:]/)[0]}`);
@@ -302,7 +338,7 @@ export async function doctor(projectDir) {
302
338
  }
303
339
  }
304
340
  if (!hookStates.length && !hook.installed) {
305
- 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");
341
+ 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 / kiro on PATH)", "not installed");
306
342
  }
307
343
  else if (!reportsActivity) {
308
344
  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`);
@@ -316,6 +352,11 @@ export async function doctor(projectDir) {
316
352
  if (hookStates.some((s) => s.agent === "codex")) {
317
353
  add("warn", "codex hooks", "installed — but codex skips new or changed hooks until they are approved", "run /hooks inside codex once to approve them (needs codex ≥ 0.115; sessions still report presence over MCP either way)", "installed, not yet approved inside codex");
318
354
  }
355
+ if (hookStates.some((s) => s.agent === "kiro")) {
356
+ kiroDefaultIsPantheon()
357
+ ? add("pass", "kiro hooks", "in the pantheon agent, which is your Kiro default — every kiro-cli chat keeps working while a teammate waits", undefined, "in the pantheon agent, the default")
358
+ : add("warn", "kiro hooks", "in the pantheon agent — used only when you run kiro-cli chat --agent pantheon", "kiro-cli agent set-default pantheon makes it every session; without it, Kiro sessions still report presence but end their turn with a teammate waiting", "in the pantheon agent, not the default");
359
+ }
319
360
  const personas = loadPersonas(projectDir);
320
361
  if (personas.length) {
321
362
  const broken = personas.filter((p) => p.warnings.length);