Skip to content
12 changes: 7 additions & 5 deletions .agents/skills/harness-adapters/references/harness/kimi.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kimi Code

Verified on 2026-07-25 with Kimi Code CLI 0.29.1.
Verified on 2026-09-17 with Kimi Code CLI 2.0.0.

## Operating facts

Expand All @@ -13,16 +13,18 @@ Verified on 2026-07-25 with Kimi Code CLI 0.29.1.
| Exit command | `/exit`. |
| Interrupt | Single Escape, which prints `Interrupted by user`. |
| Skill invocation | `/<skill>`, for example `/no-mistakes`; Firstmate skills are discovered. |
| Autonomy | `--auto`; `-y` and `--yolo` are weaker and are not used. |
| Trust dialog | None observed on a clean first launch in a fresh pooled worktree. |
| Autonomy | `--auto` is the `Never Ask` tier; `-y` and `--yolo` now select the distinct, weaker `Ask When Needed` tier and are not used. |
| Trust dialog | A fresh worktree shows `Trust this folder?` with `Trust this folder` pre-selected; spawn reads the visible pane, recognizes the complete dialog (its title, both navigation-hint tokens `↑↓ navigate` and `Enter select` - matched separately so a hint wrapped in a narrow pane still counts - the selected `❯ Trust this folder`, and `Don't trust`), sends Enter on every poll the complete dialog is still there, verifies that a later visible-pane capture no longer contains it, and then continues the ordinary readiness gate. Trust is never pre-registered in `config.toml`; the dialog is answered live. |
| Slash submission | One Enter submits, with no popup swallow or settle hazard. |
| Environment marker | None; identity comes from process ancestry command name `kimi`, which `../../../bin/fm-harness.sh` keeps a retained foreign marker from overriding. |
| Composer | Bordered box with a bare `>` prompt glyph and no observed ghost or placeholder text. |
| Effort | No verified reasoning-effort flag; `references/common/model-and-effort.md` owns unsupported-value handling. |
| Effort | `kimi provider list --json` exposes per-model `supportEfforts` values `low`, `high`, and `max` plus a `defaultEffort`; the launch flag and mapping remain unverified, so spawn records and omits requested effort per `references/common/model-and-effort.md`. |

## Readiness-gated start

`../../../bin/fm-spawn.sh` launches Kimi bare, waits for the composer box or `Welcome to Kimi Code!`, sends only `Read the brief at <absolute-path> and follow it exactly.`, and requires a cleared composer plus either the echoed `✨` submission or nonzero context before accepting delivery.
`../../../bin/fm-spawn.sh` launches Kimi bare, handles the complete 2.0.0 trust dialog when it appears, waits for the composer box or `Welcome to Kimi Code!`, sends only `Read the brief at <absolute-path> and follow it exactly.`, and requires a cleared composer plus either the echoed `✨` submission or nonzero context before accepting delivery.
Every trust predicate reads `fm_backend_visible_capture` - the viewport with no scrollback - never the 120-line history read the delivery gate uses: the dialog is a TUI frame, and a history-backed capture would keep reporting it after Kimi redrew past it, storming Enter into a live composer and then failing an already trusted spawn. That primitive is implemented on tmux (`capture-pane -p -S -0`), herdr (`pane read <pane> --source visible`, verified against Herdr 0.8.0 in `docs/verification/runtime-backends.md`) and zellij (`action dump-screen --pane-id`, no `--full`), and `FM_BACKEND_VISIBLE_CAPTURE` in `bin/fm-backend.sh` is the one list of them. orca has only a history read; cmux's `read-screen` without `--scrollback` plausibly reads just the viewport but has not been live-verified. A Kimi spawn on either is therefore refused at preflight, before the worktree or pane exists, naming the backend and the missing verified viewport capability, pending that verification for cmux. There is no fallback to the scrollback read. A viewport read that exits nonzero fails readiness immediately with the backend named, rather than being mistaken for a blank screen. A successful but blank viewport read is absence of evidence, not evidence of a cleared dialog: it costs that poll, restarts the two-capture ready count below, and leaves the trust diagnostics where they were. The trust answer is retried until the dialog clears - Kimi swallows keypresses during its startup window, so a single Enter can be dropped - and the re-send is gated on the complete dialog still being on that visible pane, so it cannot fire once the dialog cleared. Trust is accepted only after a later visible-pane capture proves that the dialog cleared; a stuck dialog fails with the observed dialog signals and the answer count in the diagnostic.
Any single marker of the dialog on that visible pane - `Trust this folder` or the negative `Don't trust` option - withholds the ready verdict, because a capture caught mid-redraw and a capture that has painted only the box title both miss the complete dialog while the banner above it would otherwise read as ready. The banner also prints before the dialog paints at all, which no single capture can distinguish from a ready pane, so the verdict additionally requires two consecutive captures that are each ready and free of dialog text; a capture that is not ready, and a blank one, restarts that count, which is what keeps the pre-banner boot captures and redraw frames from spending it.
This launch-then-send shape is mandatory because Kimi rejects positional instructions as an unknown command.
The path must be absolute because the instructions live outside the task worktree and Kimi reads them there without `--add-dir`.

