Skip to content

feat(agent-hook): add ZCode harness support - #1368

Merged
mariusvniekerk merged 10 commits into
kenn-io:mainfrom
subaru-ye:zcode-agent-hook
Oct 14, 2026
Merged

mariusvniekerk merged 10 commits into
kenn-io:mainfrom
subaru-ye:zcode-agent-hook

Conversation

@subaru-ye

@subaru-ye subaru-ye commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds agent-hook support for ZCode, a Claude Code-compatible coding agent
client. roborev agent-hook install auto-detects ZCode (zcode on PATH or
~/.zcode/cli/), installs bundled skills to ~/.zcode/skills/, and
optionally installs MCP configuration into the nested mcp.servers object
of 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 Bash tool 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 the
design (both are ZCode issues, not roborev defects):

  • Configuration-file hooks never load. 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 hooks.enabled: true) are silently ignored. Confirmed with a
    zero-dependency echo hook and client log inspection: loadHooks ran once
    early in the day and never again across three client restarts.
  • Only SessionStart and UserPromptSubmit are dispatched. ZCode
    documents seven hook events, but a full day of turn-phase logs (76+ turns)
    showed zero Stop, PreToolUse, and PostToolUse dispatches.

Implementation

  • Inline plugin form. Hooks are delivered as a roborev plugin directory
    (~/.roborev/zcode-plugin/ with .zcode-plugin/plugin.json plus
    hooks/hooks.json) registered through plugins.dirs in
    ~/.zcode/cli/config.json. Plugin-contributed hooks load in the CLI
    process and enable the hook runner automatically, bypassing the
    configuration-hook gap. Unrelated config keys are preserved, and
    roborev-owned hooks.events entries from earlier installs are removed.
  • Scheduled-event reminders. The default form registers
    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 the
    Stop/PreToolUse/PostToolUse form for harnesses that dispatch those events.
  • Diagnostics and guards from integration testing. Non-triggering hook
    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 install
    now 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.toml fallback.

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.go and runGrokAgentHook generalized to runLocalAgentHook
so 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.

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-ci

roborev-ci Bot commented Oct 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (3e22dcf)

Verdict: Changes require fixes for 4 findings.

High

  • internal/agenthook/state_test.go:3047: TestRecordUserPromptRespectsFailedReviewThreshold duplicates the failed-review threshold case in TestRecordUserPromptDiagnosisExplainsEveryQuietOutcome, adding repository setup and maintenance without distinct failure detection.

    Fix: Delete the standalone test and retain the diagnostic test's no-trigger and count assertions.

Medium

  • internal/agenthook/state.go:1507; internal/agenthook/state.go:386: ZCode scheduled reminders ignore req.Instruction, discarding overrides from config, environment variables, or --instruction. Default CLI reminders also omit the explicit roborev-fix skill invocation.

    Fix: Pass the resolved instruction into the scheduled reminder builder, preserve custom overrides, explicitly invoke roborev-fix in default scheduled guidance, and cover default and custom reminder output.

  • internal/agenthook/zcode_install.go:87: ZCode dump emits only config.json registration data, omitting the required plugin manifest and hook definitions. Applying it on a fresh installation registers a nonexistent plugin, and hook options such as --stop-form, --command, and --timeout are not represented in dumped hook content.

    Fix: Provide documented dump targets or a complete multi-file representation containing all required plugin files and their destinations, including actual hook commands and events.

  • internal/agenthook/zcode_install.go:124: Every ZCode installation writes the same plugin directory, even when --config selects another configuration. Installing with a different daemon address or --stop-form silently changes hooks used by existing configurations.

    Fix: Scope plugin payload directories to the target configuration so separate installations cannot overwrite each other's commands or events.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 21s | Total: 6m35s

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-ci

roborev-ci Bot commented Oct 12, 2026

Copy link
Copy Markdown

roborev: Combined Review (df7c7d0)

Verdict: Changes require fixes for 4 findings.

High

  • internal/agenthook/state_test.go:3047: TestRecordUserPromptRespectsFailedReviewThreshold duplicates the threshold case in TestRecordUserPromptDiagnosisExplainsEveryQuietOutcome, adding repository setup and maintenance without distinct failure detection.

    Fix: Delete TestRecordUserPromptRespectsFailedReviewThreshold and retain the diagnosis test's no-trigger and count assertions.

Medium

  • internal/agenthook/state.go:386: Scheduled ZCode reminders discard req.Instruction, so configuration, environment, and --instruction overrides never reach the agent. Default CLI reminders also omit the explicit roborev-fix skill invocation.

    Fix: Pass the resolved instruction into the scheduled reminder builder, preserve custom overrides, and include explicit skill invocation in default scheduled guidance. Test both output paths.

  • internal/agenthook/zcode_install.go:87: ZCode dump returns only config.json registration data, omitting the required plugin manifest and hook definitions. On a fresh installation, this registers a nonexistent plugin; --stop-form, --command, and --timeout are also absent from the dumped payload.

    Fix: Provide dump targets or a complete documented multi-file representation containing registration configuration, the manifest, and hook definitions with their destinations.

  • internal/agenthook/zcode_install.go:121: All ZCode installations share one plugin directory, including installations targeting different --config paths. Installing a second configuration with another daemon address or --stop-form overwrites the commands or events used by the first configuration.

    Fix: Scope plugin directories to the resolved target configuration so separate installations retain independent payloads.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 16s | Total: 5m40s

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.
@subaru-ye

Copy link
Copy Markdown
Contributor Author

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); --stop-form adds the PreToolUse/PostToolUse shell hooks on top.

@roborev-ci

roborev-ci Bot commented Oct 13, 2026

Copy link
Copy Markdown

roborev: Combined Review (4ef90a8)

Verdict: Changes require fixes for 4 findings.

High

  • internal/agenthook/state_test.go:3047: TestRecordUserPromptRespectsFailedReviewThreshold duplicates the threshold scenario already covered by TestRecordUserPromptDiagnosisExplainsEveryQuietOutcome, adding setup and maintenance without distinct failure detection.

    Fix: Delete the standalone threshold test and retain the diagnosis test’s no-trigger and count assertions.

Medium

  • internal/agenthook/state.go:386: Scheduled reminders discard req.Instruction, so configuration, environment, and --instruction overrides never reach ZCode. Default CLI guidance also omits the explicit roborev-fix skill invocation.

    Fix: Pass the resolved instruction into the scheduled reminder builder, preserve complete custom overrides, and explicitly invoke roborev-fix in default guidance. Cover both output paths.

  • internal/agenthook/zcode_install.go:121: Installations targeting different --config files share one plugin directory. A second installation with different daemon, command, MCP mode, or --stop-form settings overwrites the hooks used by the first configuration.

    Fix: Scope plugin payload directories to the resolved target configuration so installations retain independent commands and events.

  • internal/agenthook/zcode_install.go:87: ZCode dump emits only plugin registration configuration, omitting the required manifest and hooks. Applying it on a fresh installation registers a nonexistent plugin, and command, timeout, and event-selection settings cannot be inspected.

    Fix: Provide explicit dump targets or a documented multi-file representation containing the registration configuration, manifest, and hook definitions with their destinations.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 20s | Total: 5m38s

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-ci

roborev-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (ad98ba8)

Verdict: Changes require fixes for 3 findings.

High

  • internal/agenthook/zcode_install_test.go:138: TestRunDumpZcodeDoesNotWriteSource re-marshals decoded JSON and checks only that the resulting bytes are nonempty. Null or empty objects pass, so the test does not verify complete installation payloads.

    Fix: Replace the re-marshalling assertions with direct checks of required destinations, manifest fields, and configured hook commands and events. Retain the no-write assertions.

Medium

  • internal/agenthook/zcode_install.go:66: Installing from different directories with the same relative --config value hashes the same path string and shares a plugin directory. The second installation overwrites the first installation's command, daemon selection, and event settings.

    Fix: Resolve the configuration path to a cleaned absolute path before comparing or hashing it. Add coverage for identical relative paths resolving to different files.

  • internal/agenthook/install.go:173: For ZCode, --mcp with a custom --config filename writes MCP settings to the adjacent config.json instead. The selected configuration receives MCP-dependent hooks and skills without their required server entry, while an unrelated configuration is modified.

    Fix: Set mcpOpts.ConfigPath to planned.ConfigPath for ZCode so MCP settings merge into the selected shared configuration.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 21s | Total: 6m25s

- 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-ci

roborev-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown

roborev: Combined Review (1939b41)

Verdict: No findings at or above medium severity.


Reviewers: 3x codex, codex (security) | Synthesis: codex, 12s | Total: 5m48s

@mariusvniekerk
mariusvniekerk merged commit 127008f into kenn-io:main Oct 14, 2026
20 checks passed
wesm added a commit to someben/roborev that referenced this pull request Oct 15, 2026
…-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)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants