Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a81655a
feat(claude-code): Claude Code as an iii worker
rohitg00 Jun 11, 2026
890c8c1
feat(claude-code): pure API pass-through, raw claude::events stream, …
rohitg00 Jun 11, 2026
a119e2d
refactor(claude-code): drop the MCP bus bridge
rohitg00 Jun 11, 2026
efcc073
docs(claude-code): drop MCP framing from README and skill
rohitg00 Jun 11, 2026
7043e55
docs(claude-code): rewrite skill as a usage doc, drop internal jargon
rohitg00 Jun 11, 2026
8a71900
test(claude-code): cover the registered surface, not just helpers
rohitg00 Jun 11, 2026
ad78bbd
docs(claude-code): iii trigger CLI examples and the two-session-id di…
rohitg00 Jun 11, 2026
722319e
docs(claude-code): CLI screenshots for run, status, and help output
rohitg00 Jun 11, 2026
8884fd6
feat(claude-code): publish request schemas for every function
rohitg00 Jun 11, 2026
b837d44
docs(claude-code): refresh help screenshot with the published schema …
rohitg00 Jun 11, 2026
fb1112c
docs(claude-code): observability section with console trace explorer …
rohitg00 Jun 11, 2026
52f1730
fix(claude-code): address review findings
rohitg00 Jun 11, 2026
83f02e5
docs(claude-code): plan mode and permission modes section
rohitg00 Jun 12, 2026
14f9bb1
docs(claude-code): plan-then-execute recipe in the skill
rohitg00 Jun 12, 2026
881a77c
feat(claude-code): engine-grounded iii runtime context, on by default
rohitg00 Jun 12, 2026
9b025ea
docs(claude-code): engine-generic discovery example
rohitg00 Jun 12, 2026
81f03d8
test(claude-code): iii context stacking and config-level disable
rohitg00 Jun 12, 2026
9afc51c
fix(claude-code): harden approval gate, background error status, usag…
rohitg00 Jun 12, 2026
ff3b353
Merge remote-tracking branch 'origin/main' into claude-code-worker
rohitg00 Jun 12, 2026
85fb220
fix(claude-code): guard concurrent same-session runs, order working-save
rohitg00 Jun 12, 2026
5d55990
chore(claude-code): add iii-permissions.yaml per new-worker SOP
rohitg00 Jun 15, 2026
4e5b390
feat(claude-code): manage config via the configuration worker
rohitg00 Jun 15, 2026
44c67b8
Merge remote-tracking branch 'origin/main' into claude-code-worker
rohitg00 Jun 15, 2026
a99e8a0
chore(claude-code): release wiring + resilient config boot
rohitg00 Jun 15, 2026
35fa431
fix(claude-code): atomic live-run reserve + always release the slot
rohitg00 Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/create-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
type: choice
options:
- acp
- claude-code
- coder
- console
- database
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
tags:
- 'acp/v*'
- 'claude-code/v*'
- 'coder/v*'
- 'console/v*'
- 'database/v*'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ npx skills add iii-hq/iii --all
|---|---|---|
| [`acp`](acp/) | Rust | Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions. |
| [`harness`](harness/) | Node | TS port of the iii harness stack — bundles `harness` (provider registry + credentials/settings/permissions via the `configuration` worker), `turn-orchestrator`, `approval-gate`, `hook-fanout`, `models-catalog`, the `provider-*` workers, `llm-budget`, and `context-compaction` as one pnpm monorepo. Conversations persist in `session-manager`. See [`harness/README.md`](harness/README.md). |
| [`claude-code`](claude-code/) | Node | Claude Code as an iii worker — `claude::*` runs headless Claude Code turns, mirrors raw messages onto `claude::events`, and streams AgentEvent frames onto `agent::events`. |
| [`session-manager`](session-manager/) | Rust | Durable, reactive, branching conversation store — fourteen `session::*` functions plus six trigger types; the transcript backend for `harness` and `console`. See [`session-manager/architecture/`](session-manager/architecture/). |
| [`database`](database/) | Rust | PostgreSQL, MySQL, and SQLite client — query, execute, transactions, prepared statements, and change feeds. |
| [`iii-directory`](iii-directory/) | Rust | Engine introspection (functions / triggers / workers), workers-registry proxy, and filesystem-backed skill + prompt reader. |
Expand Down
1 change: 1 addition & 0 deletions claude-code/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.tsbuildinfo
187 changes: 187 additions & 0 deletions claude-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# claude-code

