Skip to content

feat(agents): wire Kaiten Agents into columns - #275

Merged
ANonABento merged 1 commit into
mainfrom
agents-in-columns
Aug 21, 2026
Merged

feat(agents): wire Kaiten Agents into columns#275
ANonABento merged 1 commit into
mainfrom
agents-in-columns

Conversation

@ANonABento

Copy link
Copy Markdown
Owner

The Roster shipped agents you could craft but never run. Now a column can run one.

A spawn_cli trigger carries agent_id. When set the agent supplies the CLI, instructions, tools and its preferred model, and the column may override model only — the rule the Kaiten Agents spec locked in, enforced in pipeline::spawn::resolve() and nowhere else.

task > trigger > agent > workspace > global > default

A leftover cli token on the column is logged and ignored rather than obeyed: the roster tile has to tell the truth about what runs. The UI says the same thing by removing controls it can't honour — the CLI token disappears from the automation sentence once an agent is attached, and Advanced swaps its dropdown for "Set by <agent>".

The one real design decision: instructions ship as a file

No flag spans the three runtimes, so I checked each rather than assuming:

runtime system-prompt flag
codex none at all (0.145.0)
claude --append-system-prompt is last-wins, not cumulative — verified against 2.1.239: two flags, only the second applied
script no prompt concept

That second one is the trap. Interactive mode already spends --append-system-prompt on the done-sentinel and appends it after user args, so routing an agent's instructions through it would have silently dropped them in exactly the columns most likely to use an agent — the same class of bug as the codex flag that killed every sentinel-carrying column before.

So instructions and skills render to .agent.md in the working dir and the prompt gains a newline-free pointer to it — the convention .task.md already established. One mechanism across 3 runtimes × 3 runtime modes, no flag-existence assumptions, and switching a column between headless and interactive cannot change what the agent was told. Mode is a rendering and billing choice; it has no business altering behaviour.

Verified end-to-end against a real claude -p: given an .agent.md saying "begin every reply with BENTO-OK", the reply began with BENTO-OK.

A stale .agent.md is removed when a column has no agent — worktrees are reused across columns, so leaving it would let one column's agent keep instructing the next.

Script agents run too

Same tmux transport, so they get a live terminal, exit-code completion and an agent card. Two things they need that LLM agents don't:

  • They bypass validate_agent_cli_path. That allow-list guards hand-editable trigger JSON; a script agent's command comes from an agents row authored in the Roster UI — the same trust level run_script already grants user-authored scripts. Flagging explicitly because it is me widening a security boundary.
  • They get the prompt via $TRIGGER_PROMPT, never argv. The generic command builder appends the prompt as a positional argument, which would break ./render.sh --preset high on the extra word. They're also forced to terminal mode, since managed parses an LLM event stream and interactive drives a TUI.

Deleting an agent that columns use

get_agent_usage sweeps every workspace — agents are global, so scoping to the open board would miss most attachments. The dossier lists the columns; the delete confirmation names them.

Deleting is still allowed, not blocked: forbidding it would leave no way to remove an agent attached to a workspace you no longer have open. A fired trigger for a missing agent fails loudly by name instead of quietly falling back to a bare CLI, which would look like it still worked.

The seam stayed shut

Runtime still has no execute. roster::plan::plan_for() builds spawn parameters, resolve() applies precedence, neither launches anything — so there is still exactly one copy of the cli/model/cwd/prompt rules.

Not in this slice

MCP + /api/* tools for agents, RAG, typed I/O, Orchestrator-as-section, per-task agent override. Listed under "Still v2" in the spec.

Checks

cargo clippy -D warnings · cargo test 557 (+13) · tsc · eslint · test:ipc · test:type-scale · vitest 444 (+10) · vite build · pnpm audit.

Visually verified in browser-mock: picker lists agents plus a bare-CLI escape hatch, CLI token vanishes on attach, script agents show neither CLI nor model.

A `spawn_cli` trigger now carries `agent_id`. When set, the agent supplies
the CLI, instructions, tools and its preferred model, and the column may
override model only — the rule the Kaiten Agents spec locked in, enforced
in `pipeline::spawn::resolve()` and nowhere else. Precedence reads
task > trigger > agent > workspace > global > default.

A leftover `cli` token on the column is logged and ignored rather than
obeyed: the roster tile has to tell the truth about what runs. The UI says
the same thing by removing controls it can't honour — the CLI token
disappears from the automation sentence once an agent is attached, and the
Advanced editor swaps its dropdown for "Set by <agent>".

Instructions ship as `.agent.md`, not a system-prompt flag.

No flag spans the three runtimes. codex has none. claude's
`--append-system-prompt` is last-wins rather than cumulative — verified
against 2.1.239, two flags and only the second applied — and interactive
mode already spends it on the done-sentinel, appending it after user args.
So routing instructions through it would silently drop them in exactly the
columns most likely to use an agent. Scripts have no prompt concept at all.

So instructions and skills render to a file in the working dir and the
prompt gains a newline-free pointer to it, the convention `.task.md`
already established. One mechanism across three runtimes and three runtime
modes, no flag-existence assumptions, and switching a column between
headless and interactive cannot change what the agent was told — mode is a
rendering and billing choice and has no business altering behaviour.
Verified end-to-end against a real `claude -p`. A stale `.agent.md` is
removed when a column has no agent, since worktrees are reused and it would
otherwise keep instructing the next column.

Script agents run too, through the same tmux transport, so they get a live
terminal, exit-code completion and an agent card. They bypass
`validate_agent_cli_path`: that allow-list guards hand-editable trigger
JSON, and a script agent's command comes from an `agents` row authored in
the Roster UI — the same trust level `run_script` already grants
user-authored scripts. They are forced to `terminal` mode, since managed
parses an LLM event stream and interactive drives a TUI, and they receive
the prompt via `$TRIGGER_PROMPT` rather than argv — the generic command
builder appends the prompt as a positional argument, which would break
`./render.sh --preset high` on the extra word.

`get_agent_usage` sweeps every workspace for columns running an agent;
agents are global, so scoping it to the open board would miss most of them.
The dossier lists them and the delete confirmation names them. Deleting is
still allowed — blocking it would leave no way to remove an agent attached
to a workspace you no longer have open — and a fired trigger for a missing
agent fails loudly by name instead of quietly falling back to a bare CLI.

The `Runtime` trait still has no `execute`. `roster::plan::plan_for()`
builds spawn parameters; resolve applies precedence; neither launches
anything.

44 new tests (13 Rust, 10 frontend, plus the plan/precedence coverage).
Renames the action clause from "run an AI agent" to "run an agent", since
it now covers script agents too.
@ANonABento
ANonABento merged commit 81c921d into main Aug 21, 2026
3 checks passed
@ANonABento
ANonABento deleted the agents-in-columns branch August 21, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant