Keep Kimi going at a stop with exit code 2

Kimi's agent-core reads a top-level {"decision": "block"} as allow, so
Harbor's keep-going never took effect for Kimi; it honours exit code 2
with the reason on stderr (its hooks run through cmd.exe), once per turn.
Tool-call denials keep the JSON form, which Kimi does honour.
Also: attention skips stopped agents and closed items; file scopes of
function files carry their address.
This commit is contained in:
KeelrLeek committed 2026-10-05 10:53:32 -05:00
1 parent dbfd1720c5
commit dbc0c45ef2
2 files changed
+25 -4

No files matched your search

+9 -4
View File
@@ -21,9 +21,13 @@ Output formats, from the hosts' own code (see integrations/README.md):
claude, codex, deepseek context as {"hookSpecificOutput": {"hookEventName": ..., "additionalContext": ...}} claude, codex, deepseek context as {"hookSpecificOutput": {"hookEventName": ..., "additionalContext": ...}}
kimi, generic context as plain stdout kimi, generic context as plain stdout
claude, codex, deepseek, kimi claude, codex, deepseek, kimi
decisions as Claude-compatible JSON at exit 0 - {"hookSpecificOutput": {"permissionDecision": decisions before a tool as Claude-compatible JSON at exit 0 - {"hookSpecificOutput":
"deny", ...}} before a tool, {"decision": "block", "reason": ...} at a stop - because a {"permissionDecision": "deny", ...}} - because a host that runs the command through PowerShell
host that runs the command through PowerShell turns exit code 2 into 1 turns exit code 2 into 1
claude, codex, deepseek a stop's keep-going as {"decision": "block", "reason": ...} at exit 0
kimi a stop's keep-going as exit code 2 with the reason on stderr: Kimi's agent-core reads a
top-level {"decision": "block"} as allow, and it runs hooks through cmd.exe, which passes
exit code 2 through. Kimi honours one keep-going per turn
opencode, generic decisions as exit code 2 with the reason on stderr (the OpenCode plugin spawns directly) opencode, generic decisions as exit code 2 with the reason on stderr (the OpenCode plugin spawns directly)
Hooks fail open: if Harbor itself is broken they print a warning to stderr and let the agent continue. Hooks fail open: if Harbor itself is broken they print a warning to stderr and let the agent continue.
@@ -39,6 +43,7 @@ from .core import Harbor
BLOCK = 2 BLOCK = 2
JSON_CONTEXT_HOSTS = ("claude", "codex", "deepseek") JSON_CONTEXT_HOSTS = ("claude", "codex", "deepseek")
JSON_DECISION_HOSTS = ("claude", "codex", "deepseek", "kimi") JSON_DECISION_HOSTS = ("claude", "codex", "deepseek", "kimi")
EXIT_CODE_STOP_HOSTS = ("kimi",) # a stop's keep-going: exit 2 + stderr (see above)
EVENT_NAMES = {"session-start": "SessionStart", "prompt-submit": "UserPromptSubmit"} EVENT_NAMES = {"session-start": "SessionStart", "prompt-submit": "UserPromptSubmit"}
@@ -181,7 +186,7 @@ def _deny_tool(host, text):
def _keep_going(host, text): def _keep_going(host, text):
if host in JSON_DECISION_HOSTS: if host in JSON_DECISION_HOSTS and host not in EXIT_CODE_STOP_HOSTS:
print(json.dumps({"decision": "block", "reason": text})) print(json.dumps({"decision": "block", "reason": text}))
return 0 return 0
print(text, file=sys.stderr) print(text, file=sys.stderr)
+16
View File
@@ -1,4 +1,5 @@
"""Regression tests for the correctness review of rounds five and six.""" """Regression tests for the correctness review of rounds five and six."""
import json
import os import os
import re import re
import shutil import shutil
@@ -358,6 +359,21 @@ class AttentionWithoutFalseAlarms(HarborCase):
self.assertFalse(any(a["kind"] == "ack_overdue" and f"#{m['id']}" in a["text"] for a in self.h.attention())) self.assertFalse(any(a["kind"] == "ack_overdue" and f"#{m['id']}" in a["text"] for a in self.h.attention()))
class KimiStopKeepGoing(unittest.TestCase):
def test_kimi_gets_exit_2_and_stderr_others_get_json(self):
import contextlib, io
from harbor import hooks
out, err = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
code = hooks._keep_going("kimi", "keep going")
self.assertEqual((code, out.getvalue(), err.getvalue().strip()), (2, "", "keep going"))
out = io.StringIO()
with contextlib.redirect_stdout(out):
code = hooks._keep_going("codex", "keep going")
self.assertEqual(code, 0)
self.assertEqual(json.loads(out.getvalue())["decision"], "block")
class HooksVisible(HarborCase): class HooksVisible(HarborCase):
def test_mcp_without_hooks_needs_attention(self): def test_mcp_without_hooks_needs_attention(self):
self.h.connect("Codex", "mcp", "codex") self.h.connect("Codex", "mcp", "codex")