Claude Code as an iii worker: the Claude Code API exposed as functions and streams on the iii bus, nothing else. The worker spawns the same `claude` binary the user runs in their terminal, with the same login, the same filesystem, and the same tools (file edits, shell, web). `claude::run` executes one headless turn and returns the result; the raw Claude Code messages mirror verbatim onto the `claude::events` stream, and a translated AgentEvent view lands on `agent::events`, so the iii console, the acp worker, and any sibling worker observe a Claude Code run exactly like a native harness turn. The worker also registers `run::start_and_wait`, the same entrypoint the console and the acp worker drive, so both run Claude Code with no changes.

## Install

```bash
iii worker add claude-code
```

Requires the `claude` CLI on the host (the Agent SDK shells out to it) and either `ANTHROPIC_API_KEY` in the worker environment or an existing `claude` login.

## Skills

Install the `claude-code` agent skill for Claude Code, Cursor, and 30+ other agents:

```bash
npx skills add iii-hq/workers --skill claude-code
```

## Quickstart

From zero to a Claude Code turn over the bus:

```bash
curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
iii worker add claude-code
iii # starts the engine + worker
```

Then talk to it like any other function: from the console chat, from `iii trigger claude::run`, or from any SDK:

```ts
import { registerWorker } from 'iii-sdk';

const iii = registerWorker('ws://127.0.0.1:49134', { workerName: 'demo' });

const res = await iii.trigger({
function_id: 'claude::run',
payload: {
prompt: 'Add a /health endpoint to server.ts and run the tests',
cwd: '/path/to/repo',
permission_mode: 'acceptEdits',
},
timeout_ms: 600_000,
});
// { session_id, claude_session_id, result, stop_reason, usage, total_cost_usd }
```

Or straight from the terminal with the `iii trigger` CLI:

```bash
# one full turn (raise the timeout; the default 30s is too short for agent turns)
iii trigger claude::run --timeout-ms 600000 \
--json '{"prompt":"add a /health endpoint and run the tests","cwd":"/path/to/repo"}'

# quick reads use key=value syntax
iii trigger claude::sessions::list
iii trigger claude::status session_id=<session_id>

# background turn + control
iii trigger claude::start --json '{"prompt":"...","cwd":"/path/to/repo"}'
iii trigger claude::stop session_id=<session_id>

# ask the running engine for a function's description
iii trigger claude::run --help
```

A turn from the CLI and the session record it leaves behind:

![iii trigger claude::run returning the result with usage and cost](assets/cli-run.png)

![iii trigger claude::status showing the stored session record](assets/cli-status.png)

![iii trigger claude::run --help printing the published request schema as a parameter table](assets/cli-help.png)

Call `claude::run` again with the returned `session_id` to continue the same conversation: the worker maps iii session ids to Claude Code session ids in engine state and resumes automatically.

Two ids come back from every run. `session_id` is the iii session id: the key for `claude::status`, `claude::stop`, resume, and the stream group. `claude_session_id` is Claude Code's internal session id (what the worker passes to the CLI's resume under the hood) — returned for reference, not a lookup key.

Long turns: use `claude::start` to return immediately, then watch `agent::events` (group_id = your session_id) for `message_complete`, `function_execution_start/end`, and `turn_end` frames. `claude::stop` interrupts a live run, `claude::status` reads a point-in-time view, `claude::sessions::list` enumerates past sessions.

## Functions

| Function | Purpose |
| --- | --- |
| `claude::run` | Run one turn, wait, return the final result |
| `claude::start` | Fire-and-forget turn; progress arrives on `agent::events` |
| `claude::stop` | Interrupt a live run |
| `claude::status` | Session state, live flag, usage, cost |
| `claude::sessions::list` | All sessions this worker has run |
| `run::start_and_wait` | Alias for `claude::run` under the entrypoint the console and acp worker drive |

`claude::run` accepts either a bare `prompt` string or a `messages` array (`[{ role: 'user', content: [{ type: 'text', text }] }]`), plus `model`, `cwd`, `system_prompt`, `append_system_prompt`, `permission_mode`, `allowed_tools`, `disallowed_tools`, and `max_turns` overrides.

