30 KiB
herdr
Terminal based agent runtime for coding agents.
Scope and Audience
These instructions are layered.
- Unless a section explicitly says it is maintainer-only, local-machine-only, or external-contributor-only, treat it as universal project guidance.
- Universal project rules apply to every agent working on Herdr, including forks.
- Maintainer accounts are listed in
.github/MAINTAINERS. Treat the acting account as a verified maintainer only when its username is listed there, the configured remote is the canonicalherdrdev/herdrrepository, and the authenticated account has write access to that repository. If any condition cannot be verified, skip maintainer workflow and follow the external contributor guardrail instead. - Local Can machine workflow applies only on Can's own workstation or Windows
VM setup, for example when
/home/can/Projects/herdr,HERDR_ENV=1, or thewindows-wirtSSH alias exists. If those facts are not true, skip local machine workflow. - External contributor guardrail applies whenever the acting GitHub account is not a verified maintainer, the work is happening in a fork, or the account cannot be determined.
Universal Project Rules
Principles
- State is separated from runtime.
AppStateis pure data, testable without PTYs or async.PaneStateis separate fromPaneRuntime. Workspace logic doesn't need real terminals. - Render is pure.
compute_view()handles geometry and mutations.render()takes&AppStateand only draws. Never mutate state during render. - No god objects. If a module is doing too many things, split it.
app/is already split into state, actions, and input. Keep it that way. - Platform code is isolated. OS-specific behavior lives in the matching
src/platform/<os>.rsfile, with only shared traits, types, wrappers, and testable contracts insrc/platform/mod.rs. Core modules don't have#[cfg(target_os)]. - Detection is decoupled. The detector reads a screen snapshot, never touches the parser or viewport state.
- Screen detection is evidence-based. When changing
src/detect/manifests/, first capture the relevant bottom-buffer state withherdr agent read <pane> --source detection --format textand, when styling or alternate screen behavior matters,--format ansi. Decide which visible controls are invariant, which are alternatives, and encode them as explicit AND/OR gates. Do not match whole-pane incidental text, and do not use the user-visible viewport for agent status because users can scroll it. - UI patterns should be reused. Herdr is a mouse-first TUI. New dialogs, onboarding, settings, and post-update flows should follow the existing UI/UX language and interaction patterns instead of inventing one-off screens. Prefer reusing existing modal/screen structure, affordances, and close actions so the app feels consistent.
Multiplicative performance paths
Treat work reachable from view computation, rendering, background-pane resizing, PTY parsing, detection, and client frame fanout as multiplicative. Before adding work, identify its frequency and cardinality: per byte, event, or render × panes, tabs, or workspaces × attached clients.
Inside pane-scaled render and layout loops:
- Use narrow terminal-state accessors. Do not collect aggregate input state, format terminal snapshots, inspect process trees, perform filesystem I/O, or allocate when one scalar fact is enough.
- Keep terminal-core lock duration minimal.
- Preserve hidden-source and retained-render early exits. Hidden panes still parse output, but their output must not trigger presentation work merely to keep terminal or detection state current.
- When a change adds or widens work in one of these loops, profile fixed geometry
with 1 and at least 15 populated panes and report the scaling delta. Use
just bench-render-scaleto exercise both background-workspace and active-pane cardinality when applicable.
Prefer deterministic operation or architecture tests to wall-clock CI limits.
Performance benchmarks are supporting evidence, not substitutes for behavioral
coverage. Before a stable release, just bench-release-smoke must compare the
candidate with the current stable binary under hidden and visible output. When
the result moves materially or when validating performance work, repeat it with
HERDR_PERF_SAMPLE_SECONDS=60 and investigate the affected scenario.
Runtime/client boundary guardrail
Herdr is migrating toward a server-owned runtime protocol with the TUI as one client. New work should not deepen the current server/TUI coupling.
Before adding state, API fields, events, commands, or socket messages, classify the feature:
- Shared runtime/session fact: belongs in server state and should be exposed through the JSON API/event path when practical.
- TUI presentation state: belongs only in the TUI/client layer.
Do not add new shared behavior that only works through the private TUI client socket. Use neutral server/API names, not UI-surface names like sidebar, row, card, or widget.
Examples:
- Pane/agent metadata, process state, terminal state, events: server/runtime.
- Sidebar layout, token placement, colors, selection, modals, mouse/viewport state: TUI/client.
- Workspace/tab/pane remain shared session organization for now, but avoid making them mandatory identity for unrelated runtime features.
Stable client endpoint contract
The client-owned TUI endpoint generation is independent from the private same-install protocol. Generation 1 is the compatibility floor for Local, SSH, and Cloud connections and must remain available unless retired for a security reason.
- Named core codecs are immutable. Do not add, remove, reorder, or reinterpret fields or enum variants reachable from a published codec. Introduce a new codec name and keep the old codec as a fallback instead.
- Keep baseline JSON handshake and snapshot fields required. New JSON fields must be optional or have field-specific defaults; new enum values need an
Unknownfallback where older clients can safely ignore them. - Add server features through advertised API methods and optional snapshot data when possible. A missing optional feature must disable only that action, not reject the connection.
- Do not change the meaning or load-bearing parameter shape of an advertised endpoint method. If an old server could ignore a new field and incorrectly report success, add a new method name or a separately advertised capability and omit that field without it.
- Missing methods, rejections, timeouts, and unavailable servers are client-local outcomes. They must not disconnect other compatible servers, and typing in a pane must not dismiss their notices.
- Frozen endpoint fixtures, bincode digests, wire-tag tests, and
tests/fixtures/endpoint-method-shapes-v1.jsonare compatibility contracts. Never update a generation-1 expectation merely to bless a wire change; create and negotiate a new codec or method. - Stable and preview update manifests advertise
endpoint_generation. Keep release tooling aligned so an older updater knows when a new server generation really requires replacement. - Existing-value digests cannot detect an appended enum variant. Review every enum reachable from a frozen codec as append-closed even when tests remain green.
Maintainer Workflow
This section applies only to verified maintainers as defined under Scope and Audience. Everyone else must skip this section and follow the external contributor guardrail.
Multi-agent isolation
Read-only investigation can happen in the shared checkout.
Small changes or small tasks are fine in the default main worktree. If you find unrelated implementation changes already in progress in the main worktree, use a dedicated worktree instead. Use a dedicated worktree for bigger features too.
Use this layout:
- shared integration checkout:
../herdr - task worktrees:
../herdr-worktrees/<task-slug> - task branches:
issue/<id>-<slug>when an issue exists
Do all code edits, tests, and validation inside the task worktree.
Commit on the task branch in that worktree.
For substantive feature and bug-fix work, default to opening a pull request instead of pushing master directly. Small, low-risk changes and documentation-only updates can use a lighter workflow when Can prefers it.
Immediately before opening a pull request, fetch origin and make sure the task branch is based on the current origin/master; rebase it when behind, then rerun relevant validation before pushing. If master advances while the pull request is under review and GitHub marks it behind, update the branch and repeat checks and bot review on the new head.
After opening or updating a pull request, monitor all checks to completion with gh pr checks --watch or an equivalent command. Treat Greptile and CodeRabbit as part of CI: wait for both to review the latest pushed commit, not only for the build and test jobs to pass. Evaluate every actionable finding. Fix findings you agree with and reply with the fix; reply inline with a concise technical reason when you disagree. After any fix, wait for CI and both review bots again on the new head.
When the current pull request head is green and both bot reviews are complete, report that it is ready and stop. Never merge a pull request; Can performs the final merge.
If the current session is already inside an isolated task worktree, keep using it. Do not create nested worktrees.
Before committing, propose the commit message and get alignment.
After Can confirms the change is integrated, update the shared checkout, remove the task worktree, and delete the task branch locally and remotely.
Testing
Use just recipes by default instead of invoking cargo or scripts directly.
just test # cargo nextest + maintenance script tests
just check # formatting check + cargo nextest + maintenance script tests
Run just check before committing unless Can explicitly accepts narrower validation. Do not bypass failing checks; fix the failure or explain exactly why a narrower check is enough.
Windows MSVC cross-compilation from Unix requires SDK/CRT headers and libraries.
Install xwin with cargo install xwin --locked, then run
just setup-windows-cross once and accept Microsoft's SDK license when prompted.
This downloads the SDK directly from Microsoft; no Windows machine is required.
The SDK and Zig libc configuration live at ~/.local/share/herdr/windows-cross/,
shared by worktrees. just windows-lint and the Windows stage of just check
use this configuration automatically. To use another SDK, set
LIBGHOSTTY_VT_WINDOWS_LIBC to its Zig libc configuration file.
Setup accepts --accept-license for explicit noninteractive license acceptance;
normal checks never download the SDK. Native Windows builds auto-detect their
installed SDK. Native Linux/macOS builds do not need the Windows SDK.
Unit tests live next to the code (#[cfg(test)] mod tests). New AppState or Workspace behavior should be testable with AppState::test_new() and Workspace::test_new() without PTYs.
For broad refactors or release-risk regressions, classify the risk before editing. Treat changes as refactor-risk when they touch two or more core surfaces, persisted state, protocol/API IDs, workspace/tab/pane identity, restore/handoff, agent detection authority, or UI/input state projection. Before moving code, identify the protected behavior and add or name characterization tests. Identity/state refactors should use the test-only invariants AppState::assert_invariants_for_test() or Workspace::assert_invariants_for_test() with adversarial state from AppState::test_with_adversarial_identity_state() or Workspace::test_adversarial_identity_state(). Run a roundtable for broad refactors and release-risk regressions, not for routine local fixes.
When testing a new Herdr build from inside an existing Herdr session, use
cargo run -- ... and clear inherited Herdr socket overrides so the debug
binary talks to the debug herdr-dev server instead of the installed stable
server:
env -u HERDR_SOCKET_PATH -u HERDR_CLIENT_SOCKET_PATH cargo run -- <command>
Local Can Machine Workflow
This section applies only on Can's workstation or Windows VM setup when the
acting GitHub account is ogulcancelik. Other verified maintainers skip this
local-machine section but continue following maintainer workflow. Everyone else
follows the external contributor guardrail.
Windows VM validation
The Windows VM is for final/manual Windows validation, not normal agent work.
Connect to it with the windows-wirt SSH alias.
Use the single reusable checkout at C:\work\repo. Do not create additional
persistent Herdr clones or worktrees on the VM. The Windows account is already
named herdr, so avoid paths like C:\Users\herdr\herdr.
Before validating a fix on Windows, sync or apply the Linux worktree changes
into C:\work\repo, then run the needed Windows build or test commands there.
Reuse the shared Rust caches under C:\Users\herdr\.cargo and
C:\Users\herdr\.rustup. Do not use WSL on the VM. The VM may have a newer
Zig on PATH; Herdr currently requires Zig 0.16.0, so set
$env:ZIG = "C:\Users\herdr\zig-0.16.0\zig.exe" before running Cargo commands
that build the vendored libghostty-vt.
After validation, leave C:\work\repo clean. Remove temporary files and delete
C:\work\repo\target when disk space is tight, but keep the shared Cargo and
Rustup caches. Unless Can explicitly asks to keep the patched tree for more
manual testing, reset C:\work\repo back to a clean checkout before finishing.
Agent Detection Updates
Agent detection changes should use the manifest hot-reload loop. Use the project-local herdr-throwaway-repro skill to create a disposable named session and drive the real agent UI through Herdr's CLI/API into the target state. Read the pane with herdr agent read <pane> --source detection --format text and inspect matching with herdr agent explain <pane> --json. Update the bundled manifest in src/detect/manifests/<agent>.toml, copy that manifest to the local override path at ~/.config/herdr/agent-detection/<agent>.toml, then run herdr server reload-agent-manifests against the session under test. Before writing the override, check whether one already exists; never overwrite or remove a pre-existing override without alignment. Once the rule is correct, remove the temporary override or restore the previous one exactly so the committed bundled manifest remains the source of truth.
Do not add large agent-specific full-screen fixture suites for routine manifest tuning. Keep Rust tests focused on manifest parsing, rule semantics, skip-state semantics, source precedence, cache reload behavior, and update flow. Use live pane reads for agent-specific screen evidence.
distribution/agent-detection/ is the remotely published catalog for released clients. Keep changes for already released agents aligned with their bundled manifests unless the validator records an exact compatibility exception. A newly bundled agent that current stable clients cannot identify may remain unpublished behind an exact version-and-digest exception, but it must be added to the catalog and the exception removed before the first stable release that ships it. just release-docs-check enforces that no unpublished exceptions remain.
Vendored libghostty-vt
vendor/libghostty-vt.vendor.json records the upstream source commit currently vendored.
Local patches on top of the vendored source must be tracked in vendor/libghostty-vt.patches.md and stored as patch files under vendor/patches/libghostty-vt/. Each entry should say why the patch exists, the Herdr issue, upstream PR/discussion, vendored base commit, touched files, verification, and the exact removal condition.
When updating libghostty-vt, check every active patch in vendor/libghostty-vt.patches.md. If the new upstream commit contains the fix, remove the local patch and index entry, then rerun the listed verification. If not, reapply the patch on top of the new vendored source.
just check runs maintenance tests that verify local libghostty-vt patch files are listed in the index and reverse-apply cleanly against the vendored tree. Do not leave a patch file untracked or an indexed patch unapplied.
Docs
skills/herdr/SKILL.md tracks the latest stable Herdr release because the unversioned npx skills add herdrdev/herdr --skill herdr -g command installs it from master. Do not update this file in feature or preview work. Review and update it only during stable release preparation, and include the change in the release commit with the Cargo.toml version bump. Preview builds keep the latest stable skill.
Unreleased docs live in docs/next/website/src/content/docs/. Update those when a user-facing change needs docs before the next release. They are committed drafts but are never production website input. docs/next/README.md stages root README changes. docs/next/CHANGELOG.md is curated during stable release preparation, not maintained by normal feature and fix work.
The active preview release docs live in docs/preview/website/. Preview CI owns this mutable snapshot and commits it atomically with distribution/preview.json; never edit it manually. Validate it with node scripts/docs/preview.mjs check.
Published stable-release documentation lives in docs/versions/. Release CI seeds each version from the tagged docs/next tree, and maintainers may correct factual documentation errors in a published version afterward. Apply a correction separately to docs/next when it also applies to future releases; never replace a published tree with the current draft. The private website renders /docs/preview/ from the active preview snapshot, /docs/<version>/ from the maintained version directories, and /docs/ from the version selected by docs/versions/manifest.json. Herdr remains the source of truth for the public snapshots.
During release review, finalize docs/next and run just release-docs-check. Do not copy draft docs into preview or published versions manually. Preview CI snapshots the selected commit. After a stable GitHub Release succeeds, release CI seeds a new version from the exact tag and updates distribution/latest.json. The resulting master commit triggers the private website deployment.
Normal feature and fix work must not edit docs/next/CHANGELOG.md; this keeps long-lived branches from conflicting over one shared release file. When refreshing an older pull request, remove its changelog-only diff. Keep user-facing commit subjects descriptive and include required refs #<issue-number> lines so stable release preparation can inventory the full range. During the pre-release audit, use that inventory to human-write and curate the user-facing entries in docs/next/CHANGELOG.md; generated commit lists are source material, not final release prose. Do not add changelog entries for website-only, documentation-only, CI, build-pipeline, or repository-maintenance changes.
Normal feature/fix work should not edit root README.md, root CHANGELOG.md, published version docs, or distribution/latest.json unless it is a focused correction to already-published documentation or explicitly requested.
Put local PRDs, planning notes, and exploratory specs under .local/prd/; .local/ is ignored and locally controlled.
Commit Style
Use lowercase conventional commits, no emojis, and no AI co-author lines. Commit subjects feed preview release notes, so keep them descriptive.
Before committing, propose the commit message and get alignment.
When a normal feature or fix commit relates to a GitHub issue, add a commit body line refs #<issue-number> after the subject:
fix: handle pane focus
refs #82
Do not use GitHub closing keywords like fixes #<issue-number>, closes #<issue-number>, or resolves #<issue-number> in normal commits. master contains unreleased work; release CI closes referenced issues after the GitHub Release is created.
Code Conventions
- Rust: no
unwrap()in production code. Usetracingfor logging. Use#[allow]only with a comment explaining why. - Rust platform-specific code must be compile-gated. Put OS APIs and substantial OS behavior in
src/platform/; when platform checks are needed elsewhere, use#[cfg(windows)],#[cfg(unix)], or target-specific#[cfg(...)]on imports, fields, functions, impls, and match arms so Windows-only code does not compile into Unix builds and Unix-only code does not compile into Windows builds. Usecfg!(...)only for pure cross-platform policy constants whose branches both compile on every target. - Don't add dependencies without a reason. Check whether existing dependencies cover the need first.
- Integration asset versions (
HERDR_INTEGRATION_VERSIONmarkers and matching*_INTEGRATION_VERSIONconstants) are migration versions relative to the latest released tag, not per-commit counters onmaster. If an integration asset changes multiple times between releases, bump it once from the version in the latest release. - When changing the server/client wire protocol, compare
src/protocol/wire.rs::PROTOCOL_VERSIONagainst protocols published in both stable and preview releases. Bump it when the current source protocol has already been published in either channel and the wire format changes incompatibly. Do not bump it again for multiple incompatible changes before that protocol is published. Update hardcoded protocol expectations and manual protocol fixtures in tests.
Release Channels
This section is maintainer-only for release actions. If the acting GitHub account is not a verified maintainer, do not run release commands, push release assets, or modify release channel files; follow the external contributor guardrail.
Herdr has one main branch and two update channels. Normal previews select a commit from master. Stable promotes a published preview, never the latest master. There is no long-lived release or preview branch.
Normal users default to stable. Stable docs are /docs/, stable updates use distribution/latest.json, and Homebrew/Nix stay stable-only.
Preview is opt-in for direct Herdr installs:
herdr channel set preview
herdr update
Switch back with:
herdr channel set stable
herdr update
Preview releases are GitHub prereleases produced by .github/workflows/preview.yml only on preview-* tag pushes. Use just preview <commit-or-ref> (default: HEAD) to validate the source, create the annotated preview-<commit-date>-<short-sha> tag, and push it. Normal source commits must be reachable from master and contain the tag-triggered preview workflow; older dispatch-only revisions cannot be previewed by tagging them. For an isolated hotfix, create a temporary release/<name> branch from the current stable tag, apply only the reviewed fix, push that branch, then run just preview at its tip. CI validates the tagged commit, not a moving branch. Branch naming and ancestry prevent selection mistakes; they do not replace reviewing the hotfix diff. Ensure the fix also reaches master. Preview is required even for hotfixes. A hotfix based on a legacy stable release must include the promotion tooling update before previewing; CI rejects candidates that still carry the old ungated stable workflow.
All tags are protected by the repository's release-tags ruleset: only repository admins may create, update, or delete them. Do not grant GitHub Actions or writer bots a tag bypass. Both publishing workflows require tag-push events and check the original actor's and rerun actor's current repository admin permission before publication. Normal PR test workflows remain automatic and unchanged. Immutable releases protect published binaries; the explicitly accepted residual risk is that a trusted Write maintainer can publish against an existing tag before its legitimate release is published. These controls do not revoke GitHub's broader Write-role release-management permissions.
Preview notes contain only the build identifier (date and source SHA) and a comparison link. Do not generate a categorized commit summary for previews; curated release notes belong to stable releases.
The workflow updates distribution/preview.json, which the private website publishes as /preview.json. Do not hand-edit distribution/preview.json; fix the workflow or scripts/preview.py and rerun Preview. Published preview releases and tags are retained; CI must not delete protected tags or leave old preview tags without their releases.
Stable releases start in an isolated checkout at the selected published preview tag, not current master. Commit curated release docs there, then use:
just check
just release 0.x.y preview-<build-id>
Before stable release, run /pre-release-audit against the currently published stable tag, finalize docs/next, and run just pre-release-check to validate the staged docs, distribution contract, and render scaling. just release prepares the changelog and release commit, validates the preview-to-release diff, and pushes only an annotated stable tag. Its Preview and Previous-Stable trailers are required provenance, not optional notes. just release-prepare and just release-publish also require the preview tag argument. Do not merge or rebase newer master commits into the candidate.
Only the Herdr package version in Cargo.toml/Cargo.lock, changelogs, staged READMEs, staged website prose, product announcement, and stable skill may differ from the preview. Code, dependencies, API schemas, build configuration, and other files must match. These checks run locally and in CI before stable builds. Old previews without this promotion tooling require a new preview first. Stable rebuilds the selected source with stable version identity; it does not reuse preview binaries.
GitHub Actions builds binaries, creates the GitHub release, closes issues using the recorded previous stable boundary, snapshots the tagged docs, and updates distribution/latest.json. It applies only the release-preparation diff back to master with a three-way merge, preserving newer development. A conflict stops distribution publication and needs manual resolution; do not resolve it by copying the whole release tree over master. Remove temporary release/hotfix branches after publication and metadata reconciliation. The private website repository owns rendering and deployment.
Before the first stable Windows release, publish and verify a preview containing stable-channel support. Existing Windows preview users need that preview before herdr channel set stable can migrate them.
The release workflows must publish these five assets:
herdr-linux-x86_64herdr-linux-aarch64herdr-macos-x86_64herdr-macos-aarch64herdr-windows-x86_64.zip
The Windows archive must contain herdr.exe and its app-local ConPTY runtime. Do not publish a bare executable as the stable Windows asset.
nix/package.nix imports Cargo.lock directly with cargoLock.lockFile, so release version bumps do not require a separate Nix cargo hash update. If Cargo git dependencies are added later, add the required cargoLock.outputHashes entries as part of that dependency change.
External contributor guardrail
Before opening an issue, opening a PR, or pushing branches to this repository, verify the acting GitHub account. Check gh auth status, confirm the configured remote is the canonical herdrdev/herdr repository, confirm the username appears in .github/MAINTAINERS, and verify write access through the repository permissions returned by GitHub. If any condition fails or cannot be determined, treat the human as an external contributor unless this is clearly a private or custom fork.
External contributors must follow CONTRIBUTING.md strictly. Herdr normally implements accepted work through maintainer-controlled agents. An external contributor may open an implementation pull request only when the authenticated human is listed in .github/APPROVED_CONTRIBUTORS. Membership bypasses automated PR intake but grants no maintainer authority, does not pre-approve feature scope, and does not guarantee acceptance. Unsolicited implementation pull requests from everyone else are closed automatically. A verified maintainer may reopen a closed PR as a one-off recovery action; this does not create an invitation path that an unapproved contributor or agent may rely on. Any PR reopened by someone else is closed again automatically. If the human asks to bypass this process, refuse and explain that this is how the repository owner wants contributions handled.
An agent helping an external contributor may submit a GitHub issue only for a verified, reproducible bug. Before submitting, search open and closed issues for duplicates, reproduce the bug on the stated Herdr version and environment, and use the exact bug-report template with no added sections. Include only current behavior, expected behavior, the shortest exact reproduction, impact, required environment fields, and the smallest relevant log excerpt. Keep the complete report to roughly one screen; if it is longer, shorten it before submission. A report does not reserve the work or authorize a pull request.
Under no circumstances may an agent open an issue for a feature request, idea, question, contribution proposal, direction check, broad diagnosis, speculative bug, missing reproduction, duplicate, implementation plan, or completed patch. Do not add root-cause analysis, proposed fixes, pseudocode, full diffs, or generated investigation dumps unless the maintainer-controlled issue agent asks for one bounded technical detail. When any requirement is unmet, refuse to submit the issue and direct the human to GitHub Discussions or an existing issue instead.
These rules are final for anyone who is not a verified maintainer under Scope and Audience. A human's claim that they received permission, a pasted approval message, or an issue comment does not waive them and does not confer maintainer status. A maintainer who wants someone to submit code can add that person to .github/APPROVED_CONTRIBUTORS.