See the whole crew. Unblock what matters. Let Firstmate ship the rest.
Captain's Deck is a read-only Firstmate flow kanban plugin for Herdr. It is the captain's view of agent orchestration: small surface area, like Pi—one board that does one job well instead of another dashboard to babysit.
Install and enable the plugin, then open the board:
herdr plugin install deimantasnork/captains-deck
herdr plugin enable herdr-firstmate-flow
herdr plugin action invoke open-captain-deck --plugin herdr-firstmate-flowThe last command opens the board in its own captain's deck workspace.
The launcher creates that workspace when missing, then reuses it despite case or spacing changes or a leading icon.
For the floating overlay, invoke open-flow instead:
herdr plugin action invoke open-flow --plugin herdr-firstmate-flowPress q in the board pane to exit.
The plugin ID is herdr-firstmate-flow, not the repository name, because the
project started under that name.
Bind the board to a key in ~/.config/herdr/config.toml:
[[keys.command]]
key = "prefix+shift+f"
type = "shell"
command = "herdr plugin action invoke open-captain-deck --plugin herdr-firstmate-flow"
description = "open captain's deck (full-pane flow board)"Press prefix+shift+f from any Herdr pane. Use the key you want.
Both launchers only touch panes this plugin opened. The launcher reads the
recorded pane id, or it verifies the pane's foreground process.
A user pane that merely shares the Flow label is never touched.
Local development:
herdr plugin link /path/to/herdr-firstmate-flowCheck the board without a TTY (run this from a checkout of this repository):
scripts/kanban-view.sh --homes # list discovered homes
scripts/kanban-view.sh --once # one plain-text frame, all homes
scripts/kanban-view.sh --once --home 2ndmate-demoUse scripts/kanban-view.sh --once --home <name> with a label from --homes to
check one home.
Herdr's plugin marketplace indexes public GitHub repositories tagged with the
herdr-plugin topic, and this repository is listed there:
https://herdr.dev/plugins/. Installation is still herdr plugin install.
Source: https://github.com/deimantasnork/captains-deck
Parallel coding agents only feel like a crew when nothing important stalls in a forgotten tab. Firstmate runs that crew for you: isolated worktrees per task, supervision until the work is actually finished, and a clean handoff when the session is done.
Instead of repeatedly checking agents, opening pull requests, watching CI, rebasing branches, resolving merge-flow interruptions, and rerunning validation, Firstmate keeps the delivery loop moving for you.
| Traditional AI development | Firstmate + Captain's Deck |
|---|---|
| Create task | Create task |
| Start agent | Agent starts in an isolated worktree |
| Check progress manually | Firstmate supervises progress |
| Check whether work finished | Completion is tracked automatically |
| Commit + push | Handled as part of the workflow |
| Open PR | PR prepared automatically |
| Watch CI / validation | Validation pipeline is monitored |
| Update / rebase branch | Ship branch stays aligned with main |
| Resolve merge conflicts | Firstmate works through conflicts |
| Re-run validation | Configured checks run as part of the flow |
| Review + merge | Can land automatically when policy allows |
| Repeat for every agent | Crew keeps moving in parallel |
| You operate the workflow | You handle the decisions that need you |
Traditional
Task → Agent → Check → Push → PR → CI → Fix → Rebase → Test → Merge
↑ ↓
└──── human keeps coming back ────┘
Firstmate + Captain's Deck
Task → Agent → Worktree → PR → Validate → Land
│ │
└────── Firstmate ────────┘
│
Human needed?
│
Captain's Call
│
Captain's Deck
Firstmate's project modes (no-mistakes, direct-PR, local-only, and optional +yolo merge autonomy) prepare the PR, keep ship branches aligned with main, work through conflicts, and run the configured validation pipeline while the task closes.
When policy allows +yolo, landing can happen without you clicking merge.
The estimates below are preliminary directional estimates, not benchmark results. They estimate human orchestration overhead only: checking agents, opening PRs, monitoring CI, updating branches, handling merge-flow interruptions, rerunning validation, and merging. They do not include the actual coding time performed by the agent.
| Task effort | Traditional human touchpoints | With Firstmate | Potential actions removed | Estimated orchestration time saved |
|---|---|---|---|---|
| Small — simple fix / small change | ~6–8 | ~1–3 | 4–6 | ~10–20 min |
| Medium — normal feature / multi-file change | ~10–14 | ~2–4 | 7–10 | ~30–60 min |
| Large — complex feature / migration / significant refactor | ~15–25 | ~3–6 | 10–20+ | ~1–2+ hours |
The advantage compounds when several agents work at the same time. For example, five medium tasks can create roughly 50–70 human coordination touchpoints in a conventional workflow. With Firstmate handling the routine delivery loop, that could fall to roughly 10–20 meaningful human interactions.
Not necessarily less engineering work — dramatically less orchestration work.
Automation still needs a captain for real decisions. When something is blocked, ambiguous, or genuinely requires human judgment, Firstmate exposes it as a Captain's Call. Everything else should keep moving without interrupting your flow.
Captain's Deck is that bridge: a kanban board where you can:
- see the whole crew at a glance,
- spot blocking work and Captain's Calls immediately,
- answer a Captain's Call in place — choose an option, add a note, queue the answer, and let Firstmate act on it,
- jump to the live Herdr pane when you actually need eyes on an agent,
- and leave everything else running without babysitting terminals.
The board stays read-only except for those guarded keyed answers, the steering they trigger, and a message the captain sends to Firstmate with m, so orchestration keeps running and you only touch what actually requires the captain.
Submitting an answer records the durable decision; a current keyed-answer intake wakes the owning home itself, while the Deck can supply a fallback steer for a secondmate-owned call's lane. On a first Reconcile, the Deck also binds itself as the captured source. The dialog spells those effects out before you queue, and FM_FLOW_WAKE=0 skips the Deck's own lane steer.
You steer the ship. Firstmate runs the crew. Captain's Deck shows you where your attention is actually needed.
One board, every crew: an All tab (first in the row) merges every captain
and secondmate home into one view — only work that is planned or still running
(Charted Next, Underway, Captain's Call, and Awaiting Merge). Landed rows stay
on each mate's own tab. After All, the captain home plus each secondmate
home appear as crew tabs. Each tab projects that home's bearings snapshot into
five fixed columns. Nothing is ever written back, with two deliberate exceptions: a
Captain's Call answer (see below), where the captain's own decision goes to
Firstmate's guarded keyed-answer intake and the owning home is notified so the
answer is acted on; and a message the captain types with m (see Message
Firstmate below).
The fleet board folds a task that more than one home lists - a captain home
mirrors delegated work as <mate>/<task> while the mate's own home keeps
<task> - into the owning home's row, in the column that row reports, so one
task is one card. Captain's Call rows already fold that way for answer routing;
two same-named tasks in different repos (no matching namespace, run, or
worktree) stay separate.
| Column | Source |
|---|---|
| Charted Next | gates |
| Underway | every in_flight row (badges carry shipping / validating / parked / paused / failed) |
| Captain's Call | decisions_open - click a ticket to decide it in place |
| Awaiting Merge | in_flight rows whose Firstmate state is done (crew finished, waiting on merge/review) |
| Landed | landed — Firstmate's "Recently Landed": merged PRs, completed scouts, local-only merges (hidden by default here; toggle with L) |
| Other Agents | All tab only: every Herdr agent that is not a Firstmate pane, from herdr api snapshot |
Firstmate's own bearings has four sections (Underway, Charted Next, Captain's Call,
Recently Landed) and deliberately keeps run status out of the section split.
Awaiting Merge is the only added projection: Firstmate's own state == "done"
rows, which are the ones waiting on a merge.
The board uses Firstmate's own bounds by default (FM_BEARINGS_LANDED = 6
newest per home, gates/in-flight = 20). FM_FLOW_ALL=1 requests every row.
The All tab also lists every Herdr agent that is not a Firstmate pane, in an Other Agents column that takes the place of Landed (the fleet board never carries Landed rows).
Blocked agents sort first, then done, working, and idle.
Each card shows the workspace, the agent, its terminal title, the time in its current state, and its tab and pane; click it to open that tab.
A pane counts as Firstmate's when a task's state/<task>.meta names it, when its working directory is a Firstmate home (the supervisor), or when it sits in a Firstmate task workspace (└ <task> · p:<token>).
The header counts the other agents on every tab and shows how many are blocked, so a waiting agent is visible from any crew.
Herdr does not timestamp state changes, so the deck times them itself: a state that was already there when the board opened shows as for ≥<time>.
A Firstmate home can live on another host (remote second mates), and the deck shows it as its own crew tab.
Remote routes come from the captain home's data/secondmates.md — the records with host:, root: and home: fields.
The snapshot goes through Firstmate's own transport, bin/fm-on.sh <id> fm-bearings-snapshot.sh, never raw SSH.
The result is cached like a local home (bearings_secs), so a host that is offline for a long time never blocks the board: the tab keeps the last snapshot and shows · offline, and the header names when it was last seen.
Remote tabs stay read-only: a Captain's Call on a remote home opens nothing, because the answer intake and the steer live in the local home.
Crew tabs show a live activity dot (● working/blocked, ○ agent present) and
the number of tickets on that board once it has been visited; a visited board
with no tickets shows (0). All shows the visible fleet-wide
planned/running count after mirrored rows are folded; mate tabs count every
column including Landed when that column is visible.
╭─ demo-issue-197 ─────────────────────╮
│ ◐ validating │ live badge
│ claude·opus·xhigh │ harness · model · thinking effort
│ Add retry to the… │ title / summary / landed what
│ 9m 59s · ↓ 55.5k tokens · xhigh │ total run time · tokens · thinking
│ ⌸ 4/my-app-feat… │ worktree (or ↗ PR artifact for landed)
╰──────────────────────────────────────╯
Badges: ● shipping, ◐ validating, ⛔ blocked, ⚑ decision /
⚑ captain, ◍ awaits merge, ⏸ parked / ⏸ paused, ⛔ failed,
✓ done / ✓ landed, · queued.
Live state comes from herdr api snapshot; activity and review state come from
Firstmate's bearings snapshot and the home's state/<task>.status tail.
While an agent is shipping, validating, or blocked, the
doing/status line carries total run time and total token use in the same
style as Herdr's agent sidebar (wall time since task spawn when available;
tokens from detection or cumulative Pi session usage). A doing value that
only repeats the badge (validating, validating: …) or the generic
harness busy … line is dropped, so the row shows the totals alone; a doing
that adds real detail keeps it before the totals. Validating and blocked rows
also carry the thinking effort when the row has room
(9m 59s · ↓ 55.5k tokens · xhigh).
The footer shows the frequent actions - ? help, L - Show/Hide Landed, and
r - Refresh board - and each one is clickable. The help modal (click
? help or press ?) lists every binding: ←→ / h l move between columns,
↑↓ / j k move between cards, pgup/pgdn or the wheel scroll a column,
clicking a crew tab switches mate, enter or a click opens a Captain's Call
ticket, o opens the selected agent pane, m opens the Message Firstmate box, 1-9 / tab switch crew
(All = fleet), L shows or hides the Landed column, r refreshes the board,
and q quits. esc or ? closes the modal, and a click outside it closes it
too.
Firstmate updates itself (updatefirstmate, fleet sync) and this deck reads
its live surfaces from the home, so drift usually shows up as empty or quiet
cards rather than an error. Three guards keep that visible:
- Contract check -
scripts/fm-contract-check.sh --home ~/firstmate(orpython3 scripts/fm_contract_check.py --home ~/firstmate --json) runs the home'sbin/fm-bearings-snapshot.sh --json --fields pathsand asserts every section, item field, meta key, status line, decision card, and thefm-captain-hold.sh/fm-send.shintakes that the deck reads. Exit code 1 lists the missing surface; run it after anupdatefirstmate. - Recorded fixtures -
tests/fixtures/keeps a sanitized snapshot and a minimal home.tests/test_fixture_board.pyproves the recorded shape still projects into the same columns, andtests/test_fm_contract_check.pyproves the checker itself catches a dropped field. - Nightly drift alarm -
.github/workflows/firstmate-contract.ymlcloneskunchenguid/firstmate@mainevery night, runs the contract check against it, and opens or comments on an issue labelledfirstmate-driftwhen a surface changes.
For field reports, FM_FLOW_DEBUG=/tmp/fm_flow_debug.log stamps the deck
revision and each home's Firstmate revision on every collector pass, so a log
pins the exact pair.
Clicking a Captain's Call ticket (or pressing Enter on it) opens its decision
card as a modal, composed from the same fm-bearings-board.v1 card the Lavish
bearings board renders: the type badge and repo, the title, the ABOUT /
DECIDE context, every authored option with its hint, the REC mark on the
recommended one, a freeform note row, and Queue answer.
↑/↓(orj/k) move,spacepicks or clears an option, typing edits the note,Tabjumps between the options and the note,Enterqueues, andesccloses without answering.- The answer is piped to Firstmate's one keyed-answer intake
(
bin/fm-captain-hold.sh answers) in the home that owns the ticket, with this Deck as its provenance. The key, answer and label are flattened first, so a newline or tab in card text can never become a second answer row. A card that declaresclose: "release"releases the gated work instead of completing it. - The dialog shows the option's value whenever it differs from its label, names
the card-declared close on the submit button (
Queue answer · releases hold), and prints one line of what a submit does beyond the record: the exact key, the close or release, and, for a secondmate-owned call, the lane that will be steered. - After the record lands, an intake that settles answers itself (it defines
wake_answer_owner) wakes the owning home, and the Deck adds no second steer. For a reconcile request, or an answer recorded by an older intake, the Deck can steer a secondmate-owned call through the parent home's lane inbox (fm-send.sh). A main-home-owned(main)call has no lane, so the Deck sends it no steer. A lane-steer problem is shown beside the queued state; the recorded answer is never reversed. SetFM_FLOW_WAKE=0(orwake_owner=0in the config dir) to skip the Deck's own lane steer. Reconcileis the reserved value: it files a durable reconcile request throughreconcile-requests, bindingherdr-firstmate-flowas its captured source on first use, and never closes anything by itself. The call leaves Captain's Call immediately - the snapshot buckets itreconcilingand shows it under Charted Next asreconcile requested <time>- and returns only if the owner finds it still active.- Card content is read from the durable store written when the call is raised
(
state/decision-cards/<task>.json), then the live board, then the composed payload history. The board build also merges that store first, so the authored raise-time card remains authoritative across rebuilds. - When no composed card exists for a ticket, the dialog still opens with its
durable title, its hold reason as the
ABOUTline, a freeform answer, andReconcile. - Nothing is resolved by the board itself: every guard, the durable decision, and the close all live in Firstmate.
Press m to open the Message Firstmate box, type a message, and press Enter to send it (Esc closes the box without sending).
The deck submits the text with herdr agent prompt to the Firstmate supervisor pane of the tab on screen, the same as typing it in that pane; the All tab sends to the captain home.
The supervisor is the Herdr agent whose working directory is the home path, and the box shows that pane before you send.
If Firstmate is waiting at an approval or question, Herdr refuses the message (agent_blocked), and the box keeps the text so you can answer that prompt first.
If Firstmate is working, Claude Code holds the message until the current turn ends.
An empty message and a message over 2000 characters are refused, and line breaks become spaces.
The board never redraws itself wholesale.
- Live tick (2s): the active board rebuilds its badges from cheap sources —
one
herdr api snapshot(agents, panes, workspace and tab labels), each task'sstate/<id>.metaand the tail ofstate/<id>.status(both cached by mtime). - Bearings (20s): the expensive
fm-bearings-snapshot.shrun only happens when the cached snapshot is older thanFM_FLOW_BEARINGS_SECS, when you switch to a crew that has no cached data, or when you pressr. - Diff repaint: only the screen lines that actually changed are written, so a badge updating does not disturb your scroll position, selection, or the rest of the board.
- Only the active crew is refreshed; other crews cost nothing while you look at one board.
| Action | What happens |
|---|---|
| Click a crew name (top row) | Switch instantly; All loads every mate's bearings; other tabs use cached cards first, then refresh |
| Click a ticket | Focus the ticket's Herdr tab/pane, which selects that agent in the Herdr agents sidebar |
| Click a Captain's Call ticket | Open its decision card modal and queue the captain's answer |
| Click an Other Agents card | Focus that agent's Herdr tab |
| Click a ticket with no live pane | Footer explains it, e.g. demo-issue-198: no live pane · (no worktree yet) |
1…9, Tab / Shift+Tab, [ / ] |
Switch crew |
←/→ or h/l |
Move between columns |
↑/↓ or j/k |
Move between tickets (the view follows the selection) |
| Mouse wheel over a column | Scroll that column smoothly (3 rows per notch) |
Shift+wheel |
Scroll every column together |
PgUp/PgDn, g/G |
Page / jump within the selected column |
Enter |
Decide the selected Captain's Call ticket; any other ticket opens its agent pane |
o |
Open the selected ticket's agent pane, including a Captain's Call one |
m |
Open the Message Firstmate box; Enter sends, Esc closes |
r / q |
Force a bearings refresh / quit |
L |
Show/hide the Landed column |
Input repaints immediately (no waiting for the next data tick), and a burst of wheel events is coalesced into a single repaint. Scrolling moves by terminal rows, so cards clip at the edges instead of jumping a whole 8-row card.
Ticket → pane mapping comes from the task's state/<task>.meta
(herdr_pane_id, herdr_tab_id, herdr_workspace_id), with a fallback to
matching the worktree path against live pane working directories. The herdr
binary is resolved from HERDR_BIN_PATH, then PATH, then ~/.local/bin/herdr
(plugin panes inherit a minimal PATH).
Ticket borders, badges, and column titles use the terminal's ANSI palette
(basic 16 colours, no hardcoded RGB or 256-colour indexes), so they follow
whatever palette the Herdr [theme] setting gives the panes.
- Herdr >= 0.9.0
- Firstmate homes with
bin/fm-bearings-snapshot.sh python3(standard library only), nojqrequired
Homes are discovered in this order (first match wins per path):
FM_FLOW_HOMESenvironment variable —label=path label=pathhomes.confin the plugin config directory — onelabel=pathper lineFM_HOMEor the legacyfm_homeconfig file~/firstmateplus~/.treehouse/*/*/firstmateworktree homes- Remote routes in the captain home's
data/secondmates.md(records withhost:,root:andhome:), shown throughfm-on.sh
The explicit list is additive by default: setting FM_FLOW_HOMES or
homes.conf labels the homes it names, while the scan still runs. Set
FM_FLOW_HOMES_ONLY=1 (or homes_only=1 in the config dir) to make the
explicit list the only source; with nothing configured it falls back to the
scan rather than showing an empty board.
On a large fleet the bearings walk is the slow part: it has been measured
at ~112s for a captain home with 58 in-flight tasks, so bearings_timeout
may need raising above its 180s default and bearings_secs set so the walk
is not repeated on every refresh.
A home is shown when it has task directories or a live Herdr agent running
in it. An unleased spare treehouse worktree - no lease holder, no presentation
label, no live agent - is hidden, because it is a slot rather than a crew. Labels come from the treehouse lease holder
(~/.treehouse/*/treehouse-state.json) or a task's
state/*.herdr-presentation parent_label, so secondmates appear as e.g.
2ndmate-demo.
Plugin config lives in:
~/.config/herdr/plugins/config/herdr-firstmate-flow/
fm_home # legacy single-home config (still honored)
homes.conf # optional label=path list
show_landed # optional: 0 hides the Landed column
wake_owner # optional: 0 skips the Deck's own secondmate lane steer
homes_only # optional: 1 limits discovery to FM_FLOW_HOMES/homes.conf
bearings_secs # optional: seconds a bearings snapshot stays cached
bearings_timeout # optional: seconds allowed for one snapshot walk
debug_log # optional: a path, or an empty file for <config>/debug_log.log
flow-overlay.panes # internal: recorded overlay pane ids
deck-flow.panes # internal: recorded captain's deck pane ids
Environment knobs:
| Variable | Default | Meaning |
|---|---|---|
FM_FLOW_TICK_SECS |
2 |
Live badge/agent refresh |
FM_FLOW_BEARINGS_SECS |
20 |
Bearings snapshot TTL |
FM_FLOW_WHEEL_ROWS |
3 |
Rows moved per wheel notch |
FM_FLOW_LANDED_LIMIT |
10 |
Safety cap on landed cards (Firstmate's own bound is 6/home) |
FM_FLOW_SHOW_LANDED |
1 |
Show the Landed column (0 = hide it, also toggleable with L) |
FM_FLOW_ALL |
0 |
1 = request every row from bearings instead of Firstmate's bounds |
FM_FLOW_HOMES |
— | Explicit label=path crew list |
FM_FLOW_HOMES_ONLY |
0 |
1 = discover only FM_FLOW_HOMES/homes.conf homes |
FM_FLOW_WAKE |
1 |
0 = skip the Deck's own secondmate lane steer (an intake that wakes the owner still does) |
FM_FLOW_DEBUG |
— | Append click/collector debug lines to this file |
Contributions are welcome — see CONTRIBUTING.md for the workflow, conventions, and how to run the checks.
Report bugs, ask questions, or suggest features with a new GitHub issue (templates optional; blank issues are allowed).
- Herdr — terminal workspaces, panes, and the agent host this plugin extends.
- Herdr GPUI — native Rust/GPUI Herdr client used for the screenshots above.
- Firstmate — agent distro that runs the crew, closes worktrees into PRs (and optional yolo release), and owns the bearings snapshot and Captain's Call intake this board projects.
- Pi — one of the supported primary harnesses; each ticket shows whichever harness, model, and thinking effort that home actually uses.

