diff --git a/CLI_AND_DAEMON.md b/CLI_AND_DAEMON.md
index c8c43fb72..1fdf2a4c3 100644
--- a/CLI_AND_DAEMON.md
+++ b/CLI_AND_DAEMON.md
@@ -146,6 +146,8 @@ The daemon auto-detects these AI CLIs on your PATH:
| Gemini | `gemini` | Google's coding agent |
| [Pi](https://pi.dev/) | `pi` | Pi coding agent |
| [Cursor Agent](https://cursor.com/) | `cursor-agent` | Cursor's headless coding agent |
+| Kimi | `kimi` | Moonshot coding agent |
+| Kiro CLI | `kiro-cli` | Kiro ACP coding agent |
You need at least one installed. The daemon registers each detected CLI as an available runtime.
@@ -193,6 +195,10 @@ Agent-specific overrides:
| `MULTICA_PI_MODEL` | Override the Pi model used |
| `MULTICA_CURSOR_PATH` | Custom path to the `cursor-agent` binary |
| `MULTICA_CURSOR_MODEL` | Override the Cursor Agent model used |
+| `MULTICA_KIMI_PATH` | Custom path to the `kimi` binary |
+| `MULTICA_KIMI_MODEL` | Override the Kimi model used |
+| `MULTICA_KIRO_PATH` | Custom path to the `kiro-cli` binary |
+| `MULTICA_KIRO_MODEL` | Override the Kiro model used |
### Self-Hosted Server
diff --git a/README.md b/README.md
index edbe069a6..817be3ded 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ Turn coding agents into real teammates — assign tasks, track progress, compoun
Multica turns coding agents into real teammates. Assign issues to an agent like you'd assign to a colleague — they'll pick up the work, write code, report blockers, and update statuses autonomously.
-No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**, **OpenClaw**, **OpenCode**, **Hermes**, **Gemini**, **Pi**, and **Cursor Agent**.
+No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**, **OpenClaw**, **OpenCode**, **Hermes**, **Gemini**, **Pi**, **Cursor Agent**, **Kimi**, and **Kiro CLI**.
@@ -98,7 +98,7 @@ multica setup # Connect to Multica Cloud, log in, start daemon
multica setup # Configure, authenticate, and start the daemon
```
-The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`, `hermes`, `gemini`, `pi`, `cursor-agent`) on your PATH.
+The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`, `hermes`, `gemini`, `pi`, `cursor-agent`, `kimi`, `kiro-cli`) on your PATH.
### 2. Verify your runtime
@@ -108,7 +108,7 @@ Open your workspace in the Multica web app. Navigate to **Settings → Runtimes*
### 3. Create an agent
-Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, OpenClaw, OpenCode, Hermes, Gemini, Pi, or Cursor Agent). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
+Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, OpenClaw, OpenCode, Hermes, Gemini, Pi, Cursor Agent, Kimi, or Kiro CLI). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
### 4. Assign your first task
@@ -162,7 +162,8 @@ See the [CLI and Daemon Guide](CLI_AND_DAEMON.md) for the full command reference
│ Agent Daemon │ runs on your machine
└──────────────┘ (Claude Code, Codex, OpenCode,
OpenClaw, Hermes, Gemini,
- Pi, Cursor Agent)
+ Pi, Cursor Agent, Kimi,
+ Kiro CLI)
```
| Layer | Stack |
@@ -170,7 +171,7 @@ See the [CLI and Daemon Guide](CLI_AND_DAEMON.md) for the full command reference
| Frontend | Next.js 16 (App Router) |
| Backend | Go (Chi router, sqlc, gorilla/websocket) |
| Database | PostgreSQL 17 with pgvector |
-| Agent Runtime | Local daemon executing Claude Code, Codex, OpenClaw, OpenCode, Hermes, Gemini, Pi, or Cursor Agent |
+| Agent Runtime | Local daemon executing Claude Code, Codex, OpenClaw, OpenCode, Hermes, Gemini, Pi, Cursor Agent, Kimi, or Kiro CLI |
## Development
diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md
index 40dc5cff1..7e86fb9e4 100644
--- a/SELF_HOSTING.md
+++ b/SELF_HOSTING.md
@@ -98,6 +98,8 @@ You also need at least one AI agent CLI installed:
- Gemini (`gemini` on PATH)
- [Pi](https://pi.dev/) (`pi` on PATH)
- [Cursor Agent](https://cursor.com/) (`cursor-agent` on PATH)
+- Kimi (`kimi` on PATH)
+- Kiro CLI (`kiro-cli` on PATH)
### b) One-command setup
diff --git a/apps/docs/content/docs/agents-create.mdx b/apps/docs/content/docs/agents-create.mdx
index 3a9e48a1a..2d4a7af5c 100644
--- a/apps/docs/content/docs/agents-create.mdx
+++ b/apps/docs/content/docs/agents-create.mdx
@@ -21,7 +21,7 @@ The form has only two required fields: **name** (unique within the workspace) an
## Pick an AI coding tool
-Each runtime is backed by a specific AI coding tool. Multica supports 10 of them. The most common choices:
+Each runtime is backed by a specific AI coding tool. Multica supports 11 of them. The most common choices:
| Tool | Good for |
|---|---|
@@ -31,7 +31,7 @@ Each runtime is backed by a specific AI coding tool. Multica supports 10 of them
| **Copilot** | Teams leveraging their GitHub account entitlements |
| **Gemini** | Users in the Google ecosystem |
-The other five (Hermes, Kimi, OpenCode, Pi, OpenClaw), along with each tool's full capability matrix (session resume, MCP, skill injection path, model selection), are covered in [AI coding tools comparison](/providers).
+The other six (Hermes, Kimi, Kiro CLI, OpenCode, Pi, OpenClaw), along with each tool's full capability matrix (session resume, MCP, skill injection path, model selection), are covered in [AI coding tools comparison](/providers).
## Writing system instructions
@@ -123,5 +123,5 @@ Archived agents can't be assigned new tasks.
## Next steps
- [Skills](/skills) — attach knowledge packs to an agent
-- [AI coding tools comparison](/providers) — full capability matrix across all 10 tools
+- [AI coding tools comparison](/providers) — full capability matrix across all 11 tools
- [Assigning issues to agents](/assigning-issues) — put your new agent to work
diff --git a/apps/docs/content/docs/agents-create.zh.mdx b/apps/docs/content/docs/agents-create.zh.mdx
index fb0eac6f3..57eb645c7 100644
--- a/apps/docs/content/docs/agents-create.zh.mdx
+++ b/apps/docs/content/docs/agents-create.zh.mdx
@@ -21,7 +21,7 @@ multica agent create
## 选一款 AI 编程工具
-运行时背后是一款具体的 AI 编程工具。Multica 支持 10 款,最常用的几款:
+运行时背后是一款具体的 AI 编程工具。Multica 支持 11 款,最常用的几款:
| 工具 | 适合 |
|---|---|
@@ -31,7 +31,7 @@ multica agent create
| **Copilot** | 用 GitHub 账号权益的团队 |
| **Gemini** | Google 生态用户 |
-另外 5 款(Hermes、Kimi、OpenCode、Pi、OpenClaw)以及每款工具的完整能力差别(会话恢复、MCP、skill 注入路径、模型选择)见 [AI 编程工具对照](/providers)。
+另外 6 款(Hermes、Kimi、Kiro CLI、OpenCode、Pi、OpenClaw)以及每款工具的完整能力差别(会话恢复、MCP、skill 注入路径、模型选择)见 [AI 编程工具对照](/providers)。
## 写系统指令
@@ -123,5 +123,5 @@ claude --model --max-turns 100 --append-system-prompt "always respond in
## 下一步
- [Skills](/skills) —— 给智能体挂专业知识包
-- [AI 编程工具对照](/providers) —— 10 款工具的完整能力差别
+- [AI 编程工具对照](/providers) —— 11 款工具的完整能力差别
- [把 issue 分配给智能体](/assigning-issues) —— 创建完之后怎么用起来
diff --git a/apps/docs/content/docs/cli/installation.zh.mdx b/apps/docs/content/docs/cli/installation.zh.mdx
index b5c8f02ff..81ae5bbad 100644
--- a/apps/docs/content/docs/cli/installation.zh.mdx
+++ b/apps/docs/content/docs/cli/installation.zh.mdx
@@ -78,7 +78,7 @@ multica daemon status
Confirm:
1. Status is `running`
-2. At least one agent is listed (e.g. `claude`, `codex`, `gemini`, `opencode`, `openclaw`, `hermes`, or `pi`)
+2. At least one agent is listed (e.g. `claude`, `codex`, `gemini`, `opencode`, `openclaw`, `hermes`, `kiro`, or `pi`)
3. At least one workspace is being watched
If the agents list is empty, install at least one supported AI agent CLI:
@@ -88,6 +88,8 @@ If the agents list is empty, install at least one supported AI agent CLI:
- OpenCode (`opencode`)
- OpenClaw (`openclaw`)
- Hermes (`hermes`)
+- Kimi (`kimi`)
+- Kiro CLI (`kiro-cli`)
Then restart the daemon:
diff --git a/apps/docs/content/docs/cli/reference.zh.mdx b/apps/docs/content/docs/cli/reference.zh.mdx
index f43c534cd..f87579c81 100644
--- a/apps/docs/content/docs/cli/reference.zh.mdx
+++ b/apps/docs/content/docs/cli/reference.zh.mdx
@@ -92,6 +92,10 @@ The daemon auto-detects these AI CLIs on your PATH:
| OpenCode | `opencode` | Open-source coding agent |
| OpenClaw | `openclaw` | Open-source coding agent |
| Hermes | `hermes` | Nous Research coding agent |
+| Kimi | `kimi` | Moonshot coding agent |
+| Kiro CLI | `kiro-cli` | Kiro ACP coding agent |
+| Pi | `pi` | Inflection coding agent |
+| Cursor Agent | `cursor-agent` | Cursor coding agent |
You need at least one installed. The daemon registers each detected CLI as an available runtime.
@@ -134,6 +138,14 @@ Agent-specific overrides:
| `MULTICA_HERMES_MODEL` | Override the Hermes model used |
| `MULTICA_GEMINI_PATH` | Custom path to the `gemini` binary |
| `MULTICA_GEMINI_MODEL` | Override the Gemini model used |
+| `MULTICA_PI_PATH` | Custom path to the `pi` binary |
+| `MULTICA_PI_MODEL` | Override the Pi model used |
+| `MULTICA_CURSOR_PATH` | Custom path to the `cursor-agent` binary |
+| `MULTICA_CURSOR_MODEL` | Override the Cursor model used |
+| `MULTICA_KIMI_PATH` | Custom path to the `kimi` binary |
+| `MULTICA_KIMI_MODEL` | Override the Kimi model used |
+| `MULTICA_KIRO_PATH` | Custom path to the `kiro-cli` binary |
+| `MULTICA_KIRO_MODEL` | Override the Kiro model used |
### Self-Hosted Server
diff --git a/apps/docs/content/docs/cloud-quickstart.mdx b/apps/docs/content/docs/cloud-quickstart.mdx
index bdd172526..01d86193d 100644
--- a/apps/docs/content/docs/cloud-quickstart.mdx
+++ b/apps/docs/content/docs/cloud-quickstart.mdx
@@ -7,7 +7,7 @@ import { Callout } from "fumadocs-ui/components/callout";
This page walks you end-to-end through Multica Cloud — **sign up → install the [CLI](/cli) → start the [daemon](/daemon-runtimes) → create an [agent](/agents) → assign your first [task](/tasks)**. Takes about 5 minutes.
-One prerequisite: you already have at least one [AI coding tool](/providers) installed locally ([Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), or [Pi](/providers#pi)). The daemon auto-detects them on startup and refuses to start if none are present.
+One prerequisite: you already have at least one [AI coding tool](/providers) installed locally ([Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), or [Pi](/providers#pi)). The daemon auto-detects them on startup and refuses to start if none are present.
## 1. Create an account
@@ -114,6 +114,6 @@ The web UI updates in **real time** (via WebSocket) — no refresh needed.
- [Daemon and runtimes](/daemon-runtimes) — how the daemon operates and what runtimes mean
- [Tasks](/tasks) — task lifecycle and retry rules
-- [AI coding tools compared](/providers) — capability differences across the 10 tools
+- [AI coding tools compared](/providers) — capability differences across the 11 tools
- [Desktop app](/desktop-app) — if you'd rather not run the daemon yourself
- [Self-host quickstart](/self-host-quickstart) — run your own backend
diff --git a/apps/docs/content/docs/cloud-quickstart.zh.mdx b/apps/docs/content/docs/cloud-quickstart.zh.mdx
index 3a2c4d77c..25baabda9 100644
--- a/apps/docs/content/docs/cloud-quickstart.zh.mdx
+++ b/apps/docs/content/docs/cloud-quickstart.zh.mdx
@@ -7,7 +7,7 @@ import { Callout } from "fumadocs-ui/components/callout";
这一页带你走一遍 Multica Cloud 的端到端流程——**注册 → 装 [命令行工具](/cli) → 启动 [守护进程](/daemon-runtimes) → 创建 [智能体](/agents) → 分配第一个 [任务](/tasks)**,约 5 分钟完成。
-前置只有一个:你本地已经装了至少一款 [AI 编程工具](/providers)([Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi))中的一款。守护进程启动时会自动探测它们,没装任何一个的话守护进程会直接拒绝启动。
+前置只有一个:你本地已经装了至少一款 [AI 编程工具](/providers)([Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi))中的一款。守护进程启动时会自动探测它们,没装任何一个的话守护进程会直接拒绝启动。
## 1. 注册账号
@@ -114,6 +114,6 @@ Web 界面会**实时**(通过 WebSocket)显示进度——不需要刷新
- [守护进程与运行时](/daemon-runtimes) —— 守护进程怎么运作、运行时概念
- [执行任务](/tasks) —— 任务生命周期、重试规则
-- [AI 编程工具对照](/providers) —— 10 款工具的能力差异
+- [AI 编程工具对照](/providers) —— 11 款工具的能力差异
- [桌面应用](/desktop-app) —— 不想自己跑守护进程的话
- [Self-Host 快速上手](/self-host-quickstart) —— 在自己服务器上跑一套
diff --git a/apps/docs/content/docs/daemon-runtimes.mdx b/apps/docs/content/docs/daemon-runtimes.mdx
index 9aac2c25a..b60c07f5b 100644
--- a/apps/docs/content/docs/daemon-runtimes.mdx
+++ b/apps/docs/content/docs/daemon-runtimes.mdx
@@ -21,7 +21,7 @@ multica daemon start
On startup it does four things:
1. Reads the credentials saved when you logged in
-2. Detects AI coding tools installed on your `PATH` (10 built-in: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi))
+2. Detects AI coding tools installed on your `PATH` (11 built-in: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi))
3. Registers itself with the server, along with a runtime for each detected tool
4. Keeps **polling every 3 seconds** for tasks to pick up, and **sends a heartbeat every 15 seconds**
@@ -108,4 +108,4 @@ More scenarios in [Troubleshooting](/troubleshooting).
## Next
- [Tasks](/tasks) — the full lifecycle of a task once the daemon picks it up
-- [Providers Matrix](/providers) — capability differences across the 10 AI coding tools
+- [Providers Matrix](/providers) — capability differences across the 11 AI coding tools
diff --git a/apps/docs/content/docs/daemon-runtimes.zh.mdx b/apps/docs/content/docs/daemon-runtimes.zh.mdx
index 7553b75ed..3de87d2a5 100644
--- a/apps/docs/content/docs/daemon-runtimes.zh.mdx
+++ b/apps/docs/content/docs/daemon-runtimes.zh.mdx
@@ -21,7 +21,7 @@ multica daemon start
启动后它会做四件事:
1. 读取你登录时保存的凭证
-2. 探测本机 `PATH` 上已安装的 AI 编程工具(内置支持 10 款:[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi))
+2. 探测本机 `PATH` 上已安装的 AI 编程工具(内置支持 11 款:[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi))
3. 向服务器注册自己,以及每款检测到的工具对应的运行时
4. 持续**每 3 秒轮询一次**是否有任务要领,**每 15 秒发一次心跳**
@@ -108,4 +108,4 @@ Multica 对并发有两层限额:
## 下一步
- [执行任务](/tasks) —— 守护进程领到任务后,它的完整生命周期
-- [Providers Matrix](/providers) —— 10 款 AI 编程工具的能力差异对照
+- [Providers Matrix](/providers) —— 11 款 AI 编程工具的能力差异对照
diff --git a/apps/docs/content/docs/how-multica-works.mdx b/apps/docs/content/docs/how-multica-works.mdx
index 413c84392..659c34ca2 100644
--- a/apps/docs/content/docs/how-multica-works.mdx
+++ b/apps/docs/content/docs/how-multica-works.mdx
@@ -13,7 +13,7 @@ Multica is a **distributed** platform. The web interface you see is just the fro
- **Multica server** — the workspaces, issue lists, and comment threads you see all live in its database. It's also a WebSocket hub that pushes real-time updates between you and your teammates. It does **not** execute any agent tasks.
- **Daemon** — part of the Multica CLI, running on your own machine. On start it detects which AI coding tools are installed locally, registers with the server, and begins polling for tasks every 3 seconds and sending heartbeats every 15 seconds.
-- **AI coding tools** — one of the ten (or several in parallel): [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Once the daemon has picked up a task, it uses these tools to actually do the work.
+- **AI coding tools** — one of the eleven (or several in parallel): [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Once the daemon has picked up a task, it uses these tools to actually do the work.
Because the toolchain stays local, **your API keys, code directories, and authorized tools** are only ever used on your machine — the Multica server never sees any of them. This holds whether you self-host or use Cloud.
diff --git a/apps/docs/content/docs/how-multica-works.zh.mdx b/apps/docs/content/docs/how-multica-works.zh.mdx
index 705889bfa..280eba860 100644
--- a/apps/docs/content/docs/how-multica-works.zh.mdx
+++ b/apps/docs/content/docs/how-multica-works.zh.mdx
@@ -13,7 +13,7 @@ Multica 是一个**分布式**平台。你看到的 Web 界面只是前台——
- **Multica 服务器**——你看到的工作区、issue 列表、评论线都存在它的数据库里。它同时是 WebSocket hub,把你和同事之间的实时更新推送过去。它**不**执行任何智能体任务。
- **守护进程**(daemon)——Multica CLI 的一部分,跑在你自己的机器上。启动后它探测本地装了哪些 AI 编程工具,注册到 server,开始每 3 秒领一次任务、每 15 秒发一次心跳。
-- **AI 编程工具**——[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi) 十款之一(或多款并存)。守护进程领到任务后,用这些工具真正去写代码。
+- **AI 编程工具**——[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi) 11 款之一(或多款并存)。守护进程领到任务后,用这些工具真正去写代码。
工具链在本地的结果:**你的 API 密钥、代码目录、已授权的工具**都只在本地使用;Multica 服务器一个都看不到。自部署还是用 Cloud 都不改变这一点。
diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx
index ad94c1800..ff2e9a731 100644
--- a/apps/docs/content/docs/index.mdx
+++ b/apps/docs/content/docs/index.mdx
@@ -13,7 +13,7 @@ This page explains where agents run and the ways you can start using Multica.
Agents do **not** execute tasks on Multica's servers. Multica currently supports one runtime model:
-- **Local [daemon](/daemon-runtimes)** — you run `multica daemon` on your own machine, and it drives the [AI coding tools](/providers) installed locally. Ten are built in today: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Your API keys, toolchain, and code directories stay on your machine.
+- **Local [daemon](/daemon-runtimes)** — you run `multica daemon` on your own machine, and it drives the [AI coding tools](/providers) installed locally. Eleven are built in today: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Your API keys, toolchain, and code directories stay on your machine.
**Cloud runtimes are coming**, currently waitlist-only. Once live, you won't need a local daemon — agent tasks will execute on Multica Cloud directly. Sign up on the [Downloads](https://multica.ai/download) page to get notified.
diff --git a/apps/docs/content/docs/index.zh.mdx b/apps/docs/content/docs/index.zh.mdx
index a298044df..a74ddc311 100644
--- a/apps/docs/content/docs/index.zh.mdx
+++ b/apps/docs/content/docs/index.zh.mdx
@@ -13,7 +13,7 @@ Multica 是一个任务协作平台,让人类和 AI [智能体](/agents) 在
智能体执行任务**不**发生在 Multica 服务器上。目前 Multica 支持一种运行方式:
-- **本地 [守护进程](/daemon-runtimes)** — 你在自己的机器上运行 `multica daemon`,由它调用本地安装的 [AI 编程工具](/providers)。目前内置十种:[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)。你的 API 密钥、工具链、代码目录都保留在本地。
+- **本地 [守护进程](/daemon-runtimes)** — 你在自己的机器上运行 `multica daemon`,由它调用本地安装的 [AI 编程工具](/providers)。目前内置 11 款:[Claude Code](/providers#claude-code)、[Codex](/providers#codex)、[Cursor](/providers#cursor)、[Copilot](/providers#copilot)、[Gemini](/providers#gemini)、[Hermes](/providers#hermes)、[Kimi](/providers#kimi)、[Kiro CLI](/providers#kiro-cli)、[OpenCode](/providers#opencode)、[OpenClaw](/providers#openclaw)、[Pi](/providers#pi)。你的 API 密钥、工具链、代码目录都保留在本地。
**云端运行时即将开放**,目前处于等待名单阶段。上线后,你无需在本地运行守护进程,即可在 Multica Cloud 上直接执行智能体任务。在 [下载页面](https://multica.ai/download) 登记邮箱以获取通知。
diff --git a/apps/docs/content/docs/providers.mdx b/apps/docs/content/docs/providers.mdx
index 811d159fb..a2ece008b 100644
--- a/apps/docs/content/docs/providers.mdx
+++ b/apps/docs/content/docs/providers.mdx
@@ -1,11 +1,11 @@
---
title: AI coding tools matrix
-description: Multica supports 10 AI coding tools; they implement the same interface, but the capability details diverge significantly.
+description: Multica supports 11 AI coding tools; they implement the same interface, but the capability details diverge significantly.
---
import { Callout } from "fumadocs-ui/components/callout";
-Multica ships with built-in support for **10 AI coding tools**. They all implement the same interface — queue, dispatch, execute, return results — so you can drive any of them from the same Multica board. **But the capability details diverge significantly**: whether session resumption actually works, whether MCP is supported, where skill files live, how models are selected. This page is the full matrix.
+Multica ships with built-in support for **11 AI coding tools**. They all implement the same interface — queue, dispatch, execute, return results — so you can drive any of them from the same Multica board. **But the capability details diverge significantly**: whether session resumption actually works, whether MCP is supported, where skill files live, how models are selected. This page is the full matrix.
For guidance on picking a tool when creating an agent, see [Creating and configuring agents](/agents-create).
@@ -20,6 +20,7 @@ For guidance on picking a tool when creating an agent, see [Creating and configu
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | Static |
| **Hermes** | Nous Research | ✅ | ❌ | `.agent_context/skills/` (fallback) | Dynamic discovery |
| **Kimi** | Moonshot | ✅ | ❌ | `.kimi/skills/` | Dynamic discovery |
+| **Kiro CLI** | Amazon | ✅ | ❌ | `.kiro/skills/` | Dynamic discovery |
| **OpenCode** | SST | ✅ | ❌ | `.config/opencode/skills/` | Dynamic discovery |
| **OpenClaw** | Open source | ✅ | ❌ | `.agent_context/skills/` (fallback) | Bound to the agent, can't be switched per task |
| **Pi** | Inflection AI | ✅ (session is a file path) | ❌ | `.pi/skills/` | Dynamic discovery |
@@ -28,7 +29,7 @@ For guidance on picking a tool when creating an agent, see [Creating and configu
### Claude Code
-From Anthropic. **First choice for new users** — the most complete feature set: session resumption actually works, it's the **only one of the 10 that truly reads MCP configuration**, and it supports fine-tuning flags like `--max-turns` and `--append-system-prompt`. Requires an Anthropic API key.
+From Anthropic. **First choice for new users** — the most complete feature set: session resumption actually works, it's the **only one of the 11 that truly reads MCP configuration**, and it supports fine-tuning flags like `--max-turns` and `--append-system-prompt`. Requires an Anthropic API key.
### Codex
@@ -54,6 +55,10 @@ From Nous Research. Uses the ACP protocol (shares a transport with Kimi). Sessio
From Moonshot, aimed at the Chinese market. Shares the ACP protocol with Hermes, but the skill path `.kimi/skills/` is Kimi CLI's native discovery mechanism — different from Hermes's fallback.
+### Kiro CLI
+
+From Amazon. Uses ACP over stdio via `kiro-cli acp`. Session resumption works through ACP `session/load`, model selection works through `session/set_model`, and skills are copied into `.kiro/skills/` for native project-level discovery.
+
### OpenCode
From SST, open source. Dynamically discovers available models (scans the CLI's configuration file). Session resumption works. **Suitable for tinkerers who want to customize their model catalog.**
@@ -72,7 +77,7 @@ The session resumption mechanism is covered in [Tasks](/tasks#can-a-task-continu
| Status | Tools | Meaning |
|---|---|---|
-| ✅ Really works | Claude Code, Copilot, Hermes, Kimi, OpenCode, OpenClaw, Pi | Pass the resume id and it continues from the previous context |
+| ✅ Really works | Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi | Pass the resume id and it continues from the previous context |
| ⚠️ Code exists but unreachable | Codex, Cursor | Resume paths exist in the code but aren't actually reached (Codex silently falls back; Cursor doesn't return session id) — **treat as unsupported** |
| ❌ None | Gemini | The CLI has no resume mechanism |
@@ -80,7 +85,7 @@ The session resumption mechanism is covered in [Tasks](/tasks#can-a-task-continu
## MCP configuration: only Claude Code actually reads it
-**Of the 10 tools, only Claude Code actually consumes `mcp_config`**. The other 9 accept the field but **completely ignore it** — no error, no warning, the config just has no effect.
+**Of the 11 tools, only Claude Code actually consumes `mcp_config`**. The other 10 accept the field but **completely ignore it** — no error, no warning, the config just has no effect.
If you set `mcp_config` in an agent configuration but pick a tool other than Claude Code, your MCP servers have **no effect** on that agent. MCP integration currently covers Claude Code only.
@@ -97,6 +102,7 @@ Each tool uses **its own** skill discovery path. Before a task runs, the Multica
| Copilot | `.github/skills/` | ✅ Native |
| Cursor | `.cursor/skills/` | ✅ Native |
| Kimi | `.kimi/skills/` | ✅ Native |
+| Kiro CLI | `.kiro/skills/` | ✅ Native |
| OpenCode | `.config/opencode/skills/` | ✅ Native |
| Pi | `.pi/skills/` | ✅ Native |
| Gemini | `.agent_context/skills/` | ⚠️ Generic fallback |
diff --git a/apps/docs/content/docs/providers.zh.mdx b/apps/docs/content/docs/providers.zh.mdx
index d230f2c7e..98b510589 100644
--- a/apps/docs/content/docs/providers.zh.mdx
+++ b/apps/docs/content/docs/providers.zh.mdx
@@ -1,11 +1,11 @@
---
title: AI 编程工具对照
-description: Multica 支持 10 款 AI 编程工具;它们实现同一套接口,但能力细节差异很大。
+description: Multica 支持 11 款 AI 编程工具;它们实现同一套接口,但能力细节差异很大。
---
import { Callout } from "fumadocs-ui/components/callout";
-Multica 内置支持 **10 款 AI 编程工具**。它们都实现了同一套接口——排队、派发、执行、结果回传,所以你可以从 Multica 的同一个看板上指挥任意一款。**但它们在能力细节上差异很大**:会话恢复是否真用、是否支持 MCP、skill 文件该放在哪里、模型怎么选。这一页是完整对照。
+Multica 内置支持 **11 款 AI 编程工具**。它们都实现了同一套接口——排队、派发、执行、结果回传,所以你可以从 Multica 的同一个看板上指挥任意一款。**但它们在能力细节上差异很大**:会话恢复是否真用、是否支持 MCP、skill 文件该放在哪里、模型怎么选。这一页是完整对照。
创建智能体时挑选工具的指引见 [创建和配置智能体](/agents-create)。
@@ -20,6 +20,7 @@ Multica 内置支持 **10 款 AI 编程工具**。它们都实现了同一套接
| **Gemini** | Google | ❌ | ❌ | `.agent_context/skills/` | 静态 |
| **Hermes** | Nous Research | ✅ | ❌ | `.agent_context/skills/` (fallback)| 动态发现 |
| **Kimi** | Moonshot | ✅ | ❌ | `.kimi/skills/` | 动态发现 |
+| **Kiro CLI** | Amazon | ✅ | ❌ | `.kiro/skills/` | 动态发现 |
| **OpenCode** | SST | ✅ | ❌ | `.config/opencode/skills/` | 动态发现 |
| **OpenClaw** | 开源项目 | ✅ | ❌ | `.agent_context/skills/` (fallback)| 绑定在智能体上,不能在任务里切换 |
| **Pi** | Inflection AI | ✅(session 为文件路径)| ❌ | `.pi/skills/` | 动态发现 |
@@ -28,7 +29,7 @@ Multica 内置支持 **10 款 AI 编程工具**。它们都实现了同一套接
### Claude Code
-Anthropic 出品。**新用户首选**——功能最完整:会话恢复真用,是 **10 款里唯一真读 MCP 配置**的工具,支持 `--max-turns`、`--append-system-prompt` 等细调参数。需要一个 Anthropic API 密钥。
+Anthropic 出品。**新用户首选**——功能最完整:会话恢复真用,是 **11 款里唯一真读 MCP 配置**的工具,支持 `--max-turns`、`--append-system-prompt` 等细调参数。需要一个 Anthropic API 密钥。
### Codex
@@ -54,6 +55,10 @@ Nous Research 出品。使用 ACP 协议(和 Kimi 共享传输层)。会话
Moonshot 出品,中国市场向。和 Hermes 共享 ACP 协议,但 skill 路径 `.kimi/skills/` 是 Kimi CLI 的原生发现机制——和 Hermes 的 fallback 不一样。
+### Kiro CLI
+
+Amazon 出品。通过 `kiro-cli acp` 使用 ACP stdio 协议。会话恢复走 ACP `session/load`,模型选择走 `session/set_model`,skill 会复制到 `.kiro/skills/` 让 Kiro 做项目级原生发现。
+
### OpenCode
SST 出品,开源。动态发现可用模型(扫 CLI 的配置文件)。会话恢复真用。**适合爱折腾、想自定义模型目录**的开发者。
@@ -72,7 +77,7 @@ Inflection AI 出品,极简主义。**会话恢复机制特殊**——session
| 状态 | 工具 | 含义 |
|---|---|---|
-| ✅ 真用 | Claude Code、Copilot、Hermes、Kimi、OpenCode、OpenClaw、Pi | 传 resume id,会从上次上下文接着继续 |
+| ✅ 真用 | Claude Code、Copilot、Hermes、Kimi、Kiro CLI、OpenCode、OpenClaw、Pi | 传 resume id,会从上次上下文接着继续 |
| ⚠️ 代码存在但不可达 | Codex、Cursor | 代码里有 resume 路径但实际走不到(Codex 静默回落、Cursor session id 不回传)—— **当作不支持** |
| ❌ 无 | Gemini | CLI 无 resume 机制 |
@@ -80,7 +85,7 @@ Inflection AI 出品,极简主义。**会话恢复机制特殊**——session
## MCP 配置:只有 Claude Code 真的读
-**10 款工具里只有 Claude Code 实际消费 `mcp_config`**。其他 9 款会接收这个字段但**完全忽略**——不报错、不警告,只是配置不生效。
+**11 款工具里只有 Claude Code 实际消费 `mcp_config`**。其他 10 款会接收这个字段但**完全忽略**——不报错、不警告,只是配置不生效。
如果你在智能体配置里设置了 `mcp_config`,但选了 Claude Code 之外的工具,你的 MCP server 对这个智能体**没有效果**。目前的 MCP 集成只覆盖 Claude Code。
@@ -97,6 +102,7 @@ Inflection AI 出品,极简主义。**会话恢复机制特殊**——session
| Copilot | `.github/skills/` | ✅ 原生 |
| Cursor | `.cursor/skills/` | ✅ 原生 |
| Kimi | `.kimi/skills/` | ✅ 原生 |
+| Kiro CLI | `.kiro/skills/` | ✅ 原生 |
| OpenCode | `.config/opencode/skills/` | ✅ 原生 |
| Pi | `.pi/skills/` | ✅ 原生 |
| Gemini | `.agent_context/skills/` | ⚠️ 通用 fallback |
diff --git a/apps/docs/content/docs/skills.mdx b/apps/docs/content/docs/skills.mdx
index 5d799d6cf..1971a56e9 100644
--- a/apps/docs/content/docs/skills.mdx
+++ b/apps/docs/content/docs/skills.mdx
@@ -64,4 +64,4 @@ By now you know what an agent is, how to create one, and how to attach skills. T
- [Daemon and runtimes](/daemon-runtimes) — where agents actually run, and how to tell online from offline
- [Executing tasks](/tasks) — the full lifecycle of one "agent work session"
-- [AI coding tools comparison](/providers) — full comparison of all 10 tools (including each one's skill injection path)
+- [AI coding tools comparison](/providers) — full comparison of all 11 tools (including each one's skill injection path)
diff --git a/apps/docs/content/docs/skills.zh.mdx b/apps/docs/content/docs/skills.zh.mdx
index ef516b85f..47578371a 100644
--- a/apps/docs/content/docs/skills.zh.mdx
+++ b/apps/docs/content/docs/skills.zh.mdx
@@ -64,4 +64,4 @@ Skill 导入后需要**挂载到具体的智能体**才会生效。一个智能
- [守护进程与运行时](/daemon-runtimes) —— 智能体到底跑在哪、怎么判断在线 / 离线
- [执行任务](/tasks) —— 一次"智能体工作"的完整生命周期
-- [AI 编程工具对照](/providers) —— 10 款工具的完整对比(含每款的 Skill 注入路径)
+- [AI 编程工具对照](/providers) —— 11 款工具的完整对比(含每款的 Skill 注入路径)
diff --git a/apps/docs/content/docs/tasks.mdx b/apps/docs/content/docs/tasks.mdx
index b10596ffc..5952e80f8 100644
--- a/apps/docs/content/docs/tasks.mdx
+++ b/apps/docs/content/docs/tasks.mdx
@@ -100,7 +100,7 @@ Multica pins the session ID **twice** during a task: once at the start (when the
But **which AI coding tools actually support this** varies a lot:
-- ✅ **Real support** — Claude Code, Copilot, Hermes, Kimi, OpenCode, OpenClaw, Pi
+- ✅ **Real support** — Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi
- ⚠️ **Code exists but unusable** — Codex, Cursor
- ❌ **No support** — Gemini
@@ -108,5 +108,5 @@ See [Providers Matrix → Session resumption](/providers#session-resumption-who-
## Next
-- [Providers Matrix](/providers) — capability differences across the 10 AI coding tools (including the exact session-resumption status)
+- [Providers Matrix](/providers) — capability differences across the 11 AI coding tools (including the exact session-resumption status)
- [Assigning issues to agents](/assigning-issues) / [@-mentioning agents in comments](/mentioning-agents) / [Chat](/chat) / [Autopilots](/autopilots) — the four ways to trigger a task
diff --git a/apps/docs/content/docs/tasks.zh.mdx b/apps/docs/content/docs/tasks.zh.mdx
index 916716d8c..f682ef5e0 100644
--- a/apps/docs/content/docs/tasks.zh.mdx
+++ b/apps/docs/content/docs/tasks.zh.mdx
@@ -100,7 +100,7 @@ Multica 在任务过程中**两次**保存会话 ID——任务一开始(AI
但**哪些 AI 编程工具真的支持**差别很大:
-- ✅ **真支持**——Claude Code、Copilot、Hermes、Kimi、OpenCode、OpenClaw、Pi
+- ✅ **真支持**——Claude Code、Copilot、Hermes、Kimi、Kiro CLI、OpenCode、OpenClaw、Pi
- ⚠️ **代码看起来支持但实际不可用**——Codex、Cursor
- ❌ **不支持**——Gemini
@@ -108,5 +108,5 @@ Multica 在任务过程中**两次**保存会话 ID——任务一开始(AI
## 下一步
-- [Providers Matrix](/providers) —— 10 款 AI 编程工具的能力差异对照(包括会话恢复的精确状态)
+- [Providers Matrix](/providers) —— 11 款 AI 编程工具的能力差异对照(包括会话恢复的精确状态)
- [分配 issue 给智能体](/assigning-issues) / [在评论里 @智能体](/mentioning-agents) / [聊天](/chat) / [Autopilots](/autopilots) —— 触发执行任务的四种方式
diff --git a/docs/product-overview.md b/docs/product-overview.md
index 2d9b9c232..8c85f0daf 100644
--- a/docs/product-overview.md
+++ b/docs/product-overview.md
@@ -82,7 +82,7 @@ Multica 做的事:
Multica **不自己训模型**,也不锁定某一家厂商。它是调度器,本地 daemon 会自动探测以下 CLI 工具并接入:
-Claude Code · Codex · OpenClaw · OpenCode · Hermes · Gemini · Pi · Cursor Agent
+Claude Code · Codex · OpenClaw · OpenCode · Hermes · Gemini · Pi · Cursor Agent · Kimi · Kiro CLI
每个 agent 可以配置自己的模型、API Key、环境变量、MCP 服务器。
@@ -244,7 +244,7 @@ Project 相比 Issue 是更高层的组织单元。一个 issue 可以不属于
#### 配置字段
- **基本信息**:名字、描述、头像(自动生成)
-- **Provider**:选择底层是 Claude / Codex / OpenClaw / OpenCode / Hermes / Gemini / Pi / Cursor 中的哪一个
+- **Provider**:选择底层是 Claude / Codex / OpenClaw / OpenCode / Hermes / Gemini / Pi / Cursor / Kimi / Kiro 中的哪一个
- **Runtime**:绑定到哪个运行时(即在哪台机器上跑)
- **Instructions 说明书**:agent 的系统提示词("你是一个资深工程师...")
- **Custom Env**:要注入到 CLI 进程的环境变量(如 `ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL`、`CLAUDE_CODE_USE_BEDROCK`)
@@ -291,7 +291,7 @@ Agent 是 Multica 的灵魂。几乎所有功能都围绕"如何让一个 agent
`multica` CLI 在用户的机器上启动一个后台进程(macOS launchd / Linux systemd / Windows 服务风格),它:
-1. **自动探测** `$PATH` 上安装的 coding CLI(`claude`, `codex`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, `cursor-agent`)
+1. **自动探测** `$PATH` 上安装的 coding CLI(`claude`, `codex`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, `cursor-agent`, `kimi`, `kiro-cli`)
2. 向 server **注册** 为一组 runtime(一个 CLI = 一个 runtime)
3. 每 3 秒 **轮询** 一次 server,有任务就认领
4. 每 15 秒 **心跳**(keepalive),报告自己还活着
diff --git a/packages/views/onboarding/steps/step-welcome.tsx b/packages/views/onboarding/steps/step-welcome.tsx
index 6df3dd102..d47013c55 100644
--- a/packages/views/onboarding/steps/step-welcome.tsx
+++ b/packages/views/onboarding/steps/step-welcome.tsx
@@ -265,6 +265,8 @@ type ProviderName =
| "opencode"
| "openclaw"
| "hermes"
+ | "kimi"
+ | "kiro"
| "pi"
| "copilot"
| "cursor";
diff --git a/packages/views/runtimes/components/provider-logo.tsx b/packages/views/runtimes/components/provider-logo.tsx
index ab449bd2b..364283b28 100644
--- a/packages/views/runtimes/components/provider-logo.tsx
+++ b/packages/views/runtimes/components/provider-logo.tsx
@@ -125,6 +125,17 @@ function KimiLogo({ className }: { className: string }) {
);
}
+// Kiro CLI — compact "K" mark for runtime rows.
+function KiroLogo({ className }: { className: string }) {
+ return (
+
+ );
+}
+
export function ProviderLogo({
provider,
className = "h-4 w-4",
@@ -151,6 +162,8 @@ export function ProviderLogo({
return ;
case "kimi":
return ;
+ case "kiro":
+ return ;
default:
return ;
}
diff --git a/server/internal/daemon/config.go b/server/internal/daemon/config.go
index 10c306a60..626c2874f 100644
--- a/server/internal/daemon/config.go
+++ b/server/internal/daemon/config.go
@@ -35,7 +35,7 @@ type Config struct {
CLIVersion string // multica CLI version (e.g. "0.1.13")
LaunchedBy string // "desktop" when spawned by the Electron app, empty for standalone
Profile string // profile name (empty = default)
- Agents map[string]AgentEntry // keyed by provider: claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor, kimi
+ Agents map[string]AgentEntry // keyed by provider: claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor, kimi, kiro
WorkspacesRoot string // base path for execution envs (default: ~/multica_workspaces)
KeepEnvAfterTask bool // preserve env after task for debugging
HealthPort int // local HTTP port for health checks (default: 19514)
@@ -152,8 +152,15 @@ func LoadConfig(overrides Overrides) (Config, error) {
Model: strings.TrimSpace(os.Getenv("MULTICA_KIMI_MODEL")),
}
}
+ kiroPath := envOrDefault("MULTICA_KIRO_PATH", "kiro-cli")
+ if _, err := exec.LookPath(kiroPath); err == nil {
+ agents["kiro"] = AgentEntry{
+ Path: kiroPath,
+ Model: strings.TrimSpace(os.Getenv("MULTICA_KIRO_MODEL")),
+ }
+ }
if len(agents) == 0 {
- return Config{}, fmt.Errorf("no agent CLI found: install claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor-agent, or kimi and ensure it is on PATH")
+ return Config{}, fmt.Errorf("no agent CLI found: install claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor-agent, kimi, or kiro-cli and ensure it is on PATH")
}
// Host info
diff --git a/server/internal/daemon/execenv/context.go b/server/internal/daemon/execenv/context.go
index be481ceef..cf397b424 100644
--- a/server/internal/daemon/execenv/context.go
+++ b/server/internal/daemon/execenv/context.go
@@ -18,6 +18,7 @@ import (
// Pi: skills → {workDir}/.pi/skills/{name}/SKILL.md (native discovery)
// Cursor: skills → {workDir}/.cursor/skills/{name}/SKILL.md (native discovery)
// Kimi: skills → {workDir}/.kimi/skills/{name}/SKILL.md (native discovery)
+// Kiro: skills → {workDir}/.kiro/skills/{name}/SKILL.md (native discovery)
// Default: skills → {workDir}/.agent_context/skills/{name}/SKILL.md
func writeContextFiles(workDir, provider string, ctx TaskContextForEnv) error {
contextDir := filepath.Join(workDir, ".agent_context")
@@ -74,6 +75,10 @@ func resolveSkillsDir(workDir, provider string) (string, error) {
// Kimi Code CLI auto-discovers project-level skills from .kimi/skills/
// in the workdir. See https://moonshotai.github.io/kimi-cli/en/customization/skills.html
skillsDir = filepath.Join(workDir, ".kimi", "skills")
+ case "kiro":
+ // Kiro CLI auto-discovers project-level skills from .kiro/skills/
+ // in the workdir.
+ skillsDir = filepath.Join(workDir, ".kiro", "skills")
default:
// Fallback: write to .agent_context/skills/ (referenced by meta config).
skillsDir = filepath.Join(workDir, ".agent_context", "skills")
diff --git a/server/internal/daemon/execenv/execenv.go b/server/internal/daemon/execenv/execenv.go
index c05e96d0c..a69877a58 100644
--- a/server/internal/daemon/execenv/execenv.go
+++ b/server/internal/daemon/execenv/execenv.go
@@ -24,7 +24,7 @@ type PrepareParams struct {
WorkspaceID string // workspace UUID — tasks are grouped under this
TaskID string // task UUID — used for directory name
AgentName string // for git branch naming only
- Provider string // agent provider ("claude", "codex") — determines skill injection paths
+ Provider string // agent provider (determines runtime config and skill injection paths)
CodexVersion string // detected Codex CLI version (only used when Provider == "codex")
Task TaskContextForEnv // context data for writing files
}
diff --git a/server/internal/daemon/execenv/execenv_test.go b/server/internal/daemon/execenv/execenv_test.go
index 9c56d80ce..515a73a27 100644
--- a/server/internal/daemon/execenv/execenv_test.go
+++ b/server/internal/daemon/execenv/execenv_test.go
@@ -619,6 +619,33 @@ func TestWriteContextFilesOpencodeNativeSkills(t *testing.T) {
}
}
+func TestWriteContextFilesKiroNativeSkills(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+
+ ctx := TaskContextForEnv{
+ IssueID: "kiro-skill-test",
+ AgentSkills: []SkillContextForEnv{
+ {Name: "Go Conventions", Content: "Follow Go conventions."},
+ },
+ }
+
+ if err := writeContextFiles(dir, "kiro", ctx); err != nil {
+ t.Fatalf("writeContextFiles failed: %v", err)
+ }
+
+ skillMd, err := os.ReadFile(filepath.Join(dir, ".kiro", "skills", "go-conventions", "SKILL.md"))
+ if err != nil {
+ t.Fatalf("failed to read .kiro/skills/go-conventions/SKILL.md: %v", err)
+ }
+ if !strings.Contains(string(skillMd), "Follow Go conventions.") {
+ t.Error("SKILL.md missing content")
+ }
+ if _, err := os.Stat(filepath.Join(dir, ".agent_context", "skills")); !os.IsNotExist(err) {
+ t.Error("expected .agent_context/skills/ to NOT exist for Kiro provider")
+ }
+}
+
func TestInjectRuntimeConfigOpencode(t *testing.T) {
t.Parallel()
dir := t.TempDir()
@@ -655,6 +682,36 @@ func TestInjectRuntimeConfigOpencode(t *testing.T) {
}
}
+func TestInjectRuntimeConfigKiro(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+
+ ctx := TaskContextForEnv{
+ IssueID: "test-issue-id",
+ AgentSkills: []SkillContextForEnv{{Name: "Coding", Content: "Write good code."}},
+ }
+
+ if err := InjectRuntimeConfig(dir, "kiro", ctx); err != nil {
+ t.Fatalf("InjectRuntimeConfig failed: %v", err)
+ }
+
+ content, err := os.ReadFile(filepath.Join(dir, "AGENTS.md"))
+ if err != nil {
+ t.Fatalf("failed to read AGENTS.md: %v", err)
+ }
+
+ s := string(content)
+ if !strings.Contains(s, "Multica Agent Runtime") {
+ t.Error("AGENTS.md missing meta skill header")
+ }
+ if !strings.Contains(s, "Coding") {
+ t.Error("AGENTS.md missing skill name")
+ }
+ if !strings.Contains(s, "discovered automatically") {
+ t.Error("AGENTS.md missing native skill discovery hint")
+ }
+}
+
func TestPrepareWithRepoContextOpencode(t *testing.T) {
t.Parallel()
workspacesRoot := t.TempDir()
diff --git a/server/internal/daemon/execenv/runtime_config.go b/server/internal/daemon/execenv/runtime_config.go
index 57c2422e4..38785dbfa 100644
--- a/server/internal/daemon/execenv/runtime_config.go
+++ b/server/internal/daemon/execenv/runtime_config.go
@@ -20,13 +20,14 @@ import (
// For Pi: writes {workDir}/AGENTS.md (skills discovered natively from .pi/skills/)
// For Cursor: writes {workDir}/AGENTS.md (skills discovered natively from .cursor/skills/)
// For Kimi: writes {workDir}/AGENTS.md (Kimi Code CLI reads AGENTS.md natively; skills auto-discovered from project skills dirs)
+// For Kiro: writes {workDir}/AGENTS.md (Kiro CLI reads AGENTS.md natively; skills auto-discovered from project skills dirs)
func InjectRuntimeConfig(workDir, provider string, ctx TaskContextForEnv) error {
content := buildMetaSkillContent(provider, ctx)
switch provider {
case "claude":
return os.WriteFile(filepath.Join(workDir, "CLAUDE.md"), []byte(content), 0o644)
- case "codex", "copilot", "opencode", "openclaw", "hermes", "pi", "cursor", "kimi":
+ case "codex", "copilot", "opencode", "openclaw", "hermes", "pi", "cursor", "kimi", "kiro":
return os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(content), 0o644)
case "gemini":
return os.WriteFile(filepath.Join(workDir, "GEMINI.md"), []byte(content), 0o644)
@@ -191,8 +192,8 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
case "claude":
// Claude discovers skills natively from .claude/skills/ — just list names.
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
- case "codex", "copilot", "opencode", "openclaw", "pi", "cursor", "kimi":
- // Codex, Copilot, OpenCode, OpenClaw, Pi, Cursor, and Kimi discover skills natively from their respective paths — just list names.
+ case "codex", "copilot", "opencode", "openclaw", "pi", "cursor", "kimi", "kiro":
+ // Codex, Copilot, OpenCode, OpenClaw, Pi, Cursor, Kimi, and Kiro discover skills natively from their respective paths — just list names.
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
case "gemini", "hermes":
// Gemini reads GEMINI.md directly; Hermes has no native skills discovery path
diff --git a/server/internal/daemon/local_skills.go b/server/internal/daemon/local_skills.go
index f51ee6501..ffb61f4b5 100644
--- a/server/internal/daemon/local_skills.go
+++ b/server/internal/daemon/local_skills.go
@@ -46,6 +46,7 @@ type runtimeLocalSkillBundle struct {
// - Pi: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md
// - Cursor: official forum guidance referencing the built-in /create-skill flow
// (https://forum.cursor.com/t/cursor-doesnt-know-new-skills-arens-saved/158507)
+// - Kiro: project and user-level .kiro/skills directories discovered by Kiro CLI
//
// Longer-term this mapping would be better colocated with the provider
// definitions under server/pkg/agent so adding a new runtime can't silently
@@ -75,6 +76,8 @@ func localSkillRootForProvider(provider string) (string, bool, error) {
return filepath.Join(home, ".pi", "agent", "skills"), true, nil
case "cursor":
return filepath.Join(home, ".cursor", "skills"), true, nil
+ case "kiro":
+ return filepath.Join(home, ".kiro", "skills"), true, nil
default:
return "", false, nil
}
diff --git a/server/internal/daemon/local_skills_test.go b/server/internal/daemon/local_skills_test.go
index 96988f117..6de9532f8 100644
--- a/server/internal/daemon/local_skills_test.go
+++ b/server/internal/daemon/local_skills_test.go
@@ -70,6 +70,35 @@ func TestListRuntimeLocalSkills_Claude(t *testing.T) {
}
}
+func TestListRuntimeLocalSkills_Kiro(t *testing.T) {
+ home := t.TempDir()
+ t.Setenv("HOME", home)
+
+ writeTestLocalSkill(t, filepath.Join(home, ".kiro", "skills"), "review-helper", map[string]string{
+ "SKILL.md": "---\nname: Kiro Review\ndescription: Review code with Kiro\n---\n# Kiro Review\n",
+ })
+
+ skills, supported, err := listRuntimeLocalSkills("kiro")
+ if err != nil {
+ t.Fatalf("listRuntimeLocalSkills: %v", err)
+ }
+ if !supported {
+ t.Fatal("kiro should be supported")
+ }
+ if len(skills) != 1 {
+ t.Fatalf("expected 1 skill, got %d", len(skills))
+ }
+ if skills[0].Key != "review-helper" {
+ t.Fatalf("key = %q, want review-helper", skills[0].Key)
+ }
+ if skills[0].Name != "Kiro Review" {
+ t.Fatalf("name = %q, want Kiro Review", skills[0].Name)
+ }
+ if skills[0].SourcePath != "~/.kiro/skills/review-helper" {
+ t.Fatalf("source_path = %q", skills[0].SourcePath)
+ }
+}
+
// Skill installers (for example lark-cli) place every skill at a shared
// location like ~/.agents/skills/ and symlink each one into the
// runtime root (~/.claude/skills/). The previous filepath.WalkDir
@@ -180,8 +209,8 @@ func TestListRuntimeLocalSkills_DescendsIntoNestedSkillDirs(t *testing.T) {
// Top-level skill — should register at key="top" and its child SKILL.md
// must NOT register as a separate skill.
writeTestLocalSkill(t, root, "top", map[string]string{
- "SKILL.md": "---\nname: Top\n---\n",
- "templates/SKILL.md": "not a real skill — sub-template that happens to share the filename",
+ "SKILL.md": "---\nname: Top\n---\n",
+ "templates/SKILL.md": "not a real skill — sub-template that happens to share the filename",
})
// Nested skill — only valid SKILL.md is at depth 2.
diff --git a/server/pkg/agent/agent.go b/server/pkg/agent/agent.go
index 4c4256c12..246fe1494 100644
--- a/server/pkg/agent/agent.go
+++ b/server/pkg/agent/agent.go
@@ -1,6 +1,6 @@
// Package agent provides a unified interface for executing prompts via
// coding agents (Claude Code, Codex, Copilot, OpenCode, OpenClaw, Hermes,
-// Gemini, Pi, Cursor, Kimi). It mirrors the happy-cli AgentBackend
+// Gemini, Pi, Cursor, Kimi, Kiro). It mirrors the happy-cli AgentBackend
// pattern, translated to idiomatic Go.
package agent
@@ -88,13 +88,13 @@ type Result struct {
// Config configures a Backend instance.
type Config struct {
- ExecutablePath string // path to CLI binary (claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor, kimi)
+ ExecutablePath string // path to CLI binary (claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor, kimi, kiro-cli)
Env map[string]string // extra environment variables
Logger *slog.Logger
}
// New creates a Backend for the given agent type.
-// Supported types: "claude", "codex", "copilot", "opencode", "openclaw", "hermes", "gemini", "pi", "cursor", "kimi".
+// Supported types: "claude", "codex", "copilot", "opencode", "openclaw", "hermes", "gemini", "pi", "cursor", "kimi", "kiro".
func New(agentType string, cfg Config) (Backend, error) {
if cfg.Logger == nil {
cfg.Logger = slog.Default()
@@ -121,8 +121,10 @@ func New(agentType string, cfg Config) (Backend, error) {
return &cursorBackend{cfg: cfg}, nil
case "kimi":
return &kimiBackend{cfg: cfg}, nil
+ case "kiro":
+ return &kiroBackend{cfg: cfg}, nil
default:
- return nil, fmt.Errorf("unknown agent type: %q (supported: claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor, kimi)", agentType)
+ return nil, fmt.Errorf("unknown agent type: %q (supported: claude, codex, copilot, opencode, openclaw, hermes, gemini, pi, cursor, kimi, kiro)", agentType)
}
}
@@ -148,6 +150,7 @@ var launchHeaders = map[string]string{
"opencode": "opencode run (json)",
"pi": "pi (json mode)",
"kimi": "kimi acp",
+ "kiro": "kiro-cli acp",
}
// LaunchHeader returns the user-visible launch skeleton for agentType, or an
diff --git a/server/pkg/agent/agent_test.go b/server/pkg/agent/agent_test.go
index ce28fa1ee..8324b35a7 100644
--- a/server/pkg/agent/agent_test.go
+++ b/server/pkg/agent/agent_test.go
@@ -72,7 +72,7 @@ func TestLaunchHeaderCoversAllSupportedBackends(t *testing.T) {
// entry to launchHeaders in agent.go and extend this list.
supported := []string{
"claude", "codex", "copilot", "cursor", "gemini",
- "hermes", "kimi", "openclaw", "opencode", "pi",
+ "hermes", "kimi", "kiro", "openclaw", "opencode", "pi",
}
for _, t_ := range supported {
if header := LaunchHeader(t_); header == "" {
diff --git a/server/pkg/agent/hermes.go b/server/pkg/agent/hermes.go
index cc60cf55c..c4c9193e4 100644
--- a/server/pkg/agent/hermes.go
+++ b/server/pkg/agent/hermes.go
@@ -587,7 +587,7 @@ func (c *hermesClient) handleNotification(raw map[string]json.RawMessage) {
var method string
_ = json.Unmarshal(raw["method"], &method)
- if method != "session/update" {
+ if method != "session/update" && method != "session/notification" {
return
}
@@ -602,23 +602,66 @@ func (c *hermesClient) handleNotification(raw map[string]json.RawMessage) {
return
}
- // Parse the update discriminator.
+ updateType, updateData := normalizeACPUpdate(params.Update)
+
+ switch updateType {
+ case "agent_message_chunk":
+ c.handleAgentMessage(updateData)
+ case "agent_thought_chunk":
+ c.handleAgentThought(updateData)
+ case "tool_call":
+ c.handleToolCallStart(updateData)
+ case "tool_call_update":
+ c.handleToolCallUpdate(updateData)
+ case "usage_update":
+ c.handleUsageUpdate(updateData)
+ case "turn_end":
+ c.extractPromptResult(updateData)
+ }
+}
+
+func normalizeACPUpdate(data json.RawMessage) (string, json.RawMessage) {
var updateType struct {
SessionUpdate string `json:"sessionUpdate"`
+ Type string `json:"type"`
+ }
+ _ = json.Unmarshal(data, &updateType)
+ if updateType.SessionUpdate != "" {
+ return normalizeACPUpdateType(updateType.SessionUpdate), data
+ }
+ if updateType.Type != "" {
+ return normalizeACPUpdateType(updateType.Type), data
}
- _ = json.Unmarshal(params.Update, &updateType)
- switch updateType.SessionUpdate {
- case "agent_message_chunk":
- c.handleAgentMessage(params.Update)
- case "agent_thought_chunk":
- c.handleAgentThought(params.Update)
- case "tool_call":
- c.handleToolCallStart(params.Update)
- case "tool_call_update":
- c.handleToolCallUpdate(params.Update)
- case "usage_update":
- c.handleUsageUpdate(params.Update)
+ // Some ACP implementations serialize enum variants as an externally
+ // tagged object: {"agentMessageChunk": {"content": ...}}.
+ var wrapper map[string]json.RawMessage
+ if err := json.Unmarshal(data, &wrapper); err == nil && len(wrapper) == 1 {
+ for k, v := range wrapper {
+ return normalizeACPUpdateType(k), v
+ }
+ }
+
+ return "", data
+}
+
+func normalizeACPUpdateType(t string) string {
+ key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(t), "_", ""), "-", ""))
+ switch key {
+ case "agentmessagechunk":
+ return "agent_message_chunk"
+ case "agentthoughtchunk":
+ return "agent_thought_chunk"
+ case "toolcall":
+ return "tool_call"
+ case "toolcallupdate":
+ return "tool_call_update"
+ case "usageupdate":
+ return "usage_update"
+ case "turnend", "endturn":
+ return "turn_end"
+ default:
+ return ""
}
}
@@ -655,9 +698,12 @@ func (c *hermesClient) handleAgentThought(data json.RawMessage) {
func (c *hermesClient) handleToolCallStart(data json.RawMessage) {
var msg struct {
ToolCallID string `json:"toolCallId"`
+ Name string `json:"name"`
Title string `json:"title"`
Kind string `json:"kind"`
RawInput map[string]any `json:"rawInput"`
+ Input map[string]any `json:"input"`
+ Parameters map[string]any `json:"parameters"`
Content []json.RawMessage `json:"content"`
}
if err := json.Unmarshal(data, &msg); err != nil {
@@ -665,15 +711,25 @@ func (c *hermesClient) handleToolCallStart(data json.RawMessage) {
}
toolName := hermesToolNameFromTitle(msg.Title, msg.Kind)
+ if toolName == "" {
+ toolName = msg.Name
+ }
+ rawInput := msg.RawInput
+ if rawInput == nil {
+ rawInput = msg.Input
+ }
+ if rawInput == nil {
+ rawInput = msg.Parameters
+ }
// Hermes pre-populates rawInput on the initial tool_call — emit
// MessageToolUse immediately so the UI can show the tool invocation
// live. Record the emission so handleToolCallUpdate doesn't re-emit
// on completion.
- if msg.RawInput != nil {
+ if rawInput != nil {
c.trackTool(msg.ToolCallID, &pendingToolCall{
toolName: toolName,
- input: msg.RawInput,
+ input: rawInput,
emitted: true,
})
if c.onMessage != nil {
@@ -681,7 +737,7 @@ func (c *hermesClient) handleToolCallStart(data json.RawMessage) {
Type: MessageToolUse,
Tool: toolName,
CallID: msg.ToolCallID,
- Input: msg.RawInput,
+ Input: rawInput,
})
}
return
@@ -702,16 +758,32 @@ func (c *hermesClient) handleToolCallUpdate(data json.RawMessage) {
var msg struct {
ToolCallID string `json:"toolCallId"`
Status string `json:"status"`
+ Name string `json:"name"`
Title string `json:"title"`
Kind string `json:"kind"`
RawInput map[string]any `json:"rawInput"`
+ Input map[string]any `json:"input"`
+ Parameters map[string]any `json:"parameters"`
RawOutput string `json:"rawOutput"`
+ Output string `json:"output"`
Content []json.RawMessage `json:"content"`
}
if err := json.Unmarshal(data, &msg); err != nil {
return
}
+ rawInput := msg.RawInput
+ if rawInput == nil {
+ rawInput = msg.Input
+ }
+ if rawInput == nil {
+ rawInput = msg.Parameters
+ }
+ title := msg.Title
+ if title == "" {
+ title = msg.Name
+ }
+
// Mid-stream: only buffer updates. Kimi emits many of these per
// tool call, each carrying the cumulative args JSON so far.
if msg.Status != "completed" && msg.Status != "failed" {
@@ -727,9 +799,12 @@ func (c *hermesClient) handleToolCallUpdate(data json.RawMessage) {
// Completion: emit any deferred MessageToolUse first, then the result.
pending := c.takePendingTool(msg.ToolCallID)
- c.emitDeferredToolUse(pending, msg.ToolCallID, msg.Title, msg.Kind, msg.RawInput)
+ c.emitDeferredToolUse(pending, msg.ToolCallID, title, msg.Kind, rawInput)
output := msg.RawOutput
+ if output == "" {
+ output = msg.Output
+ }
if output == "" {
output = extractACPToolCallText(msg.Content)
}
@@ -961,7 +1036,7 @@ func (c *hermesClient) handleUsageUpdate(data json.RawMessage) {
// ── Helpers ──
// extractACPSessionID pulls `sessionId` out of a session/new or
-// session/resume response. Shared by all ACP backends (hermes, kimi,
+// session/resume response. Shared by all ACP backends (hermes, kimi, kiro,
// and anything else that follows the standard ACP schema).
func extractACPSessionID(result json.RawMessage) string {
var r struct {
diff --git a/server/pkg/agent/hermes_test.go b/server/pkg/agent/hermes_test.go
index 852b63b5f..5476fd19e 100644
--- a/server/pkg/agent/hermes_test.go
+++ b/server/pkg/agent/hermes_test.go
@@ -292,6 +292,28 @@ func TestHermesClientHandleAgentMessage(t *testing.T) {
}
}
+func TestHermesClientHandleSessionNotificationAgentMessage(t *testing.T) {
+ t.Parallel()
+
+ var got Message
+ c := &hermesClient{
+ pending: make(map[int]*pendingRPC),
+ onMessage: func(msg Message) {
+ got = msg
+ },
+ }
+
+ line := `{"jsonrpc":"2.0","method":"session/notification","params":{"sessionId":"ses_1","update":{"type":"AgentMessageChunk","content":{"type":"text","text":"Hello from Kiro"}}}}`
+ c.handleLine(line)
+
+ if got.Type != MessageText {
+ t.Errorf("type: got %v, want MessageText", got.Type)
+ }
+ if got.Content != "Hello from Kiro" {
+ t.Errorf("content: got %q, want %q", got.Content, "Hello from Kiro")
+ }
+}
+
func TestHermesClientHandleAgentThought(t *testing.T) {
t.Parallel()
@@ -342,6 +364,62 @@ func TestHermesClientHandleToolCallStart(t *testing.T) {
}
}
+func TestHermesClientHandleSessionNotificationToolCall(t *testing.T) {
+ t.Parallel()
+
+ var got []Message
+ c := &hermesClient{
+ pending: make(map[int]*pendingRPC),
+ onMessage: func(msg Message) {
+ got = append(got, msg)
+ },
+ }
+
+ c.handleLine(`{"jsonrpc":"2.0","method":"session/notification","params":{"sessionId":"ses_1","update":{"type":"ToolCall","toolCallId":"tc-kiro","name":"Shell","status":"pending","parameters":{"command":"pwd"}}}}`)
+ c.handleLine(`{"jsonrpc":"2.0","method":"session/notification","params":{"sessionId":"ses_1","update":{"type":"ToolCallUpdate","toolCallId":"tc-kiro","status":"completed","name":"Shell","output":"/tmp/project\n"}}}`)
+
+ if len(got) != 2 {
+ t.Fatalf("expected [ToolUse, ToolResult], got %+v", got)
+ }
+ if got[0].Type != MessageToolUse {
+ t.Errorf("first message: got %v, want MessageToolUse", got[0].Type)
+ }
+ if got[0].Tool != "Shell" {
+ t.Errorf("first tool: got %q, want Shell", got[0].Tool)
+ }
+ if cmd, _ := got[0].Input["command"].(string); cmd != "pwd" {
+ t.Errorf("first input.command: got %v, want pwd", got[0].Input["command"])
+ }
+ if got[1].Type != MessageToolResult {
+ t.Errorf("second message: got %v, want MessageToolResult", got[1].Type)
+ }
+ if got[1].Output != "/tmp/project\n" {
+ t.Errorf("second output: got %q", got[1].Output)
+ }
+}
+
+func TestHermesClientHandleSessionNotificationTurnEnd(t *testing.T) {
+ t.Parallel()
+
+ var got hermesPromptResult
+ c := &hermesClient{
+ pending: make(map[int]*pendingRPC),
+ onPromptDone: func(result hermesPromptResult) {
+ got = result
+ },
+ }
+
+ line := `{"jsonrpc":"2.0","method":"session/notification","params":{"sessionId":"ses_1","update":{"type":"TurnEnd","stopReason":"end_turn","usage":{"inputTokens":3,"outputTokens":4,"cachedReadTokens":1}}}}`
+ c.handleLine(line)
+
+ if got.stopReason != "end_turn" {
+ t.Errorf("stopReason: got %q, want end_turn", got.stopReason)
+ }
+ if got.usage.InputTokens != 3 || got.usage.OutputTokens != 4 || got.usage.CacheReadTokens != 1 {
+ t.Errorf("usage: got %+v", got.usage)
+ }
+}
+
func TestHermesClientHandleToolCallComplete(t *testing.T) {
t.Parallel()
diff --git a/server/pkg/agent/kiro.go b/server/pkg/agent/kiro.go
new file mode 100644
index 000000000..f279f5064
--- /dev/null
+++ b/server/pkg/agent/kiro.go
@@ -0,0 +1,340 @@
+package agent
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+ "os/exec"
+ "strings"
+ "sync"
+ "time"
+)
+
+// kiroBlockedArgs are flags hardcoded by the daemon that must not be
+// overridden by user-configured custom_args. `acp` is the protocol subcommand,
+// and --trust-all-tools covers Kiro's CLI-level tool gate while
+// hermesClient handles ACP session/request_permission auto-approval. In Kiro
+// CLI 2.1.1, `-a` is short for --trust-all-tools, not --agent; --agent remains
+// allowed so users can select a custom Kiro agent.
+var kiroBlockedArgs = map[string]blockedArgMode{
+ "acp": blockedStandalone,
+ "-a": blockedStandalone,
+ "--trust-all-tools": blockedStandalone,
+ "--trust-tools": blockedWithValue,
+}
+
+// kiroBackend implements Backend by spawning `kiro-cli acp` and communicating
+// via the standard ACP JSON-RPC 2.0 transport over stdin/stdout.
+//
+// Kiro CLI advertises loadSession, returns models from session/new, and supports
+// session/set_model, so the existing Hermes/Kimi ACP client can drive it with
+// only provider-specific launch and tool-name normalization.
+type kiroBackend struct {
+ cfg Config
+}
+
+func (b *kiroBackend) Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error) {
+ execPath := b.cfg.ExecutablePath
+ if execPath == "" {
+ execPath = "kiro-cli"
+ }
+ if _, err := exec.LookPath(execPath); err != nil {
+ return nil, fmt.Errorf("kiro executable not found at %q: %w", execPath, err)
+ }
+
+ timeout := opts.Timeout
+ if timeout == 0 {
+ timeout = 20 * time.Minute
+ }
+ runCtx, cancel := context.WithTimeout(ctx, timeout)
+
+ kiroArgs := append([]string{"acp", "--trust-all-tools"}, filterCustomArgs(opts.CustomArgs, kiroBlockedArgs, b.cfg.Logger)...)
+ cmd := exec.CommandContext(runCtx, execPath, kiroArgs...)
+ hideAgentWindow(cmd)
+ b.cfg.Logger.Info("agent command", "exec", execPath, "args", kiroArgs)
+ if opts.Cwd != "" {
+ cmd.Dir = opts.Cwd
+ }
+ cmd.Env = buildEnv(b.cfg.Env)
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ cancel()
+ return nil, fmt.Errorf("kiro stdout pipe: %w", err)
+ }
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ cancel()
+ return nil, fmt.Errorf("kiro stdin pipe: %w", err)
+ }
+ providerErr := newACPProviderErrorSniffer("kiro")
+ cmd.Stderr = io.MultiWriter(newLogWriter(b.cfg.Logger, "[kiro:stderr] "), providerErr)
+
+ if err := cmd.Start(); err != nil {
+ cancel()
+ return nil, fmt.Errorf("start kiro: %w", err)
+ }
+
+ b.cfg.Logger.Info("kiro acp started", "pid", cmd.Process.Pid, "cwd", opts.Cwd)
+
+ msgCh := make(chan Message, 256)
+ resCh := make(chan Result, 1)
+
+ var outputMu sync.Mutex
+ var output strings.Builder
+
+ promptDone := make(chan hermesPromptResult, 1)
+
+ c := &hermesClient{
+ cfg: b.cfg,
+ stdin: stdin,
+ pending: make(map[int]*pendingRPC),
+ pendingTools: make(map[string]*pendingToolCall),
+ onMessage: func(msg Message) {
+ if msg.Type == MessageToolUse {
+ msg.Tool = kiroToolNameFromTitle(msg.Tool)
+ }
+ if msg.Type == MessageText {
+ outputMu.Lock()
+ output.WriteString(msg.Content)
+ outputMu.Unlock()
+ }
+ trySend(msgCh, msg)
+ },
+ onPromptDone: func(result hermesPromptResult) {
+ select {
+ case promptDone <- result:
+ default:
+ }
+ },
+ }
+
+ readerDone := make(chan struct{})
+ go func() {
+ defer close(readerDone)
+ scanner := bufio.NewScanner(stdout)
+ scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+ c.handleLine(line)
+ }
+ c.closeAllPending(fmt.Errorf("kiro process exited"))
+ }()
+
+ go func() {
+ defer cancel()
+ defer close(msgCh)
+ defer close(resCh)
+ defer func() {
+ stdin.Close()
+ _ = cmd.Wait()
+ }()
+
+ startTime := time.Now()
+ finalStatus := "completed"
+ var finalError string
+ var sessionID string
+
+ _, err := c.request(runCtx, "initialize", map[string]any{
+ "protocolVersion": 1,
+ "clientInfo": map[string]any{
+ "name": "multica-agent-sdk",
+ "version": "0.2.0",
+ },
+ "clientCapabilities": map[string]any{},
+ })
+ if err != nil {
+ finalStatus = "failed"
+ finalError = fmt.Sprintf("kiro initialize failed: %v", err)
+ resCh <- Result{Status: finalStatus, Error: finalError, DurationMs: time.Since(startTime).Milliseconds()}
+ return
+ }
+
+ cwd := opts.Cwd
+ if cwd == "" {
+ cwd = "."
+ }
+
+ if opts.ResumeSessionID != "" {
+ _, err := c.request(runCtx, "session/load", map[string]any{
+ "cwd": cwd,
+ "sessionId": opts.ResumeSessionID,
+ "mcpServers": []any{},
+ })
+ if err != nil {
+ finalStatus = "failed"
+ finalError = fmt.Sprintf("kiro session/load failed: %v", err)
+ resCh <- Result{Status: finalStatus, Error: finalError, DurationMs: time.Since(startTime).Milliseconds()}
+ return
+ }
+ sessionID = opts.ResumeSessionID
+ } else {
+ result, err := c.request(runCtx, "session/new", map[string]any{
+ "cwd": cwd,
+ "mcpServers": []any{},
+ })
+ if err != nil {
+ finalStatus = "failed"
+ finalError = fmt.Sprintf("kiro session/new failed: %v", err)
+ resCh <- Result{Status: finalStatus, Error: finalError, DurationMs: time.Since(startTime).Milliseconds()}
+ return
+ }
+ sessionID = extractACPSessionID(result)
+ if sessionID == "" {
+ finalStatus = "failed"
+ finalError = "kiro session/new returned no session ID"
+ resCh <- Result{Status: finalStatus, Error: finalError, DurationMs: time.Since(startTime).Milliseconds()}
+ return
+ }
+ }
+
+ c.sessionID = sessionID
+ b.cfg.Logger.Info("kiro session created", "session_id", sessionID)
+
+ if opts.Model != "" {
+ if _, err := c.request(runCtx, "session/set_model", map[string]any{
+ "sessionId": sessionID,
+ "modelId": opts.Model,
+ }); err != nil {
+ b.cfg.Logger.Warn("kiro set_session_model failed", "error", err, "requested_model", opts.Model)
+ finalStatus = "failed"
+ finalError = fmt.Sprintf("kiro could not switch to model %q: %v", opts.Model, err)
+ resCh <- Result{
+ Status: finalStatus,
+ Error: finalError,
+ DurationMs: time.Since(startTime).Milliseconds(),
+ SessionID: sessionID,
+ }
+ return
+ }
+ b.cfg.Logger.Info("kiro session model set", "model", opts.Model)
+ }
+
+ userText := prompt
+ if opts.SystemPrompt != "" {
+ userText = opts.SystemPrompt + "\n\n---\n\n" + prompt
+ }
+
+ promptBlocks := []map[string]any{
+ {"type": "text", "text": userText},
+ }
+ // Kiro's published docs use `content`, while Kiro CLI 2.1.1 still
+ // requires the standard ACP `prompt` field. Send both so either wire
+ // shape can drive the turn.
+ // TODO: drop one field once Kiro lands on a single canonical payload.
+ _, err = c.request(runCtx, "session/prompt", map[string]any{
+ "sessionId": sessionID,
+ "content": promptBlocks,
+ "prompt": promptBlocks,
+ })
+ if err != nil {
+ if runCtx.Err() == context.DeadlineExceeded {
+ finalStatus = "timeout"
+ finalError = fmt.Sprintf("kiro timed out after %s", timeout)
+ } else if runCtx.Err() == context.Canceled {
+ finalStatus = "aborted"
+ finalError = "execution cancelled"
+ } else {
+ finalStatus = "failed"
+ finalError = fmt.Sprintf("kiro session/prompt failed: %v", err)
+ }
+ } else {
+ select {
+ case pr := <-promptDone:
+ if pr.stopReason == "cancelled" {
+ finalStatus = "aborted"
+ finalError = "kiro cancelled the prompt"
+ }
+ c.usageMu.Lock()
+ c.usage.InputTokens += pr.usage.InputTokens
+ c.usage.OutputTokens += pr.usage.OutputTokens
+ c.usageMu.Unlock()
+ default:
+ }
+ }
+
+ duration := time.Since(startTime)
+ b.cfg.Logger.Info("kiro finished", "pid", cmd.Process.Pid, "status", finalStatus, "duration", duration.Round(time.Millisecond).String())
+
+ stdin.Close()
+ cancel()
+
+ <-readerDone
+
+ outputMu.Lock()
+ finalOutput := output.String()
+ outputMu.Unlock()
+
+ if finalStatus == "completed" && finalOutput == "" {
+ if msg := providerErr.message(); msg != "" {
+ finalStatus = "failed"
+ finalError = msg
+ }
+ }
+
+ c.usageMu.Lock()
+ u := c.usage
+ c.usageMu.Unlock()
+
+ var usageMap map[string]TokenUsage
+ if u.InputTokens > 0 || u.OutputTokens > 0 || u.CacheReadTokens > 0 {
+ model := opts.Model
+ if model == "" {
+ model = "unknown"
+ }
+ usageMap = map[string]TokenUsage{model: u}
+ }
+
+ resCh <- Result{
+ Status: finalStatus,
+ Output: finalOutput,
+ Error: finalError,
+ DurationMs: duration.Milliseconds(),
+ SessionID: sessionID,
+ Usage: usageMap,
+ }
+ }()
+
+ return &Session{Messages: msgCh, Result: resCh}, nil
+}
+
+func kiroToolNameFromTitle(title string) string {
+ t := strings.TrimSpace(title)
+ if t == "" {
+ return ""
+ }
+
+ if idx := strings.Index(t, ":"); idx > 0 {
+ t = strings.TrimSpace(t[:idx])
+ }
+
+ lower := strings.ToLower(t)
+ switch lower {
+ case "read", "read file":
+ return "read_file"
+ case "write", "write file":
+ return "write_file"
+ case "edit", "patch":
+ return "edit_file"
+ case "shell", "bash", "terminal", "run command", "run shell command":
+ return "terminal"
+ case "grep", "search", "find":
+ return "search_files"
+ case "glob":
+ return "glob"
+ case "code":
+ return "code"
+ case "web search":
+ return "web_search"
+ case "fetch", "web fetch":
+ return "web_fetch"
+ case "todo", "todo write", "todo list", "todo_list":
+ return "todo_write"
+ }
+
+ return strings.ReplaceAll(lower, " ", "_")
+}
diff --git a/server/pkg/agent/kiro_test.go b/server/pkg/agent/kiro_test.go
new file mode 100644
index 000000000..acf7915f4
--- /dev/null
+++ b/server/pkg/agent/kiro_test.go
@@ -0,0 +1,284 @@
+package agent
+
+import (
+ "context"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestNewReturnsKiroBackend(t *testing.T) {
+ t.Parallel()
+ b, err := New("kiro", Config{ExecutablePath: "/nonexistent/kiro-cli"})
+ if err != nil {
+ t.Fatalf("New(kiro) error: %v", err)
+ }
+ if _, ok := b.(*kiroBackend); !ok {
+ t.Fatalf("expected *kiroBackend, got %T", b)
+ }
+}
+
+func TestKiroToolNameFromTitle(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ title string
+ want string
+ }{
+ {"Read file: /tmp/foo.go", "read_file"},
+ {"Write: /tmp/bar.go", "write_file"},
+ {"Patch: /tmp/x", "edit_file"},
+ {"Shell: ls -la", "terminal"},
+ {"Run command: pwd", "terminal"},
+ {"grep", "search_files"},
+ {"Glob: *.go", "glob"},
+ {"Code", "code"},
+ {"Todo List", "todo_write"},
+ {"Custom Thing", "custom_thing"},
+ {"", ""},
+ }
+ for _, tt := range tests {
+ got := kiroToolNameFromTitle(tt.title)
+ if got != tt.want {
+ t.Errorf("kiroToolNameFromTitle(%q) = %q, want %q", tt.title, got, tt.want)
+ }
+ }
+}
+
+func fakeKiroACPScript() string {
+ return `#!/bin/sh
+if [ -n "$KIRO_ARGS_FILE" ]; then
+ for arg in "$@"; do
+ printf '%s\n' "$arg" >> "$KIRO_ARGS_FILE"
+ done
+fi
+while IFS= read -r line; do
+ if [ -n "$KIRO_REQUESTS_FILE" ]; then
+ printf '%s\n' "$line" >> "$KIRO_REQUESTS_FILE"
+ fi
+ id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\).*/\1/p')
+ case "$line" in
+ *'"method":"initialize"'*)
+ printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true}}}\n' "$id"
+ ;;
+ *'"method":"session/new"'*)
+ printf '{"jsonrpc":"2.0","id":%s,"result":{"sessionId":"ses_new","models":{"currentModelId":"auto","availableModels":[{"modelId":"auto","name":"auto"}]}}}\n' "$id"
+ ;;
+ *'"method":"session/load"'*)
+ printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id"
+ ;;
+ *'"method":"session/resume"'*)
+ printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"session/resume should not be used for kiro"}}\n' "$id"
+ ;;
+ *'"method":"session/set_model"'*)
+ case "$line" in
+ *bogus-model*)
+ printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32602,"message":"model not available: bogus-model"}}\n' "$id"
+ exit 0
+ ;;
+ *)
+ printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id"
+ ;;
+ esac
+ ;;
+ *'"method":"session/prompt"'*)
+ case "$line" in
+ *'"content":'*)
+ ;;
+ *)
+ printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32602,"message":"session/prompt must send content and prompt"}}\n' "$id"
+ exit 0
+ ;;
+ esac
+ case "$line" in
+ *'"prompt":'*)
+ ;;
+ *)
+ printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32602,"message":"session/prompt must send content and prompt"}}\n' "$id"
+ exit 0
+ ;;
+ esac
+ printf '{"jsonrpc":"2.0","method":"session/notification","params":{"sessionId":"ses_loaded","update":{"type":"AgentMessageChunk","content":{"type":"text","text":"loaded"}}}}\n'
+ printf '{"jsonrpc":"2.0","id":%s,"result":{"stopReason":"end_turn","usage":{"inputTokens":2,"outputTokens":1}}}\n' "$id"
+ exit 0
+ ;;
+ esac
+done
+`
+}
+
+func TestKiroBackendSetModelFailureFailsTask(t *testing.T) {
+ t.Parallel()
+
+ fakePath := filepath.Join(t.TempDir(), "kiro-cli")
+ writeTestExecutable(t, fakePath, []byte(fakeKiroACPScript()))
+
+ backend, err := New("kiro", Config{ExecutablePath: fakePath, Logger: slog.Default()})
+ if err != nil {
+ t.Fatalf("new kiro backend: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ session, err := backend.Execute(ctx, "prompt-ignored", ExecOptions{
+ Model: "bogus-model",
+ Timeout: 5 * time.Second,
+ })
+ if err != nil {
+ t.Fatalf("execute: %v", err)
+ }
+ go func() {
+ for range session.Messages {
+ }
+ }()
+
+ select {
+ case result, ok := <-session.Result:
+ if !ok {
+ t.Fatal("result channel closed without a value")
+ }
+ if result.Status != "failed" {
+ t.Fatalf("expected status=failed, got %q (error=%q)", result.Status, result.Error)
+ }
+ if !strings.Contains(result.Error, `could not switch to model "bogus-model"`) {
+ t.Errorf("expected error to name the requested model, got %q", result.Error)
+ }
+ if !strings.Contains(result.Error, "model not available") {
+ t.Errorf("expected error to surface upstream message, got %q", result.Error)
+ }
+ if result.SessionID != "ses_new" {
+ t.Errorf("expected session id to be preserved on failure, got %q", result.SessionID)
+ }
+ case <-time.After(10 * time.Second):
+ t.Fatal("timeout waiting for result")
+ }
+}
+
+func TestKiroBackendInvokesACPWithTrustAllTools(t *testing.T) {
+ t.Parallel()
+
+ tempDir := t.TempDir()
+ argsFile := filepath.Join(tempDir, "argv.txt")
+ fakePath := filepath.Join(tempDir, "kiro-cli")
+ writeTestExecutable(t, fakePath, []byte(fakeKiroACPScript()))
+
+ backend, err := New("kiro", Config{
+ ExecutablePath: fakePath,
+ Logger: slog.Default(),
+ Env: map[string]string{"KIRO_ARGS_FILE": argsFile},
+ })
+ if err != nil {
+ t.Fatalf("new kiro backend: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ session, err := backend.Execute(ctx, "prompt-ignored", ExecOptions{
+ Model: "bogus-model",
+ Timeout: 5 * time.Second,
+ CustomArgs: []string{"acp", "--trust-tools", "shell", "-a", "--agent", "multica"},
+ })
+ if err != nil {
+ t.Fatalf("execute: %v", err)
+ }
+ go func() {
+ for range session.Messages {
+ }
+ }()
+ <-session.Result
+
+ raw, err := os.ReadFile(argsFile)
+ if err != nil {
+ t.Fatalf("read args file: %v", err)
+ }
+ lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
+ wantPrefix := []string{"acp", "--trust-all-tools"}
+ if len(lines) < len(wantPrefix) {
+ t.Fatalf("expected at least %d args, got %d: %q", len(wantPrefix), len(lines), lines)
+ }
+ for i, want := range wantPrefix {
+ if lines[i] != want {
+ t.Fatalf("arg[%d] = %q, want %q (full: %q)", i, lines[i], want, lines)
+ }
+ }
+ for _, blocked := range []string{"--trust-tools", "shell", "-a"} {
+ for _, got := range lines {
+ if got == blocked {
+ t.Errorf("protocol-critical custom arg %q was not filtered: %q", blocked, lines)
+ }
+ }
+ }
+ if strings.Join(lines, "\n") != strings.Join([]string{"acp", "--trust-all-tools", "--agent", "multica"}, "\n") {
+ t.Errorf("unexpected argv after filtering: %q", lines)
+ }
+}
+
+func TestKiroBackendUsesSessionLoadForResume(t *testing.T) {
+ t.Parallel()
+
+ tempDir := t.TempDir()
+ requestsFile := filepath.Join(tempDir, "requests.jsonl")
+ fakePath := filepath.Join(tempDir, "kiro-cli")
+ writeTestExecutable(t, fakePath, []byte(fakeKiroACPScript()))
+
+ backend, err := New("kiro", Config{
+ ExecutablePath: fakePath,
+ Logger: slog.Default(),
+ Env: map[string]string{"KIRO_REQUESTS_FILE": requestsFile},
+ })
+ if err != nil {
+ t.Fatalf("new kiro backend: %v", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ session, err := backend.Execute(ctx, "continue", ExecOptions{
+ ResumeSessionID: "ses_existing",
+ Timeout: 5 * time.Second,
+ })
+ if err != nil {
+ t.Fatalf("execute: %v", err)
+ }
+ go func() {
+ for range session.Messages {
+ }
+ }()
+
+ result := <-session.Result
+ if result.Status != "completed" {
+ t.Fatalf("expected completed result, got status=%q error=%q", result.Status, result.Error)
+ }
+ if result.Output != "loaded" {
+ t.Fatalf("output = %q, want loaded", result.Output)
+ }
+ if result.SessionID != "ses_existing" {
+ t.Fatalf("session id = %q, want ses_existing", result.SessionID)
+ }
+
+ raw, err := os.ReadFile(requestsFile)
+ if err != nil {
+ t.Fatalf("read requests file: %v", err)
+ }
+ requests := string(raw)
+ if !strings.Contains(requests, `"method":"session/load"`) {
+ t.Fatalf("expected session/load request, got:\n%s", requests)
+ }
+ if strings.Contains(requests, `"method":"session/resume"`) {
+ t.Fatalf("kiro backend must not call session/resume, got:\n%s", requests)
+ }
+ if !strings.Contains(requests, `"mcpServers":[]`) {
+ t.Fatalf("session/load must include mcpServers, got:\n%s", requests)
+ }
+ // Kiro docs use content, but Kiro CLI 2.1.1 still requires prompt.
+ if !strings.Contains(requests, `"content":[`) {
+ t.Fatalf("session/prompt must send Kiro content field, got:\n%s", requests)
+ }
+ if !strings.Contains(requests, `"prompt":[`) {
+ t.Fatalf("session/prompt must send standard ACP prompt field for Kiro 2.1.1 compatibility, got:\n%s", requests)
+ }
+}
diff --git a/server/pkg/agent/models.go b/server/pkg/agent/models.go
index 0a4b8857b..9d5b80516 100644
--- a/server/pkg/agent/models.go
+++ b/server/pkg/agent/models.go
@@ -75,6 +75,10 @@ func ListModels(ctx context.Context, providerType, executablePath string) ([]Mod
return cachedDiscovery(providerType, func() ([]Model, error) {
return discoverKimiModels(ctx, executablePath)
})
+ case "kiro":
+ return cachedDiscovery(providerType, func() ([]Model, error) {
+ return discoverKiroModels(ctx, executablePath)
+ })
case "opencode":
return cachedDiscovery(providerType, func() ([]Model, error) {
return discoverOpenCodeModels(ctx, executablePath)
@@ -341,10 +345,10 @@ func parsePiModels(output string) []Model {
// creatable manual-entry input instead of blocking the form.
func discoverHermesModels(ctx context.Context, executablePath string) ([]Model, error) {
return discoverACPModels(ctx, executablePath, acpDiscoveryProvider{
- defaultBin: "hermes",
- clientName: "multica-model-discovery",
- extraEnv: []string{"HERMES_YOLO_MODE=1"},
- tmpdirPrefix: "multica-hermes-discovery-",
+ defaultBin: "hermes",
+ clientName: "multica-model-discovery",
+ extraEnv: []string{"HERMES_YOLO_MODE=1"},
+ tmpdirPrefix: "multica-hermes-discovery-",
})
}
@@ -364,6 +368,16 @@ func discoverKimiModels(ctx context.Context, executablePath string) ([]Model, er
})
}
+// discoverKiroModels spins up a throwaway `kiro-cli acp` process and parses
+// the models block Kiro returns from session/new.
+func discoverKiroModels(ctx context.Context, executablePath string) ([]Model, error) {
+ return discoverACPModels(ctx, executablePath, acpDiscoveryProvider{
+ defaultBin: "kiro-cli",
+ clientName: "multica-model-discovery",
+ tmpdirPrefix: "multica-kiro-discovery-",
+ })
+}
+
// acpDiscoveryProvider configures how discoverACPModels launches an
// ACP-speaking agent CLI. The shared helper drives every CLI in
// the same way (initialize → session/new → parse models block) — the
diff --git a/server/pkg/agent/models_test.go b/server/pkg/agent/models_test.go
index ac397183e..78d32c4bb 100644
--- a/server/pkg/agent/models_test.go
+++ b/server/pkg/agent/models_test.go
@@ -78,6 +78,21 @@ func TestListModelsHermesWithoutBinary(t *testing.T) {
}
}
+func TestListModelsKiroWithoutBinary(t *testing.T) {
+ ctx := context.Background()
+ modelCacheMu.Lock()
+ delete(modelCache, "kiro")
+ modelCacheMu.Unlock()
+
+ got, err := ListModels(ctx, "kiro", "/nonexistent/kiro-cli")
+ if err != nil {
+ t.Fatalf("ListModels(kiro) error: %v", err)
+ }
+ if got == nil {
+ t.Error("expected non-nil slice even when binary is missing")
+ }
+}
+
func TestListModelsUnknownProvider(t *testing.T) {
ctx := context.Background()
_, err := ListModels(ctx, "nonexistent", "")