Expand Down
16 changes: 10 additions & 6 deletions bin/backends/cmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -512,12 +512,16 @@ fm_backend_cmux_send_text_line() { # <target> <text> [expected-label]
return 2
}

# fm_backend_cmux_capture: bounded plain-text surface capture. No herdr-style
# small-N empty-result bug was found (finding #3), but "fetch generous, trim
# locally" is kept anyway: a single read-screen call is still bounded by the
# surface's actual current viewport height regardless of the requested
# --lines value, so a caller asking for more than the viewport can see would
# otherwise silently get less than it asked for with no way to tell why.
# fm_backend_cmux_capture: bounded plain-text surface capture. `--scrollback`
# is this adapter's explicit opt-in to history, so the result can include
# lines that have scrolled out of view - it is not a viewport read, and no
# viewport-only primitive is offered for cmux (see FM_BACKEND_VISIBLE_CAPTURE in
# bin/fm-backend.sh). Finding #3's viewport-height cap was observed on
# read-screen calls; whether a call WITHOUT --scrollback is strictly bounded to
# the viewport is plausible but has not been live-verified. No herdr-style
# small-N empty-result bug was found (finding #3); "fetch generous, trim
# locally" is kept for parity with herdr and so a small caller bound never
# depends on how read-screen clamps a small --lines value.
fm_backend_cmux_capture() { # <target> <lines> [expected-label]
fm_backend_cmux_target_ready "$1" "${3:-}" || return 1
local lines=${2:-200} fetch raw out
Expand Down
9 changes: 9 additions & 0 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3016,6 +3016,15 @@ fm_backend_herdr_capture() { # <target> <lines>
printf '%s' "$out" | tail -n "$lines"
}

# fm_backend_herdr_visible_capture: the visible viewport only. `--source
# visible` is herdr's viewport-bounded read, so it needs none of the --lines
# workaround above - the bound is the pane itself, and asking for a line count
# is what triggers the empty-read bug.
fm_backend_herdr_visible_capture() { # <target>
fm_backend_herdr_target_ready "$1" || return 1
fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source visible 2>/dev/null
}

fm_backend_herdr_capture_ansi() { # <target> <lines>
fm_backend_herdr_target_ready "$1" || return 1
local lines=${2:-200} fetch out
Expand Down
8 changes: 8 additions & 0 deletions bin/backends/tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ fm_backend_tmux_capture() { # <target> <lines>
tmux capture-pane -p -t "$1" -S -"$2"
}

# fm_backend_tmux_visible_capture: the visible viewport only. `-S -0` starts at
# the first line of the pane rather than in its history, so nothing scrolled out
# of view can appear in the result - the guarantee a trust-dialog predicate
# needs, which the scrollback-bounded capture above cannot give.
fm_backend_tmux_visible_capture() { # <target>
tmux capture-pane -p -t "$1" -S -0
}

# fm_backend_tmux_send_key: one named key. Mirrors fm-send.sh's --key path:
# `tmux display-message -p -t "$T" '#{pane_id}' >/dev/null`, then
# `tmux send-keys -t "$T" "$2"`.
Expand Down
8 changes: 8 additions & 0 deletions bin/backends/zellij.sh
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,14 @@ fm_backend_zellij_capture() { # <target> <lines> [expected-label]
printf '%s' "$out" | tail -n "$lines"
}

# fm_backend_zellij_visible_capture: the visible viewport only. `dump-screen`
# without --full is already viewport-bounded; this primitive keeps the dump
# whole instead of trimming it to a caller's line bound.
fm_backend_zellij_visible_capture() { # <target> [expected-label]
fm_backend_zellij_target_ready "$1" "${2:-}" || return 1
fm_backend_zellij_cli "$FM_BACKEND_ZELLIJ_SESSION" action dump-screen --pane-id "$FM_BACKEND_ZELLIJ_PANE" 2>/dev/null
}

# --- zellij composer capture and capability primitives ----------------------
#
# `zellij action dump-screen --ansi` ("Preserve ANSI styling in the dump
Expand Down
30 changes: 30 additions & 0 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,36 @@ fm_backend_capture() { # <backend> <target> <lines> [expected-label]
esac
}

# FM_BACKEND_VISIBLE_CAPTURE: backends with a verified viewport-only read, each
# implementing fm_backend_<name>_visible_capture. This one list answers both the
# capability question and the dispatch, so they cannot disagree. cmux is absent
# pending live verification: its `read-screen` without `--scrollback` plausibly
# reads only the viewport, but that has not been observed on a real cmux, and
# the adapter's own capture opts into history with `--scrollback`. orca's
# `terminal read --limit` is a history read with no viewport mode.
FM_BACKEND_VISIBLE_CAPTURE="tmux herdr zellij"

# fm_backend_visible_capture_supported: whether <backend> can read the visible
# viewport WITHOUT scrollback. Callers that must not mistake a scrolled-away
# frame for the live screen ask this first and fail closed on a no.
fm_backend_visible_capture_supported() { # <backend>
fm_backend_list_contains "$FM_BACKEND_VISIBLE_CAPTURE" "$1"
}

# fm_backend_visible_capture: the visible viewport, never scrollback. A backend
# outside FM_BACKEND_VISIBLE_CAPTURE declines here rather than answering with a
# history-backed capture the caller would read as the live screen.
fm_backend_visible_capture() { # <backend> <target> [expected-label]
local backend=$1
shift
fm_backend_visible_capture_supported "$backend" || {
echo "error: backend '$backend' has no verified viewport-bounded capture primitive" >&2
return 1
}
fm_backend_source "$backend" || return 1
"fm_backend_${backend}_visible_capture" "$@"
}

# fm_backend_send_key: one backend-supported named special key.
fm_backend_send_key() { # <backend> <target> <key> [expected-label]
local backend=$1
Expand Down
128 changes: 118 additions & 10 deletions bin/fm-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@
# Verified per-harness turn-end hooks are installed automatically where enabled; some live outside the worktree.
# Kimi uses one surgically installed Firstmate region in $HOME/.kimi-code/config.toml,
# a firstmate-owned global hook and registry, and a gitignored per-task pointer.
# Kimi 2.0.0 also gates a fresh worktree on an interactive folder-trust dialog.
# Its launch-readiness loop reads the visible viewport - so the spawn refuses at
# preflight on a backend with no viewport-bounded capture - recognizes the
# complete dialog, re-selects the already highlighted affirmative option on
# every poll the complete dialog is still there, refuses any ready verdict while
# dialog text is on that pane, and requires two consecutive captures that are
# each ready and dialog-free before the ordinary readiness gates can pass. A
# blank viewport read proves nothing either way: it costs the poll and restarts
# that count. A viewport read that fails outright fails readiness at once.
# grok uses a firstmate-owned global hook under ${GROK_HOME:-$HOME/.grok}/hooks
# plus a gitignored .fm-grok-turnend worktree pointer and a state token.
# muse installs no hook at all - its plugin engine is off in the default build - so
Expand Down Expand Up @@ -2246,10 +2255,11 @@ effort_flag_for_harness() {
# opencode's interactive `opencode --prompt` launch has a verified --model
# flag but no verified effort flag. Its `opencode run --variant` flag belongs
# to a different, non-interactive launch mode, so fm-spawn does not pass it.
# kimi likewise has no reasoning-effort flag; the requested axis stays in
# task metadata but never reaches the launch command. Cursor encodes effort
# in model ids such as cursor-grok-4.5-high, so it also receives no separate
# effort flag.
# kimi provider catalogs expose supported and default effort values, but a
# launch flag and mapping have not been live-verified; the requested axis
# stays in task metadata but never reaches the launch command. Cursor encodes
# effort in model ids such as cursor-grok-4.5-high, so it also receives no
# separate effort flag.
esac
}

Expand Down Expand Up @@ -2277,6 +2287,10 @@ case "$LAUNCH" in
*__KIMIBIN__*)
KIMI_BIN=$(resolve_kimi_binary) || exit 1
LAUNCH=${LAUNCH//__KIMIBIN__/$(shell_quote "$KIMI_BIN")}
fm_backend_visible_capture_supported "$BACKEND" || {
echo "error: refusing Kimi spawn because backend '$BACKEND' has no verified viewport-bounded capture; Kimi 2.0.0 gates a fresh worktree on a trust dialog that can only be answered and confirmed cleared from a scrollback-free read of the live pane" >&2
exit 1
}
if [ "$KIND" != secondmate ]; then
"$FM_ROOT/bin/fm-kimi-turnend-hook.sh" install || {
echo "error: refusing Kimi spawn because the global turn-end hook could not be installed safely" >&2
Expand Down Expand Up @@ -3256,6 +3270,18 @@ kimi_capture() {
fm_backend_capture "$BACKEND" "$T" 120 "$W" 2>/dev/null || true
}

# Trust decisions read the visible pane only. The dialog is a TUI frame, so a
# scrollback-backed capture keeps reporting it long after Kimi redrew past it -
# which would storm Enter into a live composer and then fail an already trusted
# spawn for a dialog that did clear. There is deliberately no fallback to the
# bounded capture: the spawn refuses at preflight on a backend that cannot read
# the viewport, a read that fails outright fails readiness with its exit status
# and the backend's own error on stderr, and only a successful empty read is
# absence of evidence, which the poll loop treats as a skipped poll.
kimi_visible_capture() {
fm_backend_visible_capture "$BACKEND" "$T" "$W"
}

# Kimi launch-readiness and delivery route their composer-emptiness half
# through the shared classifier (bin/fm-composer-lib.sh via
# fm_backend_composer_state), the same owner every steer and injection guard
Expand All @@ -3268,17 +3294,99 @@ kimi_composer_is_empty() {
[ "$(fm_backend_composer_state "$BACKEND" "$T" "$W" 2>/dev/null)" = empty ]
}

# The navigation hint is matched as its two distinctive tokens rather than as
# one row: a pane narrower than the row wraps it, and a wrapped hint is still
# the complete dialog waiting for an answer.
kimi_trust_dialog_is_visible() { # <plain-pane-capture>
local pane=$1
case "$pane" in *'Trust this folder?'*) ;; *) return 1 ;; esac
case "$pane" in *'↑↓ navigate'*) ;; *) return 1 ;; esac
case "$pane" in *'Enter select'*) ;; *) return 1 ;; esac
case "$pane" in *'❯ Trust this folder'*) ;; *) return 1 ;; esac
case "$pane" in *"Don't trust"*) ;; *) return 1 ;; esac
}

# The complete dialog above decides whether to press Enter. Any single marker
# of it on the visible pane decides whether that pane is safe to call ready: a
# capture caught mid-redraw and one that has painted only the dialog's box
# title both fail the complete-dialog test while the dialog is still up and
# waiting, with Kimi's startup banner sitting above it in that same capture.
# Treating such a pane as ready would type the brief pointer into the dialog
# and lose it.
kimi_trust_marker_is_present() { # <plain-pane-capture>
case "$1" in *'Trust this folder'* | *"Don't trust"*) return 0 ;; esac
return 1
}

# A successful key send is not evidence that Kimi accepted trust. Only the
# ordinary readiness signals in a later capture prove advancement.
kimi_ready_signal_is_present() { # <plain-pane-capture>
case "$1" in *'Welcome to Kimi Code!'*) return 0 ;; esac
kimi_composer_is_empty
}

kimi_wait_for_ready() {
local pane i=0 max=${FM_KIMI_READY_POLLS:-60} interval=${FM_KIMI_POLL_INTERVAL:-0.5}
local pane capture_rc i=0 max=${FM_KIMI_READY_POLLS:-60} interval=${FM_KIMI_POLL_INTERVAL:-0.5}
local trust_enters=0 trust_seen=0 trust_still_visible=0 trust_markers_pending=0
local ready_captures=0
KIMI_READY_FAILURE_DETAIL='kimi did not show a verified ready signal before brief delivery'
while [ "$i" -lt "$max" ]; do
pane=$(kimi_capture)
if printf '%s\n' "$pane" | grep -Fq 'Welcome to Kimi Code!' ||
kimi_composer_is_empty; then
return 0
capture_rc=0
pane=$(kimi_visible_capture) || capture_rc=$?
if [ "$capture_rc" -ne 0 ]; then
KIMI_READY_FAILURE_DETAIL="kimi readiness could not read the visible viewport of backend '$BACKEND' (viewport capture exited $capture_rc), so the trust dialog could neither be answered nor ruled out"
return 1
fi
if [ -z "$pane" ]; then
ready_captures=0
i=$((i + 1))
[ "$i" -ge "$max" ] || sleep "$interval"
continue
fi
if kimi_trust_dialog_is_visible "$pane"; then
trust_seen=1
trust_still_visible=1
trust_markers_pending=0
ready_captures=0
# Kimi swallows keypresses during its startup window - the same hazard
# FM_KIMI_SUBMIT_RETRIES covers for the brief pointer - so the
# affirmative selection is re-sent on every poll the complete dialog is
# still on screen. The dialog's own disappearance is the postcondition:
# once it clears, this branch cannot fire again.
if ! spawn_send_key "$T" Enter; then
KIMI_READY_FAILURE_DETAIL="kimi trust dialog was seen but the affirmative selection could not be submitted"
return 1
fi
trust_enters=$((trust_enters + 1))
else
trust_still_visible=0
if kimi_trust_marker_is_present "$pane"; then
trust_markers_pending=1
ready_captures=0
else
trust_markers_pending=0
# The banner prints before the dialog paints its first frame, so one
# ready-looking capture cannot be told apart from a pane whose dialog is
# one redraw away. Two consecutive captures that are each ready and free
# of dialog text can; any capture that is not ready restarts the count.
if kimi_ready_signal_is_present "$pane"; then
ready_captures=$((ready_captures + 1))
[ "$ready_captures" -lt 2 ] || return 0
else
ready_captures=0
fi
fi
fi
i=$((i + 1))
[ "$i" -ge "$max" ] || sleep "$interval"
done
if [ "$trust_still_visible" -eq 1 ]; then
KIMI_READY_FAILURE_DETAIL="kimi trust dialog did not clear after selecting 'Trust this folder' on $trust_enters poll(s); saw 'Trust this folder?', the navigation hint, selected 'Trust this folder', and the negative Don't trust option"
elif [ "$trust_seen" -eq 1 ]; then
KIMI_READY_FAILURE_DETAIL="kimi trust dialog was answered but the pane never advanced to a verified ready signal; saw 'Trust this folder?', the navigation hint, selected 'Trust this folder', and the negative Don't trust option"
elif [ "$trust_markers_pending" -eq 1 ]; then
KIMI_READY_FAILURE_DETAIL="kimi did not show a verified ready signal before brief delivery; trust dialog text stayed on screen without the complete dialog, so the pane was never safe to answer or to treat as ready"
fi
return 1
}

Expand Down Expand Up @@ -4393,7 +4501,7 @@ fi
spawn_send_key "$T" Enter
if [ "$HARNESS" = kimi ]; then
if ! kimi_wait_for_ready; then
kimi_spawn_fail "kimi did not show a verified ready signal before brief delivery"
kimi_spawn_fail "$KIMI_READY_FAILURE_DETAIL"
exit 1
fi
KIMI_POINTER="Read the brief at $BRIEF_REAL and follow it exactly."
Expand Down
Loading
Loading