herdr-gentle-shell
Pi package for Herdr sidebar states: the Pi extension sidecar that publishes session facts as Herdr pane-metadata tokens, the optional Moshi notification channel, and the Pi-side helpers the Herdr Gentle Shell plugin drives.
Package details
Install herdr-gentle-shell from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:herdr-gentle-shell- Package
herdr-gentle-shell- Version
1.6.0- Published
- Nov 12, 2026
- Downloads
- 1,541/mo · 1,541/wk
- Author
- jonasotoaguilar
- License
- MIT
- Types
- extension
- Size
- 3.3 MB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions/gentle-herdr-state.ts",
"./extensions/herdr-session-open.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Herdr Gentle Shell

Local Herdr plugin so the sidebar recognises Pi + Gentle Shell agents.
What It Does
Reports per-pane agent state (exactly one user-facing gentle — pink
#f5c2e7, machine key orchestrating: the principal session's own working
status and its generic status; a non-contemplated subagent renders its own
sanitized name on the amber working cell #f9e2af — never the word
working, never gentle — and a recognized gentle role keeps its
abbreviation — explorer, worker,
verify, judge, rdd — with the total running count in parentheses when
two or more subagents run, for example explorer (3), and a synchronous
(task-mode) delegate counted while its launch is still in flight, so two
in-flight explorers read explorer (2)). The principal's own
activity marker (gentle_principal_v1, published while the principal session
itself works and cleared when it settles) decides the row — never the
delegate count, and never Herdr's aggregate native status (which reads
working whenever any delegate runs): the default is still the principal's
own state, so while the principal works its own turn it reads pink gentle,
with or without a fresh root marker, and no delegate word wins; while the
principal is idle the oldest running delegate role shows, or that subagent's
own name when no role word is recognized. gentle never renders on an idle
session. The single exception is a
blocking synchronous subagent call (gentle_pending_v1): a task-mode
subagent_run/subagent_continue holds the principal's turn open until the
child returns, so the principal is waiting on that child rather than working
and the in-flight child's role renders instead. A queued or running
background delegate publishes no such marker and leaves the principal's
display untouched),
ask (three independent event legs — choice, native questionnaire, legacy rpiv — plus
executed ask_user_question/ask_user_choice tool calls (execution opens, end/result closes;
proposals never block), tool-gated ui_prompt dialog spans (a custom panel — agents view, ODD
viewer, changes view, design canvas — never counts: opening a panel is navigation, not a
question), and
permission waiting; gentle_review starts never speculate ask, amber/orange ?), blocked, done, idle) plus compact
changes segments folded into the single row-1 heading cell (nonempty +N/ -N/ ↑N
with leading spaces, symbols color-distinguished by contains rules: + green for added,
- red for deleted, ↑ blue for pull — added stays green, deleted stays red),
reduced from gentle_*
pane-metadata tokens. It coexists with herdr-pi-tree through a single-writer handoff: while
this plugin owns a pane, herdr-pi-tree stays disabled for that pane, so the
two never share token names or the 16-token budget. Details live in
DESIGN.md.
What It Looks Like
The Agents panel renders the active role (worker), the principal state (gentle),
and compact +N / -N change segments in one row per pane.
Quick Start
Prerequisites: Linux, Node >= 18, Herdr meeting min_herdr_version in
herdr-plugin/herdr-plugin.toml, and
herdr-pi-tree installed (it stays installed but disabled for gentle-owned
panes).
Two packages install separately: the Herdr plugin (herdr-plugin/) and the
Pi package (the repository root, published to npm as herdr-gentle-shell
with the pi-package keyword). The two
packages version independently and release from two tag series —
pi-vX.Y.Z (npm publish) and herdr-vX.Y.Z (GitHub Release) — so either
side may lag the other; the Herdr side is not bumped when it carries no
change.
Source repository: github.com/jonasotoaguilar/herdr-gentle-shell, the
origin that publishes both packages and creates the releases.
Install the Herdr plugin (always)
herdr plugin install jonasotoaguilar/herdr-gentle-shell/herdr-plugin
The Herdr side is always installed; the Pi package below is not optional either, since the sidebar states come from it.
Install the Pi package
pi install npm:herdr-gentle-shell
Inside a Gentle Shell session, the same package is installed through the shell's launcher:
gentle-shell install npm:herdr-gentle-shell
Both hosts install the same npm package,
herdr-gentle-shell — the Pi extension that publishes the
session facts behind the sidebar states — and only the host command differs.
Running Pi sessions still need /reload (or a restart) to pick up the
updated extension.
Apply the sidebar override
herdr plugin action invoke configure --plugin herdr-gentle-shell
The action appends the managed pi-only [ui.sidebar.agents.rows_by_agent]
override described below and reloads the server; herdr server reload-config
and the daemon start (node bin/gentle-status.js) are the same commands run by
hand.
Local development
Point Herdr at this checkout's plugin subdirectory (from this directory).
herdr plugin link only registers the directory; it runs no build.
herdr plugin link --enabled ./herdr-plugin
herdr plugin action invoke configure --plugin herdr-gentle-shell
The repository root is the Pi package (herdr-gentle-shell)
and herdr-plugin/ is the Herdr plugin. Then run /reload in Pi to pick up
the local package.
For isolated/local use point Pi at a throwaway agent dir, e.g. from this
checkout PI_AGENT_DIR=/tmp/pi-agent-test pi install ..
Update
The two packages update independently. The Pi package updates with:
pi update --extensions
and the Herdr plugin updates with:
herdr plugin update herdr-gentle-shell
To move the Herdr side to an explicitly older herdr-vX.Y.Z release, install
that ref after removing the current one:
herdr plugin install jonasotoaguilar/herdr-gentle-shell/herdr-plugin --ref herdr-vX.Y.Z
If the installed plugin is locally linked, the remote install replaces it automatically and warns naming the plugin id; to restore a checkout afterwards re-link it:
herdr plugin link --enabled ./herdr-plugin
Removal
herdr plugin action invoke unconfigure --plugin herdr-gentle-shell
pi remove npm:herdr-gentle-shell
herdr plugin uninstall herdr-gentle-shell
The unconfigure action takes the managed sidebar override out of the Herdr
config and reloads it; the user-owned [ui.sidebar.agents] table is left
intact. The settings exclusion is likewise
ownership-checked: only the entry this plugin added is removed, while a
pre-existing identical entry and unrelated settings are preserved. A foreign,
invalid, or misbound ownership record fails closed: removal warns, preserves
the entry, and still removes the plugin.
Path convention used below: node bin/... runs from the installed plugin
root, while herdr-plugin/bin/... is a repository location in this checkout.
bin/configure.js lives in the Herdr plugin and is no longer shipped by the
Pi package: lib/gentle-sidebar.js and the extension resolve the installed
Herdr plugin root through herdr plugin list --json (override
GENTLE_HERDR_PLUGIN_ROOT for isolated runs) and run that install's copy.
The configure action runs node bin/configure.js --apply --reload: it
appends one marked pi-only [ui.sidebar.agents.rows_by_agent] override for
canonical agent pi and reloads the server. Direct equivalents:
node bin/configure.js --check
node bin/configure.js --apply
node bin/configure.js --apply --reload # apply + `herdr server reload-config`
node bin/configure.js --uninstall --reload
Sidebar override (non-destructive, pi-only)
bin/configure.js manages only its marked block and never edits the
user-owned [ui.sidebar.agents] table it sits under. The managed override is
exactly two pi-only rows: row 1 is the native Herdr tokens state_icon,
workspace, agent in that order as plain native token strings (no custom
cells, no colors wired here — row-1 glyph and identity coloring, including
the native state_icon, is Herdr-native and not plugin-controlled); row 2
wires the eleven presence-encoded gentle_title_* state cells (state word
only, one current title per frame, each in its state's semantic color as
base fg with no rules; role words carry a running-count suffix above one)
followed by the three
change-segment tokens (added green +N, deleted red -N, pull blue ↑N,
each its own color cell with no rules, never swapped). The review leg
renders the user-facing word rdd in blue #89b4fa (same as
explorer/pull; internal TaskStore role slug stays rdd, machine display key
and token names stay review — review execution is silent, never a
notification; only an actual waiting prompt renders ask (desktop attention
is Herdr's native blocked toast); the judge delegate role renders judge in
purple #cba6f7 (distinct from explorer blue, verify teal, and
orchestrating pink); the error value renders blocked as the row word for
the error machine key (never the pane's native state — an error is
sidebar-only, DESIGN §7; it rides the durable session-bound
gentle_error_v1 token, republished on every beat while the run-error leg
stands and cleared the beat it releases, so a beat the sidebar missed is
repaired by the next one); the orchestrating
leg renders the one user-facing gentle (machine key stays
orchestrating, pink #f5c2e7), while the working leg (amber #f9e2af)
carries a non-contemplated subagent's own sanitized name and never renders
the word gentle or working. Remaining state colors:
working/worker amber #f9e2af, explorer blue #89b4fa, verify teal
#94e2d5, ask orange #fab387, error red #f38ba8, done green #a6e3a1,
idle grey #7c7f93. Added stays green and deleted stays red; the mapping
is never swapped. The custom $gentle_heading_* / $gentle_project /
$gentle_agent cells and gentle_changes_summary stay published and owned
for compatibility but are wired into no visible row.
Only one gentle_title_* token carries a value per frame (presence
encoding), so row 2 shows the current state word plus any nonempty change
segments while the rest render empty; native separators on row 2 are
acceptable. Row 1 holds 3 native cells and row 2 holds 14 cells (11 state
- 3 changes), each within Herdr's 16-token per-row ceiling (full block below).
The
gslabel of an isolated Gentle Shell pane needs no key here:gsis not one of Herdr's canonical agents, sorows_by_agent.gsis a configuration error rather than an inert key. Only the daemon's label ladder (DESIGN.md §5.5) makes that row appear, androws_by_agentstays keyed by the canonicalpi.
# BEGIN herdr-gentle-agents (managed; do not edit)
# Sidebar override owned by herdr-gentle-agents: two pi-only rows —
# row 1 native state_icon + workspace + agent, row 2 state word + changes.
# The parent [ui.sidebar.agents] table is user-owned and never touched here.
[ui.sidebar.agents.rows_by_agent]
pi = [["state_icon", "workspace", "agent"], [{ token = "$gentle_title_working", fg = "#f9e2af" }, { token = "$gentle_title_orchestrating", fg = "#f5c2e7" }, { token = "$gentle_title_explorer", fg = "#89b4fa" }, { token = "$gentle_title_worker", fg = "#f9e2af" }, { token = "$gentle_title_verify", fg = "#94e2d5" }, { token = "$gentle_title_judge", fg = "#cba6f7" }, { token = "$gentle_title_ask", fg = "#fab387" }, { token = "$gentle_title_review", fg = "#89b4fa" }, { token = "$gentle_title_error", fg = "#f38ba8" }, { token = "$gentle_title_done", fg = "#a6e3a1" }, { token = "$gentle_title_idle", fg = "#7c7f93" }, { token = "$gentle_changes_added", fg = "#a6e3a1" }, { token = "$gentle_changes_deleted", fg = "#f38ba8" }, { token = "$gentle_changes_pull", fg = "#89b4fa" }]]
# END herdr-gentle-agents
The
single row-1 heading cell mirrors the Gentle Shell SessionChanges model semantics
(write/edit tool outcomes, never git status, never an approximate
reaggregation): files keyed by (root, path) with snapshot-continuity and
unavailable counts (unavailable files stay in the file count with 0/0); ↑N shows when the pane
branch is behind its upstream (branch-ab status only, no diff). gentle_project,
gentle_changes_summary, and the segment
tokens (gentle_changes_added/_deleted/_pull) stay published for
compatibility but render no visible row. Pink gentle (machine key
orchestrating, pink palette unchanged) is the
only user-facing gentle: it renders while the principal session actively
works — even with zero subagents, with or without a fresh root marker — and
as its generic status when a running delegate has no resolvable name, when
only unsalvageable role shapes remain, or when the root marker covers a
queued/running sub or a pending launch; it never renders on an idle session.
A non-contemplated subagent renders its own sanitized name on the amber
working cell instead, and a recognized gentle role keeps its abbreviation
(explorer, worker, verify, judge, rdd). The principal's own
activity decides the row, never the delegate count and never Herdr's
aggregate native status (the work token counts running delegates, and the
native status reads working for any running delegate or pending launch — so
neither can say the principal works): the dedicated
gentle_principal_v1 marker is published while the principal session itself
works and cleared when it settles. That is the default: a working
principal reads the pink gentle — machine key orchestrating, fresh root
marker or not — and no delegate role word
wins; an idle principal shows the oldest running TaskStore role
(explorer, worker, verify, judge, rdd), however many delegates run.
The one exception is a blocking synchronous subagent call: while a task-mode
subagent_run/subagent_continue is in flight the principal's own turn is
blocked on the child, so the producer publishes that child's role in
gentle_pending_v1 and it renders instead of gentle, for as long as the
call lasts. A resume (subagent_continue) carries no agent, so its role is
the tracked task record's agent for its task_id; when neither the input nor
a record names one, the call publishes no role and the row keeps its generic
display. Background launches resolve their tool call immediately, so a
queued or running background delegate publishes no such marker and leaves
the principal's own display untouched. These yield to ask/rdd/blocked
and terminal done — where done needs
settled-plus-empty (principal settled with no running delegates and no
pending launches; a settled principal with running delegates keeps the role
word, never done). A role word is not a subagent label; it carries the
total running delegate count in parentheses when that count exceeds one. The
work total is the union of the
Gentle Shell TaskStore running records (only running counts as
executing; queued/waiting never count), narrowed the same way to the ids the
session's presence snapshot lists running, and the distinct in-flight
subagent_run/subagent_continue calls the record set does not list yet,
deduplicated by call id so one delegate counts once. The in-flight leg is
what makes a synchronous (task-mode) launch count: it creates no TaskStore
record until it returns its tool_result, so two in-flight synchronous
delegates of one role render explorer (2) instead of a bare explorer,
and a call drops out on tool_result, cancellation, and settle. A cancelled
call is not counted, though it still blocks done through the pending-call
leg, which is separate. Names stay record-only (an in-flight call adds no
name), and the oldest-role order is unchanged: snapshot-confirmed running
records first, then in-flight launches, both oldest-first by first sighting.
A run that finished without a
delivered final contributes no count, no role and no name; with no usable
snapshot the local map stands in full, never at zero. Pending tool calls are
otherwise role-only display hints (exact
in-flight subagent agent -> role slug, resolved from the tool input or, for a
resume, from the tracked task record) plus label correlation, and
true concurrent tasks all count. A live running record is never regressed
by a racy non-final status read (only the authoritative launch outcomes,
completions, and presence terminals move it), so principal activity with
live delegates keeps gentle while an idle principal keeps the role word; a
sanitized unknown slug exactly
equal to working or subagent degrades to the pink gentle (neither bare word ever
renders; subagent is the producer's launch-correlation placeholder, never
an agent name).
Background launches (subagent_run with mode: background) announce
lifecycle over message_end custom results: an ACTIVE status promotes the
start into the tasks map like a task-mode record, so an active background
worker keeps the pane on live activity and shows the worker role word rather
than settling to idle/done.
Errors render as user-facing blocked.
Other agents are unaffected. --apply refuses (exit 2, no write) when an
unmanaged rows_by_agent override already exists; remove or rename it, then
re-run. Writes are atomic (temp file plus rename) with a backup at
<config>.herdr-gentle-agents.bak. The config path resolves exactly like the installed plugin's
bin/gentle-status.js (HERDR_CONFIG_PATH, then XDG_CONFIG_HOME, then
the platform default).
Verification
node --check herdr-plugin/bin/configure.js
node --check herdr-plugin/bin/gentle-status.js
HERDR_CONFIG_PATH=/tmp/herdr-fixture.toml node herdr-plugin/bin/configure.js --apply
HERDR_CONFIG_PATH=/tmp/herdr-fixture.toml node herdr-plugin/bin/configure.js --check
HERDR_CONFIG_PATH=/tmp/herdr-fixture.toml node herdr-plugin/bin/configure.js --uninstall
herdr plugin action invoke configure --plugin herdr-gentle-shell
herdr server reload-config
Live check: with the daemon running and the override applied, open a Pi pane
and confirm row 2 shows pink gentle (machine key
orchestrating) while
the principal session actively works — even with zero subagents, and with or
without a fresh root marker — and keeps gentle while its delegates run (a
role word such as worker, explorer,
verify, judge, or a non-contemplated subagent's own name in amber, shows
once the principal itself is idle — and also while a
task-mode subagent call blocks the principal's turn, until it returns), that
a row never reads gentle on an idle pane; row 1
shows the single heading cell (● <project> plus +N/ -N/ ↑N inline, zero ·) after edits / when behind upstream; then ask on an open
question (choice / native-questionnaire / legacy event legs, executed ask_user_question/ask_user_choice, tool-gated ui_prompt dialog spans (a custom panel — agents view, ODD viewer, changes view, design canvas — never counts, because opening a panel is navigation, not a question), or permission waiting; gentle_review starts never speculate ask, and no failed tool ever does either: a tool error changes nothing, see the error rule below). rdd (machine key review) applies during acknowledged native capture/group execution only (reviewerRunAcknowledged: true at execution start — never proposal, never forecast-only, never correctionLines planning, never gentle_review/gentle_review_scope in any operation; no hook/sidebar signal observed; unknown legacy shapes stay dark; call-id tracked so thrown/cancelled calls still clear, read-only assess excluded). Bounded approximation: the acknowledged interval contains preparation and admission; no exact reviewer internal events exist today. The orchestrator-host ask_user_choice prompt emits no Pi tool/event observable by this extension and stays covered only by the event legs above. Never point the fixture commands at the live config: keep
HERDR_CONFIG_PATH on a temp file for apply/uninstall checks.
Core Workflow
A Pi sidecar extension publishes gentle_* tokens per pane; a plugin daemon
reduces the Herdr snapshot to sidebar state on a poll cadence. Same transport
and TTL discipline as herdr-pi-tree, new token names, no new socket or
webhook. [[startup]] node bin/gentle-status.js (no args) is the polling-daemon
bootstrap (ensures the adaptive poll daemon is running: 10 s baseline,
GENTLE_STATUS_POLL_MS-overridable, with a 2 s fast cycle while recent ask
activity, a rendered non-idle display, or send/publish failures are observed);
[[events]] on = "pane.agent_detected", pane.agent_status_changed,
pane.exited, and
pane.closed each run node bin/gentle-status.js --refresh, which ensures the
adaptive poll daemon is running and requests one coalesced immediate frame
so a newly opened project name/state renders without waiting for the next
poll tick. Drawing follows the report, not the clock: the daemon also holds one
long-lived events.subscribe for pane.updated, so a token write Herdr already
repaints for wakes the same single frame loop (bounded reconnect, floored at
250 ms, no second loop, no per-event frame), and the producer requests that same
coalesced refresh after every landed token beat, so a status change
it publishes is drawn at once, and the daemon caps each sleep at the nearest
live done expiry and the nearest pending view-dwell deadline, so a row
reaches idle on time instead of one poll later. done is held until viewed:
a real finish stays done until the pane is first observed as the focused
pane in the daemon's own snapshot, then one 5 s dwell (DONE_VIEW_DWELL_MS,
consumer-side, not an override) and the recede to idle; a finish nobody
looks at recedes at GENTLE_DONE_UNSEEN_CAP_MS (default 900000 ms = 15
minutes, clamped [100, 3600000]), which is the finish token's expiry. A new
turn that reactivates the producer and a session reset clear the held done,
and a new mint is a new episode; the ledger of views is daemon memory only,
so a daemon restart while the token is still fresh can show done once more
for one dwell. GENTLE_HERDR_DONE_HOLD_MS no longer bounds this: it bounds
only the settle-edge hold that withholds the terminal state while an in-flight
launch may still report back (default 5 s, clamped [1000, 300000]). The
producer's own beat is 15 s by default and
GENTLE_HERDR_BEAT_MS-overridable for tests and debugging (clamped to
[100, 300000]); the override changes the cadence only, never the 60 s token
TTL. Reports in one window collapse into one frame, and refresh-driven frames
are floored at 250 ms, so a burst can never spin the daemon. Polling stays the
fallback for every lifecycle signal and the only source for what no Herdr event
carries (the git-derived +N/-N and ↑N row data and the presence prefix), so a
missed, undelivered, or rejected event never sticks. Read-only
supervision triage ships beside it: the doctor and whois plugin actions
(node bin/gentle-doctor.js, node bin/gentle-whois.js) plus the explicit-pane
node bin/gentle-trace.js <pane-id> CLI — all read-only, none mutating state.
See DESIGN.md §4 (producer) and §5 (consumer), and
docs/diagnostics.md for the triage commands.
Key Features You Should Know About
Full v1 state matrix including the root-session
orchestratingmarker (pink●+ the one user-facing wordgentle, the principal session's working and generic status: it shows while the principal actively works — even with zero subagents and with or without a fresh root marker — and covers queued/running subagents and pending launches; yielding only toask/rdd/blockedand terminaldone(done needs settled-plus-empty); never on an idle session) and the authoritative TaskStore role signal (explorer/worker/verify/judge/rddfromTaskRecord.agent, oldest running first by immutable first-seen, shown while the principal itself is idle — a working principal's own turn keepsgentle, asserted by the principal-onlygentle_principal_v1marker rather than by the delegate count or Herdr's aggregate native status, with one exception: a blocking synchronoussubagent_run/subagent_continuecall holds the principal's turn open, so the in-flight child's role shows instead (thegentle_pending_v1role marker) until that call resolves, while a background delegate never takes that path; an unknown delegation keeps its original sanitized name, 32-char bound — never a generic collapse (the reserved wordsgentle,working, andsubagentare no name and degrade to the pink generic) — and renders on the amberworkingcell, which never carries the wordgentle), with a running-count suffix on role labels when two or more delegates run,askon blocked user input,rddduring native review execution (machine key staysreview; execution is silent — desktop attention fires only for a waitingaskintent, as Herdr's native toast), and user-facingblockedas the row word for theerrormachine key (an error never claims the nativeblockedstate — that is the host's NeedsAttention toast and sound, so the sidebar carries the error on the durablegentle_error_v1token while the pane stays quiet onworking, and no phone envelope is sent).Single-cell row-1 heading (
<icon> <project>plus+N/-N/↑Ninline, exactly one nonempty cell so zero·separators; white base withcontainsrules keeping+green (added),-red (deleted, leading-space match),↑blue (pull)) mirroring the Gentle Shell SessionChanges model semantics over Gentle tool outcomes, nevergitstatus and never an approximate reaggregation (root+path continuity and unavailable counts); behind-upstream↑Nfrom branch-ab status only. Session-change tokens are refreshed/cleared from the current Gentle Shell session model and never carried across sessions.Single-writer coexistence with
herdr-pi-tree: no shared token names, no dual-write, scoped sweep of own tokens on handover.An isolated Gentle Shell pane gets a
gsrow in the Agents sidebar, and the label never lies. Herdr identifies an agent from the pane's foreground process group, whose canonical aliases arepiandgentle-shell— nevergs— so an isolated pane is detected only when its foreground group exposes the gentle-shell process, and otherwise has no row at all. Qualification uses that same input first: a pane belongs to the isolated install when a member of its foreground process group exposes an argv/argv0/cmdline path under$XDG_DATA_HOME/gentle-shell/install(else~/.local/share/gentle-shell/install, anchored byinstall/installation.json) — a real pane is launched from the user's project directory, so its cwd is never the install, and cwd is only the fallback for an unreadable group. When Herdr detects the pane, the daemon keeps Herdr's truthful identity and state and only reports the presentation-only--display-agent gslabel; when Herdr detects nothing, it reportspane report-agent --agent gs --state idlefrom a stable per-session source (plugin:<plugin-id>:gs) so the row exists at all. That reported label is the last resort and is reversible: a changed foreground group (the only thing that can make a new detection appear) releases exactly this plugin's authority and re-decides within one beat, andpane.exited/pane.closed, a pane leaving the install, a disabled sidebar, and daemon shutdown all release it too. Discovery is one boundedpane listread plus onepane.process-infoper pane at start, on an explicit--refresh, and on roster events — never per frame; writes are deduped, and a failure does nothing and logs one line per reason. The gs pane's own live state stays unobservable until the isolated install forwardsHERDR_ENVto its agent (a gentle-ai concern, out of scope here).Gentle mode is enabled by default: while selected, this extension is the single publisher of native
pane.report_agent(working/blocked/idle) alongside its metadata beats, with session path-or-id binding, monotonic seq, and a serial latest-wins coalesced queue. Parity contract:workingcarries the specific working role in the message (oldest running role word — known short word, unknown sanitized name, or the pink genericgentlewith no delegation; never bare genericworking; machine keys unchanged); terminalidlefires only on a truly empty settle (principal settled plus zero running/queued plus zero pending plus zero live background jobs plus no human-wait prompt; settle-with-delegates/pending/jobs staysworking, emitted once). A settle is reconciled from the host's own live truth (isIdle()) whenever nothing is active and no work evidence remains, so a run whose settle never reached the producer still reachesidleinstead of freezing the pane on nativeworking; a quiet producer the host never answers stops beating after a bounded budget instead of republishing a count-0 work token forever — but only when it observed no turn, while a turn it saw start keeps beating so the budget never trades a truthfulworkingfor a falseidleon a truth-less host; the beat is re-armed by the edges that schedule a publish (ask, review, launch, session changes, jobs), while an ordinary tool edge reaches the row through the host's own truth rather than by re-arming it; the settle-time pending latch is bounded by that settle's finish hold. A reconciled settle is not an observed turn, so it mints nodoneand sends notask_complete. Live background jobs (bash_background/monitor) are read from Gentle Shell's session-scopedgentle-ai:jobs-sidebarbus event, so waiting on a CI run or a long command keeps the row ongentleand never reachesdone;blockedfires only for an actual human-wait intervention leg with its label (review/RDD execution staysworking, silent).blockedis never derived from an error: nativeblockedis what fires the host's NeedsAttention toast and sound, so a failed run shows our ownerrorstate — the durable, session-boundgentle_error_v1token (republished every beat while the leg stands, cleared when it releases) that the sidebar reduces to theerrorrow word — while the pane stays onworking, and a failed tool call (or a delegate that endedfailed, or a failed beat transport) changes nothing at all: it is detected and logged, never published, and never reaches the error token. The native status keeps its aggregate meaning — it readsworkingwhile any delegate runs — while the sidebar row's working-vs-idle decision reads the principal-onlygentle_principal_v1marker, so an idle principal with a running delegate shows that delegate's own role word or name instead of the principal'sgentle. Our code never emitsdone/finishednative states or a Herdr notification (the opt-in Moshi replacement below is a separate, off-by-default channel): the host owns finished text ondone,blocked/unknown presentation, sound, and all desktop attention toasts (ask + finish) — there is no consumer toast path left to dedup. Single publisher by design: the managed native extension file stays on disk and is excluded through an ownership-bound Pi settings entry while Gentle mode is selected.node bin/gentle-sidebar.js disableremoves only our owned exclusion and managed rows, gates our producer/daemon, and hands the sidebar back to the native publisher (running Pi sessions need/reload); never run both publishers on the same pane. Limits (unverified, host-owned): the hostidle→done→finishedmapping and unknown-state handling are owned by Herdr.Sidebar switch (
enable/disable/status/toggle):node bin/gentle-sidebar.js <verb>picks between this plugin's sidebar rows and the native Herdr sidebar.statusis read-only;enableconverges the managed Herdr rows and records the owned native-extension exclusion;disableremoves only our owned rows and exclusion, never a user table. See docs/sidebar-control.md.In-Pi switches (
/herdr-gentle:customize): two switches, both local state and both reachable from a plain Pi session outside a Herdr pane. With no argument the command opens the floating panel in Gentle Shell's own layout: one leftCATEGORIESlist (Herdr Sidebar,Moshi notifications) and one right pane headed<Category> · n/mwith its setting rows, each row painting the stored value with●and the other with○; one line above the footer describes the focused setting (<setting> — <what it changes>), and the footer reads←/→ or Tab panes · ↑/↓ move · Enter apply · Esc close, while←/→orTabmoves between the category and value panes,↑/↓orj/kmoves the cursor,Enterapplies the focused row andEsccloses without a write. The legend names only keys: it never advertisesClick, because every token in it is a key the panel answers to, and a narrow card compacts it instead of wrapping. Pointer input still works without being advertised — a click on a category selects it, a click on a row applies it, and a click outside the panes is ignored. Each category owns exactly one switch:Herdr Sidebarshows the singleSidebar agent rowsrow, and the only Moshi switch is theNotificationsrow underMoshi notifications. The two are never mixed —Sidebar agent rowsneither reads nor writesgentle-moshi.json, so turning the agent rows off never turns the notification channel off. Herdr's own done/ask attention (the finished and needs-attention pop-ups and their sound) is Herdr's native behavior, configured in Herdr itself — its config and its settings — and this package deliberately offers no switch for it: the optional Moshi channel below is the only notification surface the package owns. The plain two-step menu stays as the fallback when the TUI helpers orctx.ui.customare unavailable.Sidebar agent rowsdrives the Herdr plugin's ownbin/configure.js(--checkto read,--apply --reload/--uninstall --reloadto change) and never edits the Herdr config itself;Moshi notificationswrites the flag below and adds or removes the-extensions/moshi-hooks.tsexclusion in Pi'ssettings.json, running themoshi-hookpairing gate before enabling (a refusal writes nothing) and reloading Pi only when that settings entry actually changed./herdr-gentle:customize statusprints one read-only line (customize status: sidebar=<on|off|unknown> moshi=<on|off>); an unknown argument, Esc, or a cancelled menu writes nothing. A sidebar verdict ofunknownis never a refusal: the panel says the state is unknown and choosingOnstill runs the apply step, reporting that step's own error when it refuses (the same behavior the menu always had). The run-by-hand equivalents are the CLI and the flag file shown below.Optional Moshi replacement (off by default): one flag file,
<state dir>/gentle-moshi.jsoncontaining{"enabled":true}(the state dir isGENTLE_STATUS_STATE_DIR, else the storedherdr-gentle-agentsstate dir under$XDG_STATE_HOME/herdror~/.local/state/herdr):dir="${GENTLE_STATUS_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/herdr/plugins/herdr-gentle-agents}" mkdir -p "$dir" && echo '{"enabled":true}' > "$dir/gentle-moshi.json"/herdr-gentle:customize→Moshi notificationswrites this flag for you, and enabling there first requires themoshi-hookCLI onPATHwithmoshi-hook status --jsonreportingpaired: true(an unpaired host is refused and nothing is written).With the flag on, this plugin replaces the Moshi-managed Pi hook: the same Moshi integration (Chat View, Live Activity, session row, terminal context, phone-side approvals) keeps working, but notifications follow this plugin's policy — one when Pi really needs your input (
ask) and one when Pi really finishes. Nothing per prompt, nothing per turn, and nothing at all while the flag is off: every envelope path requires the flag at send time, so a context captured for Chat View while the channel was on can never authorize a notification after it is turned off. Ifextensions/moshi-hooks.tsis installed, add-extensions/moshi-hooks.tsto theextensionslist in your Pisettings.jsonso the Moshi-managed hook cannot notify in parallel; that edit leaves every other settings key and entry untouched. Removal of the flag (or{"enabled":false}) silences this channel immediately — the flag is re-read per event — and restoring the hook entry brings the original notifier back. Moshi owns pairing and credentials; this plugin never reads or stores a token, an absent daemon or unpaired host is a silent no-op, and Pi is never interrupted by a Moshi failure.Delegation completion reconciled from Gentle Shell presence files, not chat delivery: a delegate resolves only from an explicit terminal status or a witnessed absence in a strictly newer verified activity generation of the same session, bound to an immutable per-task execution epoch (new launch, resume, or inactive-to-active transition — never a progress read or heartbeat tick). Ambiguous, stale, or oversize snapshots never clear tasks. Known limits: delegates that launch and finish between presence publications never appear and still depend on chat delivery, and absence-driven completion records
completedwithout touching the error leg; a delegate that endedfailedis detected and logged, never published as state.TTL-bounded tokens: stale panes fall back to native
idle.Herdr session opener (
herdr_session_open): opens a Herdr panel running a Pi session at a path. A linked Git worktree opens as a worktree panel (reusing the workspace Herdr already has for that checkout); a repository's primary checkout, a subdirectory or any generic directory opens as a normal panel. Panel kind is decided from git, the pane Herdr names is verified to belong to its workspace and to sit in the requested path before Pi starts, and (unlessnotify: false) the caller is woken when that session really finishes. The other worktree tools and the ODD and issues panels live inodd-worktreenow; the sidebar and Moshi switches stayed here and are reached through/herdr-gentle:customize. See docs/herdr-session-open.md.
Documentation
| Your task | Start here |
|---|---|
| Protocol, token grammar, reducer, and coexistence rules | DESIGN.md |
| Compatibility, migration, and rollback | DESIGN.md §9 |
| Open decisions blocking implementation | DESIGN.md §10 |
| Supervision triage (read-only doctor/trace/whois) | docs/diagnostics.md |
| Lifecycle recovery and interruption (polling fallback) | docs/supervision.md |
Sidebar switch (bin/gentle-sidebar.js) |
docs/sidebar-control.md |
Herdr session opener (herdr_session_open) |
docs/herdr-session-open.md |
| Performance trace and its report | docs/performance-trace.md |
Next Steps
- Ship the Pi producer extension (
extensions/) and the Herdr consumer daemon (herdr-plugin/bin/plusherdr-plugin/lib/); the daemon ships, the sidebar override installs via theconfigureplugin action above. - Resolve the open decisions in DESIGN.md §10 before implementing against them.
- Live sidebar eyeball with subagents/
askremains the final acceptance check (see Verification above).
License
MIT — see LICENSE.