@join-pantheon/cli 0.26.4 → 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
@@ -326,6 +326,20 @@ async function guidedSetup(cfg, project, opts = {}) {
326
326
  rl?.close();
327
327
  }
328
328
  }
329
+ async function workdirFor(cfg, hud) {
330
+ const { loadAll, findMembership } = await import("./config.js");
331
+ const { pickWorkdir } = await import("./workdir.js");
332
+ const all = loadAll();
333
+ const m = all ? findMembership(all, hud.space || cfg.space) : undefined;
334
+ const team = m?.name ?? cfg.space;
335
+ const bindings = all && m ? Object.values(all.bindings ?? {}).filter((b) => b.space === m.id) : [];
336
+ let hints = [];
337
+ if (bindings.length > 1 && hud.goal) {
338
+ const acts = await fetchHistory(cfg).catch(() => []);
339
+ hints = acts.filter((x) => x.goal === hud.goal).flatMap((x) => [x.body, JSON.stringify(x.meta ?? {})]);
340
+ }
341
+ return pickWorkdir(bindings, hints, team);
342
+ }
329
343
  async function fetchHistory(cfg, since = 0) {
330
344
  const base = cfg.relay.replace(/^ws/, "http").replace(/\/+$/, "");
331
345
  const res = await fetch(`${base}/history?space=${encodeURIComponent(cfg.space)}&token=${encodeURIComponent(cfg.token)}&since=${since}`, { signal: AbortSignal.timeout(8000) });
@@ -428,6 +442,8 @@ function printReady(tools) {
428
442
  oneTime.set("cursor", `cursor enable the "pantheon" MCP server if it asks`);
429
443
  if (/codex/i.test(t.tool))
430
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`);
431
447
  if (/copilot/i.test(t.tool))
432
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`);
433
449
  }
@@ -487,7 +503,7 @@ ${B("control")}
487
503
  pantheon admit <who> let someone in (--may-wake to allow unattended work here)
488
504
  pantheon admit <who> --revoke remove them
489
505
  pantheon worker stay answerable while you are away
490
- (--agent claude-code|codex|cursor|copilot|auto, --persona <name>)
506
+ (--agent claude-code|codex|cursor|copilot|kiro|auto, --persona <name>)
491
507
  pantheon policy what this machine's worker may do, in one screen (grant/revoke self-upgrade)
492
508
  pantheon persona how a woken worker should behave: list / init <name> / show <name>
493
509
  pantheon notifier alert you when a task requires your decision
@@ -500,7 +516,7 @@ ${B("control")}
500
516
  pantheon approvals decisions waiting on you; show or answer one
501
517
  pantheon stop [relay|worker|notifier|menubar|all] stop it
502
518
  pantheon hook install your agent keeps working when a peer is waiting on it
503
- (auto-detects claude / codex / cursor / copilot on this machine)
519
+ (auto-detects claude / codex / cursor / copilot / kiro on this machine)
504
520
  pantheon spend woken runs on this machine, and what pantheon cannot see
505
521
  pantheon leave remove what pantheon put on this machine
506
522
  pantheon panic cut off all unattended work on this machine, now
