@join-pantheon/cli 0.26.5 → 0.27.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
@@ -442,6 +442,8 @@ function printReady(tools) {
442
442
  oneTime.set("cursor", `cursor enable the "pantheon" MCP server if it asks`);
443
443
  if (/codex/i.test(t.tool))
444
444
  oneTime.set("codex", `codex run /hooks inside codex once to approve the wake-up hook`);
445
+ if (/kiro/i.test(t.tool))
446
+ 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
447
  if (/copilot/i.test(t.tool))
446
448
  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
449
  }
@@ -501,7 +503,7 @@ ${B("control")}
501
503
  pantheon admit <who> let someone in (--may-wake to allow unattended work here)
502
504
  pantheon admit <who> --revoke remove them
503
505
  pantheon worker stay answerable while you are away
504
- (--agent claude-code|codex|cursor|copilot|auto, --persona <name>)
506
+ (--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
505
507
  pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
506
508
  pantheon persona how a woken worker should behave: list / init <name> / show <name>
507
509
  pantheon notifier alert you when a task requires your decision
@@ -514,7 +516,7 @@ ${B("control")}
514
516
  pantheon approvals decisions waiting on you; show or answer one
515
517
  pantheon stop [relay|worker|notifier|menubar|all] stop it
516
518
  pantheon hook install your agent keeps working when a peer is waiting on it
517
- (auto-detects claude / codex / cursor / copilot on this machine)
519
+ (auto-detects claude / codex / cursor / copilot / kiro on this machine)
518
520
  pantheon spend woken runs on this machine, and what pantheon cannot see
519
521
  pantheon leave remove what pantheon put on this machine
520
522
  pantheon panic cut off all unattended work on this machine, now
@@ -1332,13 +1334,13 @@ async function main() {
1332
1334
  const sub = argv[1] ?? "status";
1333
1335
  const hookCfg = resolveForHook();
1334
1336
  const sessionOf = (raw) => {
1337
+ let fromJson;
1335
1338
  try {
1336
1339
  const j = JSON.parse(raw);
1337
- return j.session_id ?? j.sessionId ?? j.conversation_id ?? "unknown";
1338
- }
1339
- catch {
1340
- return "unknown";
1340
+ fromJson = j.session_id ?? j.sessionId ?? j.conversation_id;
1341
1341
  }
1342
+ catch { }
1343
+ return fromJson ?? process.env.KIRO_SESSION_ID ?? "unknown";
1342
1344
  };
1343
1345
  const reportIdle = async (session, seen) => {
1344
1346
  if (!hookCfg)
@@ -1353,6 +1355,22 @@ async function main() {
1353
1355
  }
1354
1356
  return false;
1355
1357
  };
1358
+ if (sub === "kiro-prompt") {
1359
+ let raw = "";
1360
+ try {
1361
+ for await (const chunk of process.stdin)
1362
+ raw += chunk;
1363
+ }
1364
+ catch { }
1365
+ try {
1366
+ if (hookCfg) {
1367
+ const { reportActivity } = await import("./pending.js");
1368
+ await reportActivity({ cfg: hookCfg, session: `hook:${sessionOf(raw)}`, state: "busy" });
1369
+ }
1370
+ }
1371
+ catch { }
1372
+ process.exit(0);
1373
+ }
1356
1374
  if (sub === "prompt") {
1357
1375
  let raw = "";
1358
1376
  try {
@@ -1462,7 +1480,7 @@ async function main() {
1462
1480
  process.exit(0);
1463
1481
  }
1464
1482
  }
1465
- if (sub === "codex-stop" || sub === "copilot-stop") {
1483
+ if (sub === "codex-stop" || sub === "copilot-stop" || sub === "kiro-stop") {
1466
1484
  let raw = "";
1467
1485
  try {
1468
1486
  for await (const chunk of process.stdin)
@@ -1571,7 +1589,7 @@ async function main() {
1571
1589
  }
1572
1590
  const states = agentHookStates();
1573
1591
  if (!states.length) {
1574
- console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor / copilot on PATH)`);
1592
+ console.log(`${D("not installed")}\n Install with: ${C("pantheon hook install")} (auto-detects claude / codex / cursor / copilot / kiro on PATH)`);
1575
1593
  return;
1576
1594
  }
1577
1595
  for (const s of states) {
@@ -1654,7 +1672,7 @@ async function main() {
1654
1672
  else {
1655
1673
  console.error(` config: none to remove`);
1656
1674
  }
1657
- console.error(`\n ${D("Not touched: the MCP entries in Claude Code and Cursor (pantheon install wrote them; remove them there),")}`);
1675
+ 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
1676
  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
1677
  if (cfg) {
1660
1678
  console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
@@ -2021,7 +2039,7 @@ async function main() {
2021
2039
  const publicChange = name !== undefined || title !== undefined || responsibilities !== undefined ||
2022
2040
  humanRequired !== undefined || agentCanDecide !== undefined || capabilities !== undefined || keywords !== undefined;
2023
2041
  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']");
2042
+ 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
2043
  }
2026
2044
  if (flag("space")) {
2027
2045
  const pf = loadAll() ?? die("not configured");
@@ -2462,7 +2480,7 @@ async function main() {
2462
2480
  if (hud.prompt) {
2463
2481
  const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
2464
2482
  if (!agent)
2465
- die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
2483
+ die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2466
2484
  console.error(D(`opening ${agent.id}…`));
2467
2485
  const { spawnSync } = await import("node:child_process");
2468
2486
  {
@@ -2494,7 +2512,7 @@ async function main() {
2494
2512
  }
2495
2513
  const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
2496
2514
  if (!agent)
2497
- die("no agent CLI found on this machine (claude, cursor-agent, codex, or copilot)");
2515
+ die("no agent CLI found on this machine (claude, cursor-agent, codex, copilot, or kiro-cli)");
2498
2516
  console.error(D(`opening ${agent.id} with the question…`));
2499
2517
  const { spawnSync } = await import("node:child_process");
2500
2518
  {
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,7 +15,7 @@ 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";
@@ -156,10 +157,10 @@ export async function doctor(projectDir) {
156
157
  if (branch)
157
158
  add("pass", "git branch", branch);
158
159
  }
159
- const agents = ["claude", "codex", "cursor-agent", "copilot"].filter((a) => onPathNamed(a));
160
+ const agents = ["claude", "codex", "cursor-agent", "copilot", "kiro-cli"].filter((a) => onPathNamed(a));
160
161
  agents.length
161
162
  ? 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");
163
+ : 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
164
  if (agents.length) {
164
165
  try {
165
166
  const choice = chooseAgent("auto", { prefer: prof.agent.primary_surface });
@@ -167,7 +168,10 @@ export async function doctor(projectDir) {
167
168
  const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
168
169
  const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
169
170
  const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${choice.skipped.length} skipped (not signed in)` : ""}`;
170
- if (bad)
171
+ const refused = unattendedRefusal(choice);
172
+ if (refused)
173
+ add("warn", "worker agent", line, `woken runs are refused here, not tried: ${refused}`, shareLine);
174
+ else if (bad)
171
175
  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
176
  else if (fellBack)
173
177
  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 +179,13 @@ export async function doctor(projectDir) {
175
179
  add("pass", "worker agent", line, undefined, shareLine);
176
180
  }
177
181
  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");
182
+ add("warn", "worker agent", e.message, "pantheon setup --agent <claude-code|codex|cursor|copilot|kiro> names one that is installed", "could not be chosen");
183
+ }
184
+ }
185
+ {
186
+ const here = readKiroAgent(kiroAgentFile(join(projectDir, ".kiro")));
187
+ if (here && !here.ours) {
188
+ 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
189
  }
180
190
  }
181
191
  const budget = instructionBudget(cfg);
@@ -302,7 +312,7 @@ export async function doctor(projectDir) {
302
312
  }
303
313
  }
304
314
  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");
315
+ 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
316
  }
307
317
  else if (!reportsActivity) {
308
318
  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 +326,11 @@ export async function doctor(projectDir) {
316
326
  if (hookStates.some((s) => s.agent === "codex")) {
317
327
  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
328
  }
329
+ if (hookStates.some((s) => s.agent === "kiro")) {
330
+ kiroDefaultIsPantheon()
331
+ ? 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")
332
+ : 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");
333
+ }
319
334
  const personas = loadPersonas(projectDir);
320
335
  if (personas.length) {
321
336
  const broken = personas.filter((p) => p.warnings.length);
@@ -2,7 +2,8 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { MAX_CONSECUTIVE_BLOCKS, isOurHookCommand } from "./notify.js";
5
- import { copilotHome } from "./agents.js";
5
+ import { copilotHome, kiroHome } from "./agents.js";
6
+ import { KIRO_AGENT_NAME, kiroAgentFile, kiroAgentDoc, readKiroAgent, writeKiroAgent, kiroDefaultIsPantheon } from "./kiro-files.js";
6
7
  function ours(cmd) {
7
8
  return isOurHookCommand(cmd);
8
9
  }
@@ -247,4 +248,48 @@ export function removeCopilotHooks(projectDir = process.cwd()) {
247
248
  }
248
249
  return out;
249
250
  }
251
+ export { KIRO_AGENT_NAME };
252
+ export function kiroAgentPath(scope, projectDir = process.cwd()) {
253
+ return kiroAgentFile(scope === "user" ? kiroHome() : join(projectDir, ".kiro"));
254
+ }
255
+ export function kiroHookState(scope, projectDir = process.cwd()) {
256
+ const file = kiroAgentPath(scope, projectDir);
257
+ const a = readKiroAgent(file);
258
+ return a?.ours
259
+ ? { agent: "kiro", label: "Kiro", installed: true, scope, file, events: a.events }
260
+ : { agent: "kiro", label: "Kiro", installed: false };
261
+ }
262
+ export function installKiroHooks(bin, scope, projectDir = process.cwd(), allowed = []) {
263
+ const file = kiroAgentPath(scope, projectDir);
264
+ const a = readKiroAgent(file);
265
+ if (a && !a.ours)
266
+ return [` skipped ${file} is an agent called "${KIRO_AGENT_NAME}" that pantheon did not write — left as it is`];
267
+ writeKiroAgent(file, kiroAgentDoc(bin, allowed));
268
+ return [
269
+ ` ${a ? "updated" : "installed"} ${file} (an agent: Kiro's default, plus pantheon's hooks)`,
270
+ ` runs ${bin} hook kiro-stop`,
271
+ ` ${bin} hook kiro-prompt`,
272
+ kiroDefaultIsPantheon()
273
+ ? ` ${""}(it is your default agent, so every kiro-cli chat uses it)`
274
+ : ` \x1b[33mKiro uses these only in the pantheon agent:\x1b[0m kiro-cli chat --agent pantheon`,
275
+ ...(kiroDefaultIsPantheon() ? [] : [` ${""}(or always: kiro-cli agent set-default pantheon — pantheon never changes your default)`]),
276
+ ];
277
+ }
278
+ export function removeKiroHooks(projectDir = process.cwd()) {
279
+ const out = [];
280
+ for (const scope of ["project", "user"]) {
281
+ const file = kiroAgentPath(scope, projectDir);
282
+ if (!kiroHookState(scope, projectDir).installed)
283
+ continue;
284
+ try {
285
+ rmSync(file, { force: true });
286
+ out.push(` removed ${file}`);
287
+ }
288
+ catch { }
289
+ }
290
+ if (out.length && kiroDefaultIsPantheon()) {
291
+ out.push(" Kiro's default was the pantheon agent; it now falls back to kiro_default and warns each session — kiro-cli agent set-default kiro_default");
292
+ }
293
+ return out;
294
+ }
250
295
  //# sourceMappingURL=hooks-install.js.map
package/dist/install.js CHANGED
@@ -4,11 +4,12 @@ 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";
7
- import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
7
+ import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES, toolsAllowed } from "./tools.js";
8
8
  import { load } from "./config.js";
9
9
  import { loadProfile } from "./profile.js";
10
10
  import { agentSpawn, onPathNamed, which } from "./which.js";
11
11
  import { FEEDBACK_ADDRESS } from "./feedback.js";
12
+ import { kiroDir, kiroAgentFile, refreshKiroAgentAllowed } from "./kiro-files.js";
12
13
  const HERE = dirname(fileURLToPath(import.meta.url));
13
14
  export function onPath() {
14
15
  return onPathNamed("pantheon");
@@ -40,7 +41,7 @@ function mergeMcpJson(file, entry, shape = {}) {
40
41
  const existed = existsSync(file);
41
42
  doc.mcpServers ??= {};
42
43
  const before = JSON.stringify(doc.mcpServers["pantheon"] ?? null);
43
- doc.mcpServers["pantheon"] = { ...shape, ...entry, env: {} };
44
+ doc.mcpServers["pantheon"] = { ...shape, ...entry, env: shape.env ?? {} };
44
45
  if (before === JSON.stringify(doc.mcpServers["pantheon"]))
45
46
  return "unchanged";
46
47
  mkdirSync(dirname(file), { recursive: true });
@@ -61,6 +62,12 @@ export function mergeVsCodeMcpJson(file, entry) {
61
62
  return existed ? "updated" : "created";
62
63
  }
63
64
  export const copilotMcpConfigFile = (home = homedir(), env = process.env) => join(env.COPILOT_HOME || join(home, ".copilot"), "mcp-config.json");
65
+ export { kiroDir };
66
+ export const kiroMcpConfigFile = (home = homedir(), env = process.env) => join(kiroDir(home, env), "settings", "mcp.json");
67
+ export const kiroEntryShape = (approval) => ({
68
+ autoApprove: toolsAllowed(approval),
69
+ env: { PANTHEON_AGENT: "kiro" },
70
+ });
64
71
  export function vsCodeUserDir(home = homedir(), os = platform(), env = process.env) {
65
72
  const dir = os === "darwin" ? join(home, "Library", "Application Support", "Code", "User")
66
73
  : os === "win32" ? join(env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User")
@@ -131,6 +138,9 @@ export function install(projectDir, cfg, force, os = platform(), claudeLocal = c
131
138
  if (existsSync(p) || f === "AGENTS.md")
132
139
  out.push(` ${f.padEnd(25)} ${upsertBrief(p, cfg)} (the collaboration brief)`);
133
140
  }
141
+ if (existsSync(kiroDir()) || onPathNamed("kiro-cli")) {
142
+ out.push(` .kiro/settings/mcp.json ${"not used".padEnd(9)} Kiro reads ~/.kiro/settings/mcp.json, which pantheon join wrote — a repo copy would override each teammate's own (kiro)`);
143
+ }
134
144
  out.push("");
135
145
  out.push(" Copilot asks once to trust this folder before it reads the repo's MCP servers and hooks.");
136
146
  out.push(" Codex reads ~/.codex/config.toml, which `pantheon join` already wrote at user scope.");
@@ -251,6 +261,21 @@ function copilotUserScope(entry) {
251
261
  return { tool, where: file, status: `failed: ${e.message}` };
252
262
  }
253
263
  }
264
+ function kiroUserScope(entry, approval) {
265
+ const tool = "kiro";
266
+ const file = kiroMcpConfigFile();
267
+ if (!existsSync(kiroDir()) && !onPathNamed("kiro-cli"))
268
+ return { tool, where: "-", status: "skipped (kiro-cli not on PATH)" };
269
+ try {
270
+ const status = mergeMcpJson(file, entry, kiroEntryShape(approval));
271
+ const agent = refreshKiroAgentAllowed(kiroAgentFile(kiroDir()), toolsAllowed(approval));
272
+ const which = approval === "ask-before-sending" ? "reads run without asking; send, open_goal and claim prompt" : `the ${PANTHEON_TOOL_NAMES.length} pantheon tools run without asking`;
273
+ return { tool, where: file, status: `${status} — in the Kiro IDE, ${which}${agent === "absent" ? "; the CLI asks per call unless you use the pantheon agent (pantheon hook install)" : `; in the CLI's pantheon agent too (${agent})`}` };
274
+ }
275
+ catch (e) {
276
+ return { tool, where: file, status: `failed: ${e.message}` };
277
+ }
278
+ }
254
279
  function vsCodeUserScope(entry) {
255
280
  const tool = "vs code";
256
281
  const dir = vsCodeUserDir();
@@ -268,7 +293,7 @@ export const CODEX_APPROVAL_LINE = `default_tools_approval_mode = "approve"`;
268
293
  export function approvalSummary(approval) {
269
294
  return approval === "ask-before-sending"
270
295
  ? `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`
271
- : `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`;
296
+ : `pantheon's ${PANTHEON_TOOL_NAMES.length} tools run without a per-call prompt in Claude Code, Cursor, Codex, the Kiro IDE and Kiro's pantheon agent — 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; Kiro's default CLI agent asks per call`;
272
297
  }
273
298
  export function codexTomlBlock(entry, env = { PANTHEON_AGENT: "codex" }, approval = "approve") {
274
299
  const envToml = Object.entries(env)
@@ -466,7 +491,7 @@ export function installForUser(approval = chosenApproval()) {
466
491
  const entry = userEntry();
467
492
  return [
468
493
  claudeUserScope(entry), cursorUserScope(entry), codexUserScope(entry, approval),
469
- copilotUserScope(entry), vsCodeUserScope(entry),
494
+ copilotUserScope(entry), vsCodeUserScope(entry), kiroUserScope(entry, approval),
470
495
  ...grantToolPermissions(approval).map((g) => ({
471
496
  tool: g.tool,
472
497
  where: g.tool.startsWith("claude") ? claudeSettingsFile() : cursorCliConfigFile(),
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ export const kiroDir = (home = homedir(), env = process.env) => env.KIRO_HOME || join(home, ".kiro");
5
+ export const KIRO_AGENT_NAME = "pantheon";
6
+ export const kiroAgentFile = (kiroBase) => join(kiroBase, "agents", `${KIRO_AGENT_NAME}.json`);
7
+ export const isOurKiroHook = (cmd) => !!cmd && /\bhook\s+kiro-(?:prompt|stop)\b/.test(cmd);
8
+ export const kiroAllowed = (tools) => tools.map((t) => `@pantheon/${t}`);
9
+ export function kiroAgentDoc(bin, allowed) {
10
+ return {
11
+ name: KIRO_AGENT_NAME,
12
+ description: "Kiro's default agent, plus Pantheon: its tools run without asking, and a turn is held open while a teammate is waiting on you",
13
+ tools: ["*"],
14
+ allowedTools: kiroAllowed(allowed),
15
+ includeMcpJson: true,
16
+ hooks: {
17
+ userPromptSubmit: [{ command: `${bin} hook kiro-prompt >/dev/null 2>&1` }],
18
+ stop: [{ command: `${bin} hook kiro-stop` }],
19
+ },
20
+ };
21
+ }
22
+ export function readKiroAgent(file) {
23
+ if (!existsSync(file))
24
+ return null;
25
+ try {
26
+ const doc = JSON.parse(readFileSync(file, "utf8"));
27
+ const events = Object.entries(doc.hooks ?? {}).filter(([, es]) => (es ?? []).some((h) => isOurKiroHook(h.command))).map(([e]) => e);
28
+ return { doc, ours: events.length > 0, events };
29
+ }
30
+ catch {
31
+ return { doc: {}, ours: false, events: [] };
32
+ }
33
+ }
34
+ export function writeKiroAgent(file, doc) {
35
+ mkdirSync(dirname(file), { recursive: true });
36
+ writeFileSync(file, JSON.stringify(doc, null, 2) + "\n");
37
+ }
38
+ export function refreshKiroAgentAllowed(file, allowed) {
39
+ const a = readKiroAgent(file);
40
+ if (!a?.ours)
41
+ return "absent";
42
+ const want = kiroAllowed(allowed);
43
+ if (JSON.stringify(a.doc.allowedTools) === JSON.stringify(want))
44
+ return "unchanged";
45
+ writeKiroAgent(file, { ...a.doc, allowedTools: want });
46
+ return "updated";
47
+ }
48
+ export function kiroDefaultIsPantheon(base = kiroDir()) {
49
+ try {
50
+ return JSON.parse(readFileSync(join(base, "settings", "cli.json"), "utf8"))["chat.defaultAgent"] === KIRO_AGENT_NAME;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ //# sourceMappingURL=kiro-files.js.map
package/dist/machine.js CHANGED
@@ -4,7 +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
+ import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
8
8
  const sh = (cmd) => {
9
9
  try {
10
10
  return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
@@ -120,6 +120,10 @@ export function wiring() {
120
120
  const f = join(vscodeDir, "mcp.json");
121
121
  out.push({ tool: "vs code", state: existsSync(f) && /"pantheon"/.test(readFileSync(f, "utf8")) ? "wired" : "not wired" });
122
122
  }
123
+ if (existsSync(kiroDir())) {
124
+ const f = kiroMcpConfigFile();
125
+ out.push({ tool: "kiro", state: existsSync(f) && /"pantheon"/.test(readFileSync(f, "utf8")) ? "wired" : "not wired" });
126
+ }
123
127
  return out;
124
128
  }
125
129
  export function logFileFor(kind, proc) {
package/dist/measure.js CHANGED
@@ -3,7 +3,7 @@ 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, copilotMcpConfigFile } from "./install.js";
6
+ import { codexApprovalState, copilotMcpConfigFile, kiroMcpConfigFile } from "./install.js";
7
7
  import { existsSync as fileExists, readFileSync } from "node:fs";
8
8
  import { dirname as dirOf } from "node:path";
9
9
  import { PANTHEON_TOOL_NAMES, WRITE_TOOL_NAMES } from "./tools.js";
@@ -161,13 +161,41 @@ export function toolsReport(registered, seen, approval, opts = {}) {
161
161
  }
162
162
  const copilot = seen.copilot === undefined || seen.copilot === null ? null
163
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}` : ""}`;
164
+ let kiro = null;
165
+ if (seen.kiro === "absent")
166
+ kiro = "Kiro has no pantheon entry";
167
+ else if (Array.isArray(seen.kiro)) {
168
+ const want = [...(approval === "ask-before-sending" ? PANTHEON_TOOL_NAMES.filter((t) => !WRITE_TOOL_NAMES.includes(t)) : PANTHEON_TOOL_NAMES)].sort();
169
+ const have = [...new Set(seen.kiro)].filter((t) => t.startsWith("pantheon_")).sort();
170
+ const extra = have.filter((t) => !want.includes(t)), missing = want.filter((t) => !have.includes(t));
171
+ if (!extra.length && !missing.length)
172
+ kiro = `Kiro IDE allows ${have.length}`;
173
+ else {
174
+ kiro = `Kiro IDE allows ${have.length}${missing.length ? `, missing ${missing.map((t) => t.replace(/^pantheon_/, "")).join(", ")}` : ""}${extra.length ? `, and allows ${extra.map((t) => t.replace(/^pantheon_/, "")).join(", ")} you chose to be asked about` : ""}`;
175
+ problems.push("Kiro's autoApprove does not match");
176
+ }
177
+ }
178
+ const detail = `${head}; ${claude.text}, ${cursor.text}, ${codex}${copilot ? `, ${copilot}` : ""}${kiro ? `, ${kiro}` : ""}`;
165
179
  if (!problems.length)
166
180
  return { level: "pass", detail };
167
181
  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` };
168
182
  }
169
183
  export function grantsSeen(projectDir = null) {
170
- return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState(), copilot: copilotWired() };
184
+ return { claude: claudeGrantedEffective(projectDir), cursor: cursorGrantedEffective(projectDir), codex: codexApprovalState(), copilot: copilotWired(), kiro: kiroApproved() };
185
+ }
186
+ export function kiroApproved(file = kiroMcpConfigFile()) {
187
+ if (!fileExists(dirOf(dirOf(file))))
188
+ return null;
189
+ try {
190
+ const doc = JSON.parse(readFileSync(file, "utf8"));
191
+ const entry = doc.mcpServers?.["pantheon"];
192
+ if (!entry)
193
+ return "absent";
194
+ return Array.isArray(entry.autoApprove) ? entry.autoApprove.filter((t) => typeof t === "string") : [];
195
+ }
196
+ catch {
197
+ return "absent";
198
+ }
171
199
  }
172
200
  export function copilotWired(file = copilotMcpConfigFile()) {
173
201
  if (!fileExists(dirOf(file)))
package/dist/notice.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { decisionPrompt, decisionTitle } from "./decision-ui.js";
2
- export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor", copilot: "Copilot", grok: "Grok" };
2
+ export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor", copilot: "Copilot", kiro: "Kiro", 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" },
@@ -67,6 +67,8 @@ const agentLabel = (agent) => {
67
67
  return "Cursor";
68
68
  if (a.includes("copilot"))
69
69
  return "Copilot";
70
+ if (a.includes("kiro"))
71
+ return "Kiro";
70
72
  return null;
71
73
  };
72
74
  function whoLine(from, agent, ts, now) {
package/dist/notify.js CHANGED
@@ -1,12 +1,14 @@
1
+ import { chosenApproval } from "./install.js";
2
+ import { toolsAllowed } from "./tools.js";
1
3
  import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
4
  import { homedir, platform } from "node:os";
3
5
  import { dirname, join, resolve } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { whichPantheon } from "./service.js";
6
8
  import { detectForHooks, agentById } from "./agents.js";
7
- import { codexHookState, cursorHookState, copilotHookState, installCodexHooks, installCursorHooks, installCopilotHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, } from "./hooks-install.js";
9
+ import { codexHookState, cursorHookState, copilotHookState, kiroHookState, installCodexHooks, installCursorHooks, installCopilotHooks, installKiroHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, removeKiroHooks, } from "./hooks-install.js";
8
10
  export const HOOK_MARKER = "pantheon hook";
9
- export const HOOK_RE = /(?:pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s|cli\.js"?\s+hook\s+(?:stop|prompt|end|codex-stop|copilot-stop|cursor-stop)\b)/i;
11
+ export const HOOK_RE = /(?:pantheon(?:\.cmd|\.exe|\.bat)?"?\s+hook\s|cli\.js"?\s+hook\s+(?:stop|prompt|end|codex-stop|copilot-stop|cursor-stop|kiro-stop|kiro-prompt)\b)/i;
10
12
  export const isOurHookCommand = (cmd) => !!cmd && (cmd.includes(HOOK_MARKER) || HOOK_RE.test(cmd));
11
13
  export const hookBin = (bin) => (/\s/.test(bin) ? `"${bin}"` : bin);
12
14
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -108,6 +110,9 @@ export function agentHookStates(projectDir = process.cwd(), only) {
108
110
  const copilot = copilotHookState(scope, projectDir);
109
111
  if (copilot.installed)
110
112
  out.push(copilot);
113
+ const kiro = kiroHookState(scope, projectDir);
114
+ if (kiro.installed)
115
+ out.push(kiro);
111
116
  }
112
117
  const byAgent = new Map();
113
118
  for (const s of out) {
@@ -133,6 +138,8 @@ export function repairHook(home) {
133
138
  missing.push(id);
134
139
  if (id === "copilot" && !copilotHookState(intent.scope, projectDir).installed)
135
140
  missing.push(id);
141
+ if (id === "kiro" && !kiroHookState(intent.scope, projectDir).installed)
142
+ missing.push(id);
136
143
  }
137
144
  if (!missing.length)
138
145
  return null;
@@ -184,7 +191,7 @@ export function installHooks(scope, projectDir = process.cwd(), only) {
184
191
  ? only.map((id) => agentById(id)).filter((b) => !!b)
185
192
  : available;
186
193
  if (!pick.length) {
187
- throw new Error("no agent found — install claude, codex, cursor-agent, or copilot, or open a project that already uses one");
194
+ throw new Error("no agent found — install claude, codex, cursor-agent, copilot, or kiro-cli, or open a project that already uses one");
188
195
  }
189
196
  const lines = [];
190
197
  const agents = [];
@@ -206,6 +213,10 @@ export function installHooks(scope, projectDir = process.cwd(), only) {
206
213
  lines.push(...installCopilotHooks(bin, scope, projectDir));
207
214
  agents.push("copilot");
208
215
  }
216
+ else if (backend.id === "kiro") {
217
+ lines.push(...installKiroHooks(bin, scope, projectDir, toolsAllowed(chosenApproval())));
218
+ agents.push("kiro");
219
+ }
209
220
  }
210
221
  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.`);
211
222
  return { lines, agents };
@@ -219,6 +230,7 @@ export function removeHook(projectDir = process.cwd()) {
219
230
  out.push(...removeCodexHooks(projectDir));
220
231
  out.push(...removeCursorHooks(projectDir));
221
232
  out.push(...removeCopilotHooks(projectDir));
233
+ out.push(...removeKiroHooks(projectDir));
222
234
  return out.length ? out : [" nothing installed"];
223
235
  }
224
236
  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 | copilot | auto
173
+ # agent: auto # claude-code | codex | cursor | copilot | kiro | 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,12 +1,13 @@
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", "copilot"];
4
+ export const AGENT_SURFACES = ["codex", "claude-code", "cursor", "copilot", "kiro"];
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
9
  copilot: "com.microsoft.VSCode",
10
+ kiro: "com.amazon.codewhisperer",
10
11
  };
11
12
  export const profilePath = (home = CONFIG_DIR) => join(home, "profile.json");
12
13
  export function isAgentSurface(value) {
@@ -17,6 +18,8 @@ export function surfaceFromAgent(agent) {
17
18
  return "cursor";
18
19
  if (agent && /copilot/i.test(agent))
19
20
  return "copilot";
21
+ if (agent && /kiro/i.test(agent))
22
+ return "kiro";
20
23
  if (agent && /claude/i.test(agent))
21
24
  return "claude-code";
22
25
  return "codex";
@@ -5,6 +5,7 @@ export const SIGN_IN_FIX = {
5
5
  codex: "run `codex login`",
6
6
  cursor: "run `cursor-agent login`",
7
7
  copilot: "run `copilot login` (or `gh auth login` — copilot uses gh's sign-in too)",
8
+ kiro: "run `kiro-cli login` (or set KIRO_API_KEY)",
8
9
  };
9
10
  const sh = (dir, ...args) => execFileSync("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 }).trim();
10
11
  export const gitProbe = {
@@ -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; Copilot asks once per tool per folder)"
198
+ ? "your agent sends to teammates in your name without asking each time (Claude Code, Cursor, Codex, and Kiro's IDE and pantheon agent; Copilot asks once per tool per folder)"
199
199
  : "your agent asks you before each send, goal and claim; reading and waiting never prompt"}`);
200
200
  io.say(` • Decisions: ${a.decides.length ? `you personally decide ${a.decides.join(", ")}` : "none reserved for you — your agent may decide; add an area when one comes up"}`);
201
201
  if (a.wake !== "no" && ctx.project)
package/dist/tray.js CHANGED
@@ -144,7 +144,7 @@ async function trayStatusOne(cfg) {
144
144
  ];
145
145
  const profile = loadProfile(cfg);
146
146
  const surface = profile.agent.primary_surface;
147
- const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : surface === "copilot" ? "Copilot" : "Codex";
147
+ const surfaceName = surface === "claude-code" ? "Claude" : surface === "cursor" ? "Cursor" : surface === "copilot" ? "Copilot" : surface === "kiro" ? "Kiro" : "Codex";
148
148
  const rawWorkerState = workerState();
149
149
  return {
150
150
  configured: true,
package/dist/worker.js CHANGED
@@ -4,7 +4,7 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { PantheonClient } from "./client.js";
7
- import { headlessSpawn, materializeSpawnFiles, chooseAgent, describeChoice } from "./agents.js";
7
+ import { headlessSpawn, materializeSpawnFiles, chooseAgent, describeChoice, unattendedRefusal } from "./agents.js";
8
8
  import { agentSpawn } from "./which.js";
9
9
  import { VERSION } from "./version.js";
10
10
  import { applyAnswer, loadPolicy, mayAsk, needsSelfUpgrade, recordAsk, savePolicy, useGrant, } from "./worker-policy.js";
@@ -604,6 +604,9 @@ export class Worker {
604
604
  this.opts.log(` agent ${said}`);
605
605
  this.lastAgentSaid = said;
606
606
  }
607
+ const refused = unattendedRefusal(choice);
608
+ if (refused)
609
+ throw new Error(refused);
607
610
  const plan = headlessSpawn({ backend: choice.backend, binary: choice.binary, prompt: brief, worktree, cfg: this.cfg, branch, allowBash, runHome });
608
611
  materializeSpawnFiles(worktree, plan);
609
612
  ({ command, args, env } = { command: plan.command, args: plan.args, env: { ...env, ...plan.env } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.26.5",
3
+ "version": "0.27.0",
4
4
  "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {