Skip to content

Add agent status webhook sinks to the layout plugin - #3

Merged
simoncrypta merged 2 commits into
masterfrom
herdr-agent-notify-sinks
Oct 23, 2026
Merged

simoncrypta merged 2 commits into
masterfrom
herdr-agent-notify-sinks

Conversation

@simoncrypta

@simoncrypta simoncrypta commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

The layout plugin gets a second pane.agent_status_changed hook, notify.sh, that sends agent status to webhook sinks: one ~/.config/bercail/notify.d/<name>.toml per bot. It sends only meaningful transitions, and it does nothing until a sink file exists.

How it works

Herdr's event carries no previous status. It also fires on title-only changes, and on done → idle when a pane is viewed. So notify.sh:

  • Checks the live status with herdr pane get, under a lock, and keeps each pane's last status in <state>/notify/panes.
  • Sends finished for working or blocked → done or idle, and blocked once per block. Duplicates, seen-flips, stale events, and first sightings send nothing.
  • Skips shell, review, and sidebar panes (panes = agent, the default).
  • Filters by workspace label glob (workspaces), worktree repo (repos), and kind (notify). It also supports enabled and timeout.
  • Passes the key through a 0600 curl header file (auth = both|bearer|x-automation-key|none), never through argv. It refuses a group- or world-readable key file.
  • Retries once on 000 or 5xx. On 401 or 403 it logs every failure and shows one Herdr notification per outage, plus a "recovered" one afterwards.
  • Offers notify.sh status (never prints a URL or key) and notify.sh test <sink>.

handoff-spawn now records stage and harness in handoffs.jsonl so events can say which desk they came from. Old records are unchanged.

Payload (bercail.agent_status/v1)

{source:"bercail", schema, kind: finished|blocked|test, sink, at (UTC ISO), event_id,
 pane:{pane_id, workspace_id, agent, role, status, previous_status, verified, title, display_agent},
 workspace:{label, repo, checkout_path, linked},
 desk:{branch, issue, stage, harness, result:{status, summary, session_id}|null},
 event:<raw Herdr event>, context:<raw Herdr context>}

event_id is the same for every sink and every retry. verified=false means the pane could not be read live, so the event's own status was used.

Files

  • plugins/agentic-layout/notify.sh (new): the hook, plus status and test.
  • plugins/agentic-layout/herdr-plugin.toml: the second [[events]] hook.
  • plugins/agentic-layout/notify.sink.example (new): an annotated sink file.
  • plugins/agentic-layout/tests/notify_test.sh (new): uses fake herdr and curl. It covers classification, filters and routing, enrichment, the live-status check, the unverified fallback, header styles, the key never reaching argv, body, or logs, 401 alerts and recovery, the 5xx retry, refusing an insecure key file, and status/test.
  • skills/handoff/scripts/handoff-spawn, tests/handoff_spawn_test.sh: the optional stage and harness fields.
  • .github/workflows/ci.yml: runs notify_test.sh.
  • README.md, plugins/agentic-layout/ARCHITECTURE.md: docs.

Checks

  • All 8 plugin bash tests pass (including the new notify_test), and all 15 root tests/*.sh pass.
  • cargo fmt --check, clippy -D warnings, cargo test, and the release build all pass.
  • shellcheck 0.11 is clean on notify.sh and notify_test.sh. handoff-spawn has the same 5 findings it had before this change.
  • I checked that the tests can fail: breaking the done→idle rule fails them, and so does putting the key in curl's argv.

Rollout

  1. Release, then update the plugin (herdr plugin install simoncrypta/bercail/plugins/agentic-layout --ref <tag> or bercail update).
  2. For each bot, create ~/.config/bercail/notify.d/<bot>.toml with mode 600 (url, key or key_file, filters). For example, the work bot gets repos = leftlane_app, panes = agent, and notify = finished, blocked.
  3. Run bash "$plugin/notify.sh" test <bot> and expect HTTP 200.
  4. Disable and unlink the old per-bot webhook plugins so events are not sent twice.
  5. Bot routines can drop most of their duplicate and flap suppression, but should still check live status before acting.

Open questions

  • Should perso get an exclude_repos filter, or a catch-all sink?
  • Should there be a started kind (idle → working)?
  • Should bercail doctor report sink health, like the last 401?
  • The handoff prompt still sends only the issue id, not a full line. That is out of scope here.

Follow-up (f32274b)

  • Delivery runs outside the lock. The notify-hook lock was held for the whole POST, up to about 17s per sink, so other panes' hooks queued behind it. When a queued hook ran, it read a later live status, and a short working → done → working turn was lost. Now only the live read, the previous-status swap, and the classification run under the lock. A new test holds one sink for 3s and checks that another pane's finish still posts at once. That test fails against the previous commit.
  • event_id now includes the pid, so two real transitions of the same pane in the same second get different ids.
  • pane.exited and workspace.closed hooks delete a pane's saved status. Herdr numbers panes per workspace (w2R:p9), so a reused id could inherit a stale working and send a false finished.
  • A corrupt sink state file is now replaced. Before, it made every notify_record fail and left a temp file behind.
  • README fix: headless stages have no Herdr pane and send no webhook. Shep reads their result when handoff-spawn returns.
  • Not changed: delivery stays in the foreground. The old per-bot plugins already ran curl -m 8 inside the hook without problems, and since delivery no longer holds the lock, a slow sink only delays its own hook.

🤖 Generated with Claude Code

simoncrypta and others added 2 commits October 22, 2026 19:52
notify.sh is a second pane.agent_status_changed hook. It fans out to
~/.config/bercail/notify.d/<name>.toml sinks, one per bot. It keeps each
pane's last live status (herdr pane get) under a lock, so it sends only
finished (working/blocked -> done/idle) and blocked (once). It skips
title-only changes, duplicate hooks, done->idle seen-flips, and shell or
review panes.

Payloads are bercail.agent_status/v1 with workspace, branch, beads issue,
stage, harness, and the headless handoff result. Keys go through a 0600
curl header file, never argv. A 401 or 403 raises one Herdr notification
per outage. handoff-spawn now records stage and harness in handoffs.jsonl.
…ess.

The notify-hook lock was held for the whole POST (up to ~17s per sink),
so other panes' hooks queued behind it and read a later live status;
a short working -> done -> working turn was lost. Only the live read,
the previous-status swap, and the classification stay under the lock.
event_id gains the pid so two transitions in one second stay apart.

pane.exited and workspace.closed now drop saved statuses, so a reused
pane id (herdr numbers panes per workspace) cannot inherit a stale
working and send a false finished.

A corrupt sink state file is replaced instead of failing every record
and leaving a temp file behind.

README: headless runs have no Herdr pane and send no webhook; Shep
reads their result when handoff-spawn returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simoncrypta
simoncrypta marked this pull request as ready for review October 23, 2026 00:20
@simoncrypta
simoncrypta merged commit bf0f9cd into master Oct 23, 2026
3 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.

1 participant