@@ -1318,13 +1334,13 @@ async function main() {
1318
1334
  const sub = argv[1] ?? "status";
1319
1335
  const hookCfg = resolveForHook();
1320
1336
  const sessionOf = (raw) => {
1337
+ let fromJson;
1321
1338
  try {
1322
1339
  const j = JSON.parse(raw);
1323
- return j.session_id ?? j.sessionId ?? j.conversation_id ?? "unknown";
1324
- }
1325
- catch {
1326
- return "unknown";
1340
+ fromJson = j.session_id ?? j.sessionId ?? j.conversation_id;
1327
1341
  }
1342
+ catch { }
1343
+ return fromJson ?? process.env.KIRO_SESSION_ID ?? "unknown";
1328
1344
  };
1329
1345
  const reportIdle = async (session, seen) => {
1330
1346
  if (!hookCfg)
@@ -1339,6 +1355,22 @@ async function main() {
1339
1355
  }
1340
1356
  return false;
1341
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
+ }
1342
1374
  if (sub === "prompt") {
1343
1375
  let raw = "";
1344
1376
  try {
@@ -1448,7 +1480,7 @@ async function main() {
1448
1480
  process.exit(0);
1449
1481
  }
1450
1482
  }
1451
- if (sub === "codex-stop" || sub === "copilot-stop") {
1483
+ if (sub === "codex-stop" || sub === "copilot-stop" || sub === "kiro-stop") {
1452
1484
  let raw = "";
1453
1485
  try {
1454
1486
  for await (const chunk of process.stdin)
@@ -1557,7 +1589,7 @@ async function main() {
1557
1589
  }
1558
1590
  const states = agentHookStates();
1559
1591
  if (!states.length) {
1560
- 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)`);
1561
1593
  return;
1562
1594
  }
1563
1595
  for (const s of states) {
@@ -1640,7 +1672,7 @@ async function main() {
1640
1672
  else {
1641
1673
  console.error(` config: none to remove`);
1642
1674
  }
1643
- 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),")}`);
1644
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.")}`);
1645
1677
  if (cfg) {
1646
1678
  console.error(`\n ${D(`Your teammates still list you in "${cfg.space}". Ask one of them to run:`)}`);
@@ -2007,7 +2039,7 @@ async function main() {
2007
2039
  const publicChange = name !== undefined || title !== undefined || responsibilities !== undefined ||
2008
2040
  humanRequired !== undefined || agentCanDecide !== undefined || capabilities !== undefined || keywords !== undefined;
2009
2041
  if (!surface && !publicChange) {
2010
- 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']");
2011
2043
  }
2012
2044
  if (flag("space")) {
2013
2045
  const pf = loadAll() ?? die("not configured");
@@ -2262,6 +2294,46 @@ async function main() {
2262
2294
  }
2263
2295
  return;
2264
2296
  }
2297
+ if (hud?.action === "reply") {
2298
+ const notSent = (why) => { console.log(`not sent: ${why.split("\n")[0]}`); process.exit(1); };
2299
+ const kind = hud.kind ?? "NOTE";
2300
+ const body = (hud.body ?? "").trim();
2301
+ if (!body)
2302
+ notSent("nothing to send");
2303
+ const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2304
+ await c2.connect().catch((e) => notSent(e.message));
2305
+ const o = hud.decision ? c2.obligations.find((x) => x.id === hud.decision) : undefined;
2306
+ const acts = await fetchHistory(cfg).catch(() => []);
2307
+ const target = acts.find((x) => x.id === (o?.ref ?? hud.act));
2308
+ const goal = o?.goal ?? target?.goal ?? hud.goal ?? null;
2309
+ if (kind === "ANSWER" && !o && !target) {
2310
+ c2.close();
2311
+ notSent("that decision is no longer waiting — nothing to answer");
2312
+ }
2313
+ const ref = kind === "ANSWER" ? (o?.ref ?? target?.id ?? null) : kind === "ASK" ? (target?.id ?? null) : null;
2314
+ const to = kind === "ASK" ? (target && target.from !== cfg.member ? [target.from] : null) : null;
2315
+ if (kind === "ASK" && !to) {
2316
+ c2.close();
2317
+ notSent("cannot tell who raised this — use Show the thread and reply from there");
2318
+ }
2319
+ const sent = await c2.send({ act: kind, body, goal, ref, to, after_seq: c2.readSeq, meta: { surface: "menubar" } })
2320
+ .catch((e) => notSent(e.message));
2321
+ c2.close();
2322
+ console.log(`sent ${kind} id=${sent.id} seq=${sent.seq}`);
2323
+ return;
2324
+ }
2325
+ if (hud?.action === "retract") {
2326
+ const notSent = (why) => { console.log(`not sent: ${why.split("\n")[0]}`); process.exit(1); };
2327
+ if (!hud.act)
2328
+ notSent("nothing to retract");
2329
+ const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2330
+ await c2.connect().catch((e) => notSent(e.message));
2331
+ const sent = await c2.send({ act: "RETRACT", body: "Retracted from the menu bar.", goal: null, ref: hud.act, to: null, meta: { surface: "menubar" } })
2332
+ .catch((e) => notSent(e.message));
2333
+ c2.close();
2334
+ console.log(`sent RETRACT id=${sent.id} seq=${sent.seq}`);
2335
+ return;
2336
+ }
2265
2337
  if (hud?.action === "done") {
2266
2338
  const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
2267
2339
  await c2.connect().catch((e) => die(e.message));
@@ -2278,6 +2350,15 @@ async function main() {
2278
2350
  return;
2279
2351
  }
2280
2352
  if (hud?.action === "details") {
2353
+ const wd = await workdirFor(cfg, hud).catch(() => null);
2354
+ if (wd?.path) {
2355
+ try {
2356
+ process.chdir(wd.path);
2357
+ }
2358
+ catch { }
2359
+ }
2360
+ if (wd)
2361
+ console.error(D(` ${wd.note}`));
2281
2362
  const acts = await fetchHistory(cfg).catch(() => []);
2282
2363
  const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
2283
2364
  let o;
@@ -2377,6 +2458,10 @@ async function main() {
2377
2458
  }
2378
2459
  if (hud?.action === "open") {
2379
2460
  const p = loadProfile(cfg);
2461
+ const wd = await workdirFor(cfg, hud).catch(() => null);
2462
+ const cwd = wd?.path ?? undefined;
2463
+ if (wd)
2464
+ console.error(D(` ${wd.note}`));
2380
2465
  if (hud.tool && isAgentSurface(hud.tool) && p.agent.primary_surface !== hud.tool) {
2381
2466
  saveProfile({ ...p, agent: { ...p.agent, primary_surface: hud.tool }, updated_at: Date.now() });
2382
2467
  console.error(D(`${hud.tool} is now where your decisions open`));
@@ -2388,24 +2473,30 @@ async function main() {
2388
2473
  const { spawnSync } = await import("node:child_process");
2389
2474
  {
2390
2475
  const sp = agentSpawn(bin, [hud.prompt]);
2391
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2476
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
2392
2477
  }
2393
2478
  return;
2394
2479
  }
2395
2480
  if (hud.prompt) {
2396
2481
  const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
2397
2482
  if (!agent)
2398
- 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)");
2399
2484
  console.error(D(`opening ${agent.id}…`));
2400
2485
  const { spawnSync } = await import("node:child_process");
2401
2486
  {
2402
2487
  const sp = agentSpawn(agent.binary, launchArgs(agent.id, hud.prompt, sessionName(hud.prompt)));
2403
- spawnSync(sp.command, sp.args, { stdio: "inherit", ...sp.options });
2488
+ spawnSync(sp.command, sp.args, { stdio: "inherit", ...(cwd ? { cwd } : {}), ...sp.options });
2404
2489
  }
2405
2490
  return;
2406
2491
  }
2407
2492
  if (hud.decision)
2408
2493
  id = hud.decision;
2494
+ if (cwd) {
2495
+ try {
2496
+ process.chdir(cwd);
2497
+ }
2498
+ catch { }
2499
+ }
2409
2500
  }
2410
2501
  const client = new PantheonClient(cfg, undefined, { role: "cli" });
2411
2502
  await client.connect().catch((e) => die(e.message));
@@ -2421,7 +2512,7 @@ async function main() {
2421
2512
  }
2422
2513
  const agent = pickExplainAgent(loadProfile(cfg).agent.primary_surface, detectAvailable());
2423
2514
  if (!agent)
2424
- 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)");
2425
2516
  console.error(D(`opening ${agent.id} with the question…`));
2426
2517
  const { spawnSync } = await import("node:child_process");
2427
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);
@@ -8,7 +8,8 @@ export function selectActs(acts, opts = {}) {
8
8
  }
9
9
  const when = (ts) => new Date(ts).toISOString().slice(0, 19).replace("T", " ") + "Z";
10
10
  export function formatAct(a, opts = {}) {
11
- const who = `${a.from}${a.role && a.role !== "interactive" ? ` (${a.role})` : ""}`;
11
+ const surface = a.meta?.surface;
12
+ const who = `${a.from}${surface === "menubar" ? " (from the menu bar)" : a.role && a.role !== "interactive" ? ` (${a.role})` : ""}`;
12
13
  const head = `[${a.seq}] ${a.act} from ${who}${a.to?.length ? ` to ${a.to.join(",")}` : ""} ${when(a.ts)} id=${a.id}${a.ref ? ` ref=${a.ref}` : ""}${a.goal ? ` goal=${a.goal}${opts.goalTitle ? ` "${opts.goalTitle}"` : ""}` : ""}`;
13
14
  if (opts.short)
14
15
  return `${head}\n ${(a.summary ?? a.body).split("\n")[0].slice(0, 160)}`;
@@ -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