### Raw API pass-through

The named fields above cover the common path; everything else the Agent SDK accepts goes through the `options` field untouched (camelCase, exactly as in the SDK):

```jsonc
{
"prompt": "...",
"options": {
"forkSession": true,
"includePartialMessages": true,
"fallbackModel": "claude-sonnet-4-6",
"addDirs": ["/another/repo"]
}
}
```

And the full output side is available raw: every message Claude Code emits (`system/init`, `assistant`, `user`, `result`, and `stream_event` token deltas when `includePartialMessages` is set) is mirrored verbatim onto the `claude::events` stream, group_id = session_id. Consumers that want the exact Claude Code wire format read `claude::events`; consumers that want harness-shaped frames read `agent::events`. Same turn, two views.

## The agent on the bus

By default every turn's system prompt carries the iii runtime context: the same engine-grounded rules as the harness identity prompts, retargeted to the `iii` CLI the agent reaches through its shell. The agent discovers capabilities from the live engine instead of memory — `iii trigger engine::functions::list` to find function ids, `iii trigger <fn> --help` as the contract before every first call, the registry flow (`directory::registry::workers::list/info`, `worker::add`) when nothing registered fits — plus the calling rules and error-handling discipline that go with them. The matching `Bash(iii *)` allow rule is added automatically so those calls run headless. Local file edits stay on Claude Code's native tools; backend actions go through registered functions.

```bash
# the agent answers this by querying the live engine itself
iii trigger claude::run --timeout-ms 300000 \
--json '{"prompt":"List every worker connected to this engine and what each one does.","cwd":"/tmp"}'
```

Turn it off per call with `"iii_context": false` or globally in `config.yaml`; a caller-supplied `system_prompt` always wins verbatim and gets nothing appended.

## Plan mode and permission modes

`permission_mode` maps straight onto Claude Code's native modes, per turn:

| Mode | Behavior |
| --- | --- |
| `default` | Claude Code's standard permission prompts (headless: unapproved calls fail) |
| `acceptEdits` | File edits auto-approved; the worker default |
| `plan` | Native plan mode: read-only exploration, produces a plan, refuses edits |
| `bypassPermissions` | Skip all permission checks |

Plan mode headless behaves like plan mode in the terminal: the turn ends when Claude finishes the plan, and the plan text is the `result` — nothing executes. Because the worker resumes sessions, plan-then-execute is two calls against the same `session_id`:

```bash
# 1. plan (read-only)
iii trigger claude::run --timeout-ms 600000 \
--json '{"prompt":"Plan how to add rate limiting to the REST API. Do not implement.","cwd":"/path/to/repo","permission_mode":"plan"}'

# 2. execute the plan with full context, same conversation
iii trigger claude::run --timeout-ms 600000 \
--json '{"session_id":"<from-step-1>","prompt":"Implement the plan.","permission_mode":"acceptEdits","cwd":"/path/to/repo"}'
```

The approval step is whatever sits between the two calls — a human reading the plan, another worker, or a trigger.

## Configuration

```yaml
engine_url: ws://127.0.0.1:49134

defaults:
model: "" # empty = Claude Code default
permission_mode: acceptEdits # default | acceptEdits | plan | bypassPermissions
max_turns: 50
cwd: "" # default working directory for runs

approval_gate: false # route tool permissions through policy::check_permissions
events_stream: agent::events # translated AgentEvent frames
raw_events_stream: claude::events # verbatim Claude Code messages
claude_executable: "" # path to the claude CLI; empty = SDK default resolution
```

With `approval_gate: true` and the harness worker installed, every Claude Code tool call is checked against `policy::check_permissions` before it executes, fail-closed when the gate is unreachable, so the same YAML permission rules and console approval flow that govern native harness turns govern Claude Code.

## Observability

Every `claude::run` is an ordinary traced invocation on the engine: the trace carries the full input payload (prompt, cwd, caller worker id) and the output (result, stop reason, token usage, cost) as span events, with per-function p50/p95/p99 in the console's trace explorer — no extra instrumentation in the worker.

![claude::run invocations in the iii console trace explorer, with input and output payloads](assets/console-traces.png)

## How it maps

| Claude Code | iii |
| --- | --- |
| SDK `query()` turn | `claude::run` invocation |
| every SDK message, verbatim | `claude::events` stream frame |
| assistant message | `message_complete` frame on `agent::events` |
| tool_use / tool_result | `function_execution_start` / `function_execution_end` frames |
| final result | `turn_end` + `agent_end` frames, function return value |
| session resume | engine state scope `claude_sessions`, keyed by iii session_id |
| permission prompt | `canUseTool` -> `policy::check_permissions` (optional) |
| extra capability | another iii worker on the bus (`shell`, `database`, `storage`, ...) |
Binary file added claude-code/assets/cli-help.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added claude-code/assets/cli-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added claude-code/assets/cli-status.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added claude-code/assets/console-traces.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions claude-code/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"root": false,
"vcs": { "enabled": false, "clientKind": "git" },
"files": {
"ignoreUnknown": false,
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "off"
}
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "warn"
},
"style": {
"useImportType": "off",
"useNodejsImportProtocol": "error"
},
"complexity": {
"noForEach": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
}
}
34 changes: 34 additions & 0 deletions claude-code/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
engine_url: ws://127.0.0.1:49134

defaults:
model: ""
permission_mode: acceptEdits
max_turns: 50
cwd: ""
append_system_prompt: ""
allowed_tools: []
disallowed_tools: []

# Route Claude Code permission prompts through policy::check_permissions
# (harness approval gate). Fail-closed when the gate is unreachable.
# Requires the harness worker; leave false for standalone use.
approval_gate: false

# Stream AgentEvent frames here, grouped by session_id. The console and
# acp worker both read this stream.
events_stream: agent::events

# Raw Claude Code messages (exact Agent SDK shapes: system/init, assistant,
# user, result, stream_event) mirrored verbatim here, grouped by session_id.
raw_events_stream: claude::events

# Append the iii runtime context to the system prompt: teaches the agent
# live discovery against the engine catalog through the iii CLI
# (engine::functions::list, `iii trigger <fn> --help`). Per-turn override
# via the iii_context payload field; a caller-supplied system_prompt
# always wins and gets nothing appended.
iii_context: true

# Path to the Claude Code CLI binary. Empty = Agent SDK default resolution;
# set to an absolute path (or "claude") when running the single-file bundle.
claude_executable: ""
15 changes: 15 additions & 0 deletions claude-code/iii-permissions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Agent permissions for the claude-code worker.
# Spec: docs/sops/new-worker.md § 7. First-match-wins.
#
# claude::run / claude::start (and the run::start_and_wait alias) spawn a full
# Claude Code agent with the host's filesystem and shell — an agent invoking
# those without human approval is a privilege escalation, so they are NOT
# allow-listed and stay at the needs_approval default. Read-only introspection
# is safe to allow.
version: 1

rules:
# internal config-reload callback — bus-internal, never agent-callable
- '!claude::on-config-change'
- claude::status
- claude::sessions::list
17 changes: 17 additions & 0 deletions claude-code/iii.worker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
iii: v1
name: claude-code
language: javascript
deploy: bundle
manifest: package.json
description: Claude Code as an iii worker — claude::* functions run headless Claude Code turns, mirror raw messages onto claude::events, and stream AgentEvent frames onto agent::events.

runtime:
kind: javascript

scripts:
install: npm install && npm run build
start: node dist/index.js

dependencies:
iii-state: "^0.17.0"
iii-stream: "^0.17.0"
39 changes: 39 additions & 0 deletions claude-code/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "claude-code",
"version": "0.1.0",
"private": true,
"description": "Claude Code as an iii worker: headless turns over the iii bus, raw messages on claude::events, AgentEvent frames on agent::events.",
"license": "Apache-2.0",
"type": "module",
"engines": {
"node": ">=22"
},
"packageManager": "pnpm@10.18.2",
"scripts": {
"build": "tsc -b",
"build:bundle": "node scripts/build-bundle.mjs",
"typecheck": "tsc -b --noEmit",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"test": "vitest run",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"bin": {
"claude-code": "./dist/index.js"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.173",
"iii-sdk": "^0.19.2",
"yaml": "^2.6.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.10",
"@types/node": "^22.10.0",
"esbuild": "^0.25.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}
Loading
Loading