Repository navigation
feat(agent-hook): add ZCode harness support - #1368
Conversation
ZCode follows the Claude Code hook protocol: the same seven lifecycle events, the Bash tool name for shell matchers, snake_case stdin envelopes, and the strict decision/reason JSON output, so a Stop hook blocks the stop with the reminder reason. Two ZCode specifics are handled locally: registrations live under hooks.events in ~/.zcode/cli/config.json (a shared config file whose other top-level keys must be preserved), and configuration-file hooks stay disabled until hooks.enabled is true, so the install sets it. Implemented as a repository-local profile alongside Grok Build rather than a kit profile: kit owns the generic registry, while roborev keeps harness-specific config handling close to its installer. Shared JSON config helpers moved to hooks_config.go and runGrokAgentHook generalized to runLocalAgentHook for both profiles. Bundled skills reuse the Codex embed variant like the other slash-style harnesses, and --mcp installs into the nested mcp.servers object in the same config.json.
Real-device testing showed the configuration-hook form never runs on the ZCode versions tested: the desktop client loads configuration hooks only through its UI-driven hooks service and does not reload them across process restarts, so hooks.events registrations (including enabled:true) are silently ignored. That contradicts ZCode's published hooks documentation and is a ZCode client issue, not a roborev defect. Plugin-contributed hooks load in the CLI process itself and enable the hook runner automatically, so the install now writes a roborev plugin directory (manifest at .zcode-plugin/plugin.json plus hooks/hooks.json under the roborev data dir) and appends that directory to plugins.dirs in ~/.zcode/cli/config.json. Unrelated plugins and config keys are preserved, and roborev-owned hooks.events entries from the previous install form are removed to avoid double delivery once ZCode fixes the configuration-hook path.
Real-device testing across a full day of session logs showed ZCode dispatches only SessionStart and UserPromptSubmit despite documenting seven hook events: Stop, PreToolUse, and PostToolUse had zero dispatch records, and re-adding a configuration-form Stop hook with a restart changed nothing. That is a ZCode client issue, not a roborev defect. Register the two scheduled events in the default ZCode form and deliver reminders as injected additionalContext: self-contained text naming the repo, review job IDs, commit refs, and verdicts plus triage guidance, reusing the existing failed-review threshold and acknowledgment throttling so a session is not re-prompted. The daemon now records UserPromptSubmit and SessionStart events; other agents that do not register them are unaffected. Keep the lifecycle-control form behind --stop-form (install and dump) so the registration can switch back once ZCode dispatches those events. ZCode hook timeouts are floored at 30 seconds because a real run measured 15 seconds end to end (process start, daemon discovery, git scope resolution, review lookup), which the 10-second default killed.
Real-device testing showed UserPromptSubmit events arriving and session state updating, but the failed-review query silently returned nothing for a checkout with an open failed review: ListOpenReviewJobs database errors collapse to ok=false, scope failures and muted reminders return early, and nothing on the wire distinguished an empty result from a broken query. Every non-triggering agent-hook response now carries a diagnosis: the git scope failure with the cwd, the untracked checkout, the snooze or missing-guidelines mute, or the evaluation summary (repo, branch, open failed review count, unacknowledged count, threshold) with an explicit 'query unavailable' marker when the jobs query errors. Hook runners print it on stderr, which ZCode records in its hook execution log, and the daemon also logs the underlying database error.
…ting Integration testing surfaced two silent failure modes after installing hooks. First, a daemon older than the installed binary silently skips events and reminder types it does not know, so hooks return empty output; agent-hook install now compares the running daemon's version with the installing binary and tells the user to run 'roborev daemon restart' when they differ. Second, repositories without their own review guidelines (review_guidelines in .roborev.toml on the default branch, or REVIEW.md) keep reminders muted with no surface explaining why; the docs now state the rule, that global guidelines do not satisfy it, that the hook error stream names the mute while it lasts, and the working-tree .roborev.toml fallback for repos that cannot commit guidelines to the default branch.
roborev: Combined Review (
|
The diagnosis field on AgentHookResponse needs the regenerated OpenAPI document, Go client, and web models. Also fix the lint findings from CI: gofmt slips in four edited files, max() for the ZCode timeout floor, and InDelta for the decoded JSON number assertion.
roborev: Combined Review (
|
Post-restart verification confirmed the loaded Stop hook is never dispatched by current ZCode builds (zero dispatch records over a complete turn with the hook confirmed registered), so UserPromptSubmit and SessionStart remain the delivery path. Registering Stop alongside them costs nothing while it is inert and lets reminders switch to the stop-decision form automatically once ZCode dispatches the event, sharing the existing thresholds and acknowledgment state, with no reinstall needed. --stop-form now only adds the PreToolUse/PostToolUse shell hooks on top.
|
Final verification update on the Stop event: after a full ZCode restart (agent process re-parsed hooks at session creation; the per-event hook count matched the registered set including Stop), a complete turn produced zero Stop dispatch records. Together with the earlier full-day log analysis, this confirms current ZCode builds dispatch only SessionStart and UserPromptSubmit, and the UserPromptSubmit-based delivery in this PR is the correct form. The Stop registration is now kept installed by default (inert until ZCode dispatches it, then it activates automatically with the shared thresholds and acknowledgment state, no reinstall needed); |
roborev: Combined Review (
|
Four findings from the combined review on kenn-io#1368: - Scheduled reminders discarded req.Instruction, so configuration, environment, and --instruction overrides never reached ZCode and the default guidance never named the roborev-fix skill. The instruction now replaces the default guidance verbatim, matching the stop-decision reminder, and the default names the skill invocation. Injected-event runs drop an instruction equal to the CLI default before sending, so the purpose-built injected guidance stays in effect under default configuration while explicit overrides still travel (full-chain self-testing caught the default otherwise shadowing it). - Installations targeting different --config files shared one plugin directory, so a second install with a different command, daemon, MCP mode, or event form overwrote the first configuration's hooks. Plugin directories are now scoped to the resolved target configuration, and detection follows the same scoping. - ZCode dump emitted only the registration configuration; applying it on a fresh installation would register a plugin whose files are missing. The dump is now a document carrying the configuration plus each plugin file with its destination path. - A standalone threshold test duplicated the scenario already covered by the diagnosis test; the duplicate is removed.
roborev: Combined Review (
|
- The dump test re-marshalled decoded JSON and only checked the bytes were non-empty, so null or empty objects would pass. It now asserts the required destinations, manifest fields, hook commands, events, and the registered plugin directory directly. - zcodePluginRoot hashed the configuration path as given, so the same relative --config value from different working directories shared a plugin directory; the path is resolved to an absolute path before comparison and hashing, with coverage for the collision. - ZCode --mcp installs with a custom --config wrote MCP settings to the default adjacent config.json while hooks and skills landed in the selected configuration. MCP settings now merge into the selected shared configuration.
roborev: Combined Review (
|
…-prefix * origin/main: (22 commits) Read back a summary that renders a roborev marker (kenn-io#1374) feat: re-evaluate finding rebuttals in original reviewer sessions (kenn-io#1372) fix: disambiguate repositories that share a display name (kenn-io#1371) feat(agent-hook): add ZCode harness support (kenn-io#1368) ci(windows): move Windows checks to hosted runners (kenn-io#1375) build(deps): use the Kit v0.35.1 release (kenn-io#1367) fix(daemon): back off cost lookups for jobs that stay unpriced (kenn-io#1369) fix: exclude failed jobs from open filters (kenn-io#1365) fix(daemon): control time in the reclaimed-attempt sweep test (kenn-io#1366) Update Go dependencies (kenn-io#1348) Store job prompts once, compressed, outside job rows (kenn-io#1357) fix(telemetry): retry screen reports the daemon rejects (kenn-io#1353) fix(daemon): fail orphaned running jobs in the panel sweep (kenn-io#1362) build(deps): move to kit v0.34.1 for faster lint (kenn-io#1364) Track SQLite schema changes as numbered migrations (kenn-io#1359) Expose automatic review recovery in CI health (kenn-io#1361) Speed up initial review queue loading (kenn-io#1355) Treat curly-apostrophe usage limits as quota (kenn-io#1354) fix(daemon): avoid cross-account default-port discovery (kenn-io#1350) Make web asset hashes independent of checkout paths (kenn-io#1352) ...
Summary
Adds agent-hook support for ZCode, a Claude Code-compatible coding agent
client.
roborev agent-hook installauto-detects ZCode (zcodeon PATH or~/.zcode/cli/), installs bundled skills to~/.zcode/skills/, andoptionally installs MCP configuration into the nested
mcp.serversobjectof ZCode's shared
~/.zcode/cli/config.json.ZCode protocol findings (real-device tested)
ZCode follows the Claude Code hook protocol on the wire: the same event
names, the
Bashtool name for shell matchers, snake_case stdin envelopes,and a strict JSON output schema that accepts
{"decision":"block","reason"}and
hookSpecificOutput.additionalContext. Two client-side gaps shaped thedesign (both are ZCode issues, not roborev defects):
configuration hooks only through its UI-driven hooks service and does not
reload them across process restarts, so
hooks.eventsregistrations(including
hooks.enabled: true) are silently ignored. Confirmed with azero-dependency echo hook and client log inspection:
loadHooksran onceearly in the day and never again across three client restarts.
documents seven hook events, but a full day of turn-phase logs (76+ turns)
showed zero Stop, PreToolUse, and PostToolUse dispatches.
Implementation
(
~/.roborev/zcode-plugin/with.zcode-plugin/plugin.jsonplushooks/hooks.json) registered throughplugins.dirsin~/.zcode/cli/config.json. Plugin-contributed hooks load in the CLIprocess and enable the hook runner automatically, bypassing the
configuration-hook gap. Unrelated config keys are preserved, and
roborev-owned
hooks.eventsentries from earlier installs are removed.UserPromptSubmit and SessionStart and delivers reminders as injected
additionalContext: self-contained text naming the repo, review job IDs,commit refs, and verdicts, plus triage guidance (finish the user's task
first, handle only named jobs, re-verify findings, keep out-of-scope
findings open). Existing failed-review thresholds and acknowledgment
throttling apply unchanged.
--stop-form(install and dump) registers theStop/PreToolUse/PostToolUse form for harnesses that dispatch those events.
responses carry a diagnosis (scope failure, untracked checkout, snooze or
missing-guidelines mute, or the evaluation counts) printed on stderr so an
empty reminder is distinguishable from a broken query; the daemon logs the
underlying jobs-query error. ZCode hook timeouts are floored at 30 seconds
because a real run measured 15 seconds end to end.
agent-hook installnow warns when the running daemon is older than the installing binary,
since a stale daemon silently skips events it does not know. The docs
state the repo-review-guidelines muting rule, that global guidelines do
not satisfy it, and the working-tree
.roborev.tomlfallback.Why a repository-local profile instead of kit
Kit owns the generic harness registry; the ZCode specifics are
harness-install handling (plugin directory, shared config file, event
dispatch reality) that stays close to roborev's installer, following the
existing Grok Build precedent. Shared JSON config helpers moved to
hooks_config.goandrunGrokAgentHookgeneralized torunLocalAgentHookso both local profiles share one runner.
Validation
End-to-end on a real ZCode session: a UserPromptSubmit reminder injected
with two open failed reviews, the receiving model triaged both per the
guidance (one fixed, re-reviewed to verdict P, closed; one judged stale,
documented, closed) and finished its fix session, with no human
intervention. Other agents' profiles and tests are unchanged. A
post-restart verification confirmed the loaded Stop hook is never dispatched
by current ZCode builds (zero dispatch records over a complete turn, with
the hook confirmed registered at session creation), so
UserPromptSubmit/SessionStart is the correct delivery form, and the Stop
registration is kept installed to activate automatically once ZCode
dispatches the event.