Add Kimi host support; release v0.8.0 - #86
Conversation
- Correct default plugin-root path (use .parent.parent.parent.parent). - Honor --force for version-based installs; document local-copy behavior. - Compile release-version regex once at module level. - Move re/json imports to top; replace subprocess.call with subprocess.run.
- Use host-specific session precedence in self-contained drain path: cursor prefers conversation_id, kimi prefers session_id. - Wire Kimi engagement detection by calling _maybe_mark_engaged_from_shell unconditionally for cursor/kimi. - Allow Kimi stop-hook self-continuation in _maybe_stop_nudge_text. - Update --host help text, main() docstring, and _resolve_root_from_payload docstring to mention Kimi.
…ssing kimi plugin install
The Kimi adapter installed to ~/.kimi and never registered the plugin, so
`evo install kimi` reported success while kimi-code loaded nothing. Verified
against kimi-code 0.26.0.
- host_install/kimi.py: resolve the data dir as KIMI_CODE_HOME ?? ~/.kimi-code,
the path kimi-code itself uses. The previous default wrote to a directory
kimi never reads, and doctor validated that same path, so it passed on an
install kimi could not see.
- host_install/kimi.py: register the plugin in <kimi-home>/plugins/installed.json
on install and drop it on uninstall. Kimi's plugin store builds its records
from that file alone and never scans plugins/managed/, so an unregistered
copy loads no skills, commands, or hooks. doctor now checks the record and
fails without it.
- host_install/kimi.py: find the binary under <kimi-home>/bin when it is not on
PATH. install.sh does not add it, so the previous `shutil.which` gate
rejected a working install.
- .kimi-plugin/plugin.json: drop the `tools` block. `tools` is the first entry
in kimi-code's UNSUPPORTED_RUNTIME_FIELDS; the declared evo_spawn_subagent
and evo_wait_subagent tools could never load. Removes kimi_tools/ and
hosts/kimi_native.py, which existed only to back them.
- skills/optimize/SKILL.md: fold Kimi into the existing per-host spawn list
instead of a bespoke section, and stop instructing the model to call the two
tools that never existed. Kimi is background+notify via
Agent(run_in_background=true); track experiments with `evo status`.
- inject/drain.py: emit {"message": ...} for kimi. Kimi reads
`message ?? hookSpecificOutput.message` and has no additionalContext field,
so the claude-code-shaped envelope parsed and was silently dropped, taking
every mid-run directive with it.
- inject/registry.py: drop the KIMI_CODE_SESSION_ID entry. That variable does
not exist in kimi-code; the session id arrives on the hook payload, which is
what the self-contained drain path already reads.
- Restore the cursor conversation_id rationale and tool_class debug field that
the drain refactor dropped.
- Tests assert the real contract: the manifest carries no field kimi rejects,
install registers, doctor fails on an unregistered copy, and kimi is absent
from the env-detectable host list.
Drops the superpowers plan/spec docs and the unused .worktrees ignore. No
other host ships a plan or design doc.
Kimi has no passive context-injection channel. A hook's stdout is reduced by
aggregateResults to {action:"block", reason} or {action:"allow"}; only a block
surfaces text, and only Stop's block reaches the model (its reason is appended
to the context, and the turn continues). A bare `message` or a claude-code
`additionalContext` parses but collapses to "allow" and is discarded, so every
mid-run `evo direct` was silently lost. Verified end-to-end against kimi-code
0.26.0: the model now quotes the queued directive back and runs `evo ack`.
- emit_for_host: kimi emits permissionDecision=deny with the directive text as
permissionDecisionReason.
- _self_contained_gate: kimi defers every PreToolUse (a block there would deny
the tool the agent asked for) and never drains on SubagentStop (fired
fire-and-forget; result discarded). Both would otherwise consume a directive
that can never arrive. The turn-end Stop is the delivery point.
- Tests assert the deny envelope and that a queued directive survives (marker
intact) across PreToolUse and SubagentStop.
Wire the Kimi plugin manifest into the release tooling. `.kimi-plugin/plugin.json` carries a hardcoded version like the Claude Code and Codex manifests, but neither bump-version.py nor check_versions.py knew about it, so it kept its old version through a bump. Hosts key off the manifest version to decide whether to refetch the plugin, so a stale manifest leaves Kimi pinned to the previous CLI. Adds tests/unit/test_release_version_pins.py: every `.<host>-plugin/plugin.json` on disk must be registered in both scripts and report the same version. The release tooling had no tests, which is how the third manifest drifted in unnoticed.
The published evo-hq-cli wheel ships none of the plugin files (skills, commands, .kimi-plugin/plugin.json — see package-data). A bare `evo install kimi` resolved the source with `_plugin_root(None)`, which on a wheel is the tool's site-packages parent: a directory that exists but is not a plugin root. install() copied it and then failed doctor. Only editable/source checkouts worked, which is why testing missed it (hermes.py documents the same parents[3] trap for uv-tool installs). The bare path now uses the local tree only when it is a real plugin root (_is_valid_plugin_root checks for .kimi-plugin/plugin.json) and otherwise fetches the GitHub tarball at the running version, matching the bare-case behaviour of the codex, cursor, and hermes adapters. A --from-path that is not a plugin root is now rejected instead of copying junk. Tests cover the wheel fallback (fetches the running version), the checkout path (no download), and the invalid --from-path rejection.
| if (hook_event in ("preToolUse", "PreToolUse") and sess.get("subagents_only") | ||
| and _is_denied_in_optimize_mode(tool_name, tool_input)): | ||
| return True |
There was a problem hiding this comment.
🟡 Queued instruction can wrongly block a retried action during a Kimi run
On a Kimi run, a queued user instruction is spent to refuse the assistant's retried action (the pre-tool branch at plugins/evo/src/evo/inject/drain.py:449) instead of letting that retry proceed, so the retry is wrongly blocked.
Impact: On Kimi, an action retry that the run intended to allow is instead refused, and a pending user instruction is consumed and shown as the refusal reason at the wrong moment.
Missing Kimi counterpart to the Cursor even-cadence guard
For Kimi the gate at plugins/evo/src/evo/inject/drain.py:449-451 only returns True on a PreToolUse when the session is in optimize_mode + subagents_only and the tool is denied — the policy-block case. Inside drain_session, _should_policy_block uses an alternating cadence (plugins/evo/src/evo/inject/drain.py:1317-1327): it denies on odd-numbered violations but returns False on even ones so the tool is let through. On the even case the flow continues past the Cursor-only guard at plugins/evo/src/evo/inject/drain.py:738-744 (host == "cursor"), which has no Kimi equivalent. _maybe_stop_nudge_text returns None (not a Stop event), so emit_for_host(host, hook_event, text, payload) runs with the pending directive text. For Kimi, emit_for_host (plugins/evo/src/evo/inject/drain.py:608-626) emits permissionDecision: deny with the directive as permissionDecisionReason — denying the tool the cadence intended to allow — and then drain_session advances the offset and unlinks the marker (plugins/evo/src/evo/inject/drain.py:772-782), consuming the directive on a pre-tool event even though the stated Kimi design (plugins/evo/src/evo/inject/drain.py:456-464) is to defer every PreToolUse and deliver only at the turn-end Stop.
Prompt for agents
In drain_session (plugins/evo/src/evo/inject/drain.py), there is a Cursor-only guard (host == "cursor" and preToolUse and non-shell tool) that writes "{}" and returns 0 WITHOUT consuming any pending directive, to cover the case where _should_policy_block did not fire on an even-numbered violation. The Kimi host reaches drain_session on a PreToolUse only via the policy-block branch of _self_contained_gate (optimize_mode + subagents_only + denied tool). On an even-numbered violation _should_policy_block returns False, and because there is no Kimi equivalent of the Cursor guard, the code falls through to emit_for_host with any pending directive text. For Kimi emit_for_host emits a permissionDecision:deny with the directive as the reason, which denies the tool that the alternating cadence intended to let through and consumes the directive (offset advanced, marker unlinked) on a pre-tool event — contradicting the documented Kimi design of deferring every PreToolUse and delivering only at the turn-end Stop. Extend the guard so that a Kimi PreToolUse that is not a policy block writes "{}" and returns 0 without consuming the directive (i.e. add host == "kimi" alongside the existing cursor case, or otherwise short-circuit non-blocked Kimi preToolUse), so the queued directive waits for the turn-end Stop as intended.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 57c436b. Extended the drain_session guard so a Kimi preToolUse that isn't a policy block writes {} and returns without consuming (matching the cursor non-shell case), so the even-cadence pass no longer denies the tool or spends the pending directive — it waits for the turn-end Stop. Odd/block path unchanged. Added a test that fires the denied tool twice (odd → deny, even → {}, directive still pending).
`/evo:discover` and `/evo:optimize` ran `evo discover` / `evo optimize`, which are not CLI subcommands — that functionality lives in the discover and optimize skills, and the CLI rejects both as invalid choices. On Kimi (which, unlike Claude Code and Codex, does not expose skills as slash commands) the command bodies are the only slash entry point, so typing the command told the model to run a shell command that errors. The bodies now instruct the model to load and follow the corresponding skill and pass typed arguments through Kimi's `$ARGUMENTS` placeholder. Verified against kimi-code 0.26.0: `/evo:optimize` and `/evo:discover` load their skills and follow the skill's own prerequisites. Test guards that each command loads its skill, never references `Run \`evo <name>\``, and passes `$ARGUMENTS`.
test_every_host_manifest_is_version_checked compared str(Path) against the forward-slash literals in check_versions.py SOURCES. On Windows str(Path) uses backslashes, so every manifest looked unlisted and the test failed on all windows-latest runners. Normalize with as_posix() before comparing.
The policy-block cadence denies odd-numbered violations and lets even ones
through (`_should_policy_block`). On the even pass, drain_session fell through
the cursor-only guard for Kimi and reached emit_for_host, which for Kimi emits
permissionDecision:deny — refusing the tool the cadence meant to allow — and
then advanced the offset and unlinked the marker, consuming a pending directive
on a pre-tool event. That contradicts the Kimi design of deferring every
preToolUse to the turn-end Stop.
Extend the guard so a Kimi preToolUse that is not a policy block writes "{}"
and returns without consuming, matching the cursor non-shell case. The odd/block
path is unchanged (handled earlier by the policy-block branch).
Test fires the denied tool twice: odd → deny, even → "{}" with the directive
still pending.
Adds Kimi Code as a supported host and releases v0.8.0. Supersedes #85, whose draft was verified against kimi-code 0.26.0 and corrected where it assumed the Claude Code plugin contract.
What lands
evo install kimiinstalls the plugin into<kimi-home>/plugins/managed/evoand registers it inplugins/installed.json, which is the only source Kimi reads to discover plugins..kimi-plugin/plugin.jsonmanifest wires the evo skills, the/evo:discoverand/evo:optimizecommands, and theevo-drain --host kimihooks.evo directdirectives reach the model through Kimi's Stop-hook block channel.Agent(run_in_background=true).Corrections over #85, each verified against the binary
~/.kimi; kimi-code resolves its data dir asKIMI_CODE_HOME ?? ~/.kimi-code. Doctor validated the same wrong path, so it reported success on an install Kimi could not see.installed.json, so nothing loaded even at the correct path. Registration added; doctor now fails without a record.toolsblock could never load:toolsis the first entry in kimi-code'sUNSUPPORTED_RUNTIME_FIELDS. Removed it and thekimi_tools/andhosts/kimi_native.pycode that backed it, plus the optimize-skill text that told the model to call those tools.additionalContext, which does not exist in kimi-code. The working channel is a Stop-hookpermissionDecision: denywhose reason kimi-code appends to the context. A baremessagecollapses to{action: "allow"}and is discarded, so directives were silently lost.KIMI_CODE_SESSION_IDdoes not exist; the session id arrives on the hook payload, which the self-contained drain path reads.<kimi-home>/bin); the adapter now finds it there.Release tooling
.kimi-plugin/plugin.jsoncarries a hardcoded version like the other host manifests but was not wired intobump-version.pyorcheck_versions.py, so it drifted. Both now include it, andtests/unit/test_release_version_pins.pyfails if any host manifest is missing from either script or reports a mismatched version.Verification
Installed and driven against a real kimi-code 0.26.0 on macOS:
evo install kimiregisters the plugin;evo doctor kimireports it registered and enabled.host=kimi.evo directdirective reaches the model, which quotes it back and runsevo ack; delivery and ack are recorded on disk.Full unit suite (839) and the three e2e scripts pass. v0.7.1-alpha.1 was published from this line as a pre-release test of the GitHub-tarball install path.
Co-authored-by: pierre aubert pierre@spinorama.org