Skip to content

Add Kimi host support; release v0.8.0 - #86

Merged
alokwhitewolf merged 33 commits into
mainfrom
release/0.8.0
Jul 17, 2026
Merged

Add Kimi host support; release v0.8.0#86
alokwhitewolf merged 33 commits into
mainfrom
release/0.8.0

Conversation

@alokwhitewolf

Copy link
Copy Markdown
Collaborator

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 kimi installs the plugin into <kimi-home>/plugins/managed/evo and registers it in plugins/installed.json, which is the only source Kimi reads to discover plugins.
  • The .kimi-plugin/plugin.json manifest wires the evo skills, the /evo:discover and /evo:optimize commands, and the evo-drain --host kimi hooks.
  • Mid-run evo direct directives reach the model through Kimi's Stop-hook block channel.
  • Kimi joins the background+notify spawn family in the optimize skill: Agent(run_in_background=true).

Corrections over #85, each verified against the binary

  • Install targeted ~/.kimi; kimi-code resolves its data dir as KIMI_CODE_HOME ?? ~/.kimi-code. Doctor validated the same wrong path, so it reported success on an install Kimi could not see.
  • The copy was never registered in installed.json, so nothing loaded even at the correct path. Registration added; doctor now fails without a record.
  • The manifest tools block could never load: tools is the first entry in kimi-code's UNSUPPORTED_RUNTIME_FIELDS. Removed it and the kimi_tools/ and hosts/kimi_native.py code that backed it, plus the optimize-skill text that told the model to call those tools.
  • Directive delivery used additionalContext, which does not exist in kimi-code. The working channel is a Stop-hook permissionDecision: deny whose reason kimi-code appends to the context. A bare message collapses to {action: "allow"} and is discarded, so directives were silently lost.
  • KIMI_CODE_SESSION_ID does not exist; the session id arrives on the hook payload, which the self-contained drain path reads.
  • The binary is not always on PATH (its install script drops it under <kimi-home>/bin); the adapter now finds it there.

Release tooling

.kimi-plugin/plugin.json carries a hardcoded version like the other host manifests but was not wired into bump-version.py or check_versions.py, so it drifted. Both now include it, and tests/unit/test_release_version_pins.py fails 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 kimi registers the plugin; evo doctor kimi reports it registered and enabled.
  • Launching Kimi in an evo workspace fires the SessionStart hook, which registers a session with host=kimi.
  • A queued evo direct directive reaches the model, which quotes it back and runs evo 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

pierreaubert and others added 28 commits July 11, 2026 18:26
- 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.
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.
devin-ai-integration[bot]

This comment was marked as resolved.

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +449 to 451
if (hook_event in ("preToolUse", "PreToolUse") and sess.get("subagents_only")
and _is_denied_in_optimize_mode(tool_name, tool_input)):
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@alokwhitewolf
alokwhitewolf merged commit ab5fbd6 into main Jul 17, 2026
30 checks passed
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.

2 participants