Skip to content

Emit session-lifecycle events via harness hooks #1005

Description

@williamthorsen

Problem

The event channel carries no session-lifecycle signal. An ended session persists in the lane view as "idle" indefinitely, a session is invisible until its first instrumented skill emits, and the waiting-on-user state rides the least reliable skill emission point (end of turn). Session end has no skill emission point at all — ending is not a skill moment — and activity-based inference is ruled out: sessions legitimately sit idle for days.

Context

  • Both harnesses expose event hooks whose payloads carry session_id and cwd. Confirmed inventory: Claude Code SessionStart, SessionEnd, UserPromptSubmit, Stop; Rovo Dev on_session_start, on_session_end, on_user_prompt, on_complete (configured under eventHooks in ~/.rovodev/config.yml; config is read at startup, so changes require a session restart; a failing hook never blocks the agent).
  • Rovo Dev executes hook commands via the shell (/bin/sh -c) and delivers the event payload as JSON on stdin; hook exit codes are semantic (a hook can block the agent). Verified against the CLI itself — the vendor docs' map-keyed eventHooks.events shape is wrong and is rejected as corrupt config; real configs use a list of {name, commands: [{command}]} items. See the codeassembly KB: "Rovo Dev event-hooks config schema and execution semantics".
  • The harness configs are user-curated files that must not be clobbered. Add a managed hook-entry utility for Claude Code settings.json #1007 (Claude settings.json) and Add a managed event-hook utility for Rovo Dev config.yml #1008 (Rovo config.yml) provide sentinel-based ensure/check/remove utilities; this ticket supplies the actual entries and wiring, and reworks Add a managed event-hook utility for Rovo Dev config.yml #1008's utility to the verified list schema (it shipped modeling the map shape Rovo rejects).
  • Claude Code interprets some non-zero hook exits as control signals (a Stop hook exiting 2 blocks the agent from stopping), so the relay's never-block contract means unconditionally exiting 0.

Proposed solution

One relay helper serving both harnesses, installed into each harness's scripts directory: read the hook payload from stdin, map {harness, hook name} to an event type via a static table, resolve repo and branch attribution against the payload's cwd, and append the event with the payload's session id.

Event Claude Code Rovo Dev
session.started SessionStart on_session_start
session.ended SessionEnd on_session_end
turn.started UserPromptSubmit on_user_prompt
turn.completed Stop on_complete

Waiting-on-user is derived state (latest event is turn.completed), not an event type. Start/end payload discriminators (Claude's source/reason, Rovo's switch/fork/exit) pass through in event payloads rather than minting distinct types.

Only these four hooks are relayed — not Notification or the tool-level hooks: tool events flood the log with detail the lane view does not render, and Notification overlaps the waiting signal Stop already provides. Distribution is CLI-based config injection, not a Claude Code plugin — a plugin cannot cover Rovo Dev.

Hook configuration is a discrete CLI step (configure-hooks) that install invokes by default — decoupling it later is a one-line removal. It injects the entries via the #1007/#1008 utilities, supports --skip-hooks on install, and offers configure-hooks --print emitting snippets for externally managed configs; touching the Rovo config prints its restart reminder. status reports each harness's hook entries as present, drifted, or absent via the same catalog, giving the check utilities their consumer. Ownership rides an inert --sentinel argument the relay accepts and ignores — not a shell comment, so it survives any execution semantics. Hooks carry the session-lifecycle family; skills keep the work-narration family (skill.*, artifact.written, pr.created) — two complementary channels, not a replacement.

Acceptance criteria

Must have

  • A session is visible in the lane view from session start, and ending it (exit or session switch) marks it ended, on both harnesses.
  • Turn boundaries are relayed on both harnesses: prompt submission emits turn.started; response completion emits turn.completed.
  • Relayed envelopes carry correct session, repo, and branch attribution.
  • A hook-relay failure never blocks or degrades the session it observes; the relay always exits 0.
  • Hook configuration is installed and removed via the CLI as a discrete step invoked by install, with an opt-out and a print mode, touching only sentinel-marked entries (via Add a managed hook-entry utility for Claude Code settings.json #1007/Add a managed event-hook utility for Rovo Dev config.yml #1008).
  • status reports each harness's hook entries as present, drifted, or absent.
  • session.started, session.ended, turn.started, turn.completed join the declared vocabulary (no warnings on emit).
  • Hook configuration for both harnesses is documented: manual-adoption snippets matching the print mode's output, Rovo's restart requirement, and the new CLI surface.
  • New behavior is covered by tests.

Should have

  • Session-start/-end payload discriminators (source/reason, including fork and switch) pass through in event payloads.

Metadata

Metadata

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions