Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
bash tests/handoff_agent_test.sh
bash tests/review_on_demand_test.sh
bash tests/herdr_bin_fallback_test.sh
bash tests/notify_test.sh

test:
needs: rust-plugin
Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Bercail's workflow is designed around Shep. Shep owns the workflow; bercail runs
1. Shep writes the job as a beads issue (`bd create`): the ask, the repo, links, and any "plan only" note.
2. Shep picks a stage for that issue and runs `handoff-spawn --stage start|codex|cursor --issue ID` (see [handoff](#handoff)). bercail never picks the stage and never falls back to another one.
3. bercail checks `bd` and the issue, adds a `bd comment` with the worktree (and, headless, the result file), and starts that one harness with the issue id as its whole prompt.
4. Shep reads `.bercail/handoff-result.json` (`ready`, `blocked`, or `failed`), resumes the session if needed, and tells you when the desk is ready.
4. For a `--branch` pane, Shep hears `finished` and `blocked` from its [agent status webhook](#agent-status-webhooks). A headless run has no Herdr pane and sends no webhook: `handoff-spawn` returns when it ends. Either way, Shep reads `.bercail/handoff-result.json` (`ready`, `blocked`, or `failed`), resumes the session if needed, and tells you when the desk is ready.

Shep runs the same workflow whichever harness works the issue.

Expand Down Expand Up @@ -181,6 +181,38 @@ Manual skill install: `npx skills add simoncrypta/bercail --skill handoff -g`

Review launch: own work → `tuicr -w` (uncommitted only, watching; agents stage and never commit unreviewed work, and a clean tree does not auto-open); someone else's PR on this checkout → `tuicr pr <n>`. Pickr defaults to `tuicr pr {url}`. `auto_review = false` disables the agent-done hook. A live Review pane is not restarted — tuicr's diff watch keeps it current.

### agent status webhooks

The layout plugin's `notify.sh` hook forwards agent status to webhooks, one sink per bot. Herdr's `pane.agent_status_changed` carries no previous status, and it also fires for title-only changes and for `done` → `idle` when you look at a pane. `notify.sh` keeps each pane's last live status (checked with `herdr pane get`) and sends only these:

- `finished`: working or blocked → done or idle
- `blocked`: once each time the agent blocks

Shell, review, and sidebar panes are skipped.

Sinks are `~/.config/bercail/notify.d/<name>.toml` (`chmod 600`; see [`notify.sink.example`](plugins/agentic-layout/notify.sink.example)). They filter by workspace label glob, repo, and kind. The key is sent through a private header file, never through argv. A 401 or 403 shows a Herdr notification once per outage and logs every time. Without sinks the hook does nothing.

```bash
plugin=$(herdr plugin list --plugin agentic-dev.layout --json | jq -r '.result.plugins[0].plugin_root')
bash "$plugin/notify.sh" status # sinks, filters, last HTTP code; never prints url or key
bash "$plugin/notify.sh" test work-shep
```

Each POST body is `bercail.agent_status/v1`:

```json
{"source":"bercail","schema":"bercail.agent_status/v1","kind":"finished","sink":"work-shep",
"at":"2026-10-22T22:42:00Z","event_id":"w6N:p2:finished:1792708920",
"pane":{"pane_id":"w6N:p2","workspace_id":"w6N","agent":"claude","role":"agent",
"status":"done","previous_status":"working","verified":true},
"workspace":{"label":"Render-preview_Leftlane_app","repo":"leftlane_app","checkout_path":"/path/desk","linked":true},
"desk":{"branch":"feat/render","issue":"lef2745-2vs","stage":"start","harness":"claude",
"result":{"status":"ready","summary":"...","session_id":"..."}},
"event":{"event":"pane.agent_status_changed","data":{}},"context":{}}
```

`desk.issue`, `stage`, and `harness` come from the matching `handoffs.jsonl` record. `desk.result` is the checkout's `.bercail/handoff-result.json` when one exists (usually `null`: headless runs have no pane, so they send no events). `event` and `context` are Herdr's raw hook payloads.

## keys

Prefix is **`Ctrl-Space`**. Linux: [`config/herdr/config.toml`](config/herdr/config.toml) (Alt). macOS: [`config/herdr/config.macos.toml`](config/herdr/config.macos.toml) (Option).
Expand Down
3 changes: 2 additions & 1 deletion plugins/agentic-layout/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Architecture

The plugin is three Bash modules:
The plugin is four Bash modules:

- `state.sh` — reentrant layout lock, probe, load, save, v3→v4 migrate (drops `main_tab_id`). Every state write goes through the lock.
- `topology.sh` — Shell tab always, Review on demand, four panes, dock-then-focus tab switch
- `layout.sh` — config, Herdr helpers, actions, events
- `notify.sh` — agent-status fan-out to `~/.config/bercail/notify.d` webhook sinks (own hook; per-pane prev status under `<state>/notify`, swapped under a lock; delivery runs after the lock is released so a slow sink never delays other panes; `pane.exited`/`workspace.closed` forget saved statuses so reused pane ids start fresh)

## Pane topology

Expand Down
13 changes: 13 additions & 0 deletions plugins/agentic-layout/herdr-plugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,16 @@ command = ["bash", "layout.sh", "event-tab-focused"]
[[events]]
on = "pane.agent_status_changed"
command = ["bash", "layout.sh", "event-agent-status"]

# Fan-out to webhook sinks in ~/.config/bercail/notify.d (no-op without sinks).
[[events]]
on = "pane.agent_status_changed"
command = ["bash", "notify.sh", "event"]

[[events]]
on = "pane.exited"
command = ["bash", "notify.sh", "pane-exited"]

[[events]]
on = "workspace.closed"
command = ["bash", "notify.sh", "workspace-closed"]
Loading
Loading