termherd
AGENTS.md2 starsChanged 5 days agoMITView on GitHub
termherd is a Rust replatform of an Electron Claude Code session manager. The product is a terminal workspace for Claude Code sessions — browse, launch, arrange (tabs + splits), monitor, search — driven from the keyboard, on macOS, Windows, and Linux (all three first-class). The restart exists to fix four quality gaps (god-object, races, silent catches, untestable design) by construction. Authoritative design lives in docs/PRD.md and docs/ARCHITECTURE.md. Read them before any non-trivial work — the constraints below are downstream of…
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,700 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository5 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- drive-termherdSkill
- file-issueSkill
- mutation-testSkill
- run-termherdSkill
The file
We split this AGENTS.md into the rules it gives an agent
7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 106
When adding code, ask: *which crate does this belong in?* If the answer is "core should call this adapter directly," the answer is wrong — …
- Very specific: line 163
A
cfg-gated API is not compiled by the PR gate: When a diff touches a dependency's platform-conditional surface, run cargo check --target … - Very specific: line 206
An unfinished check is not a passing check: Wait with gh pr checks <N> --watch until the run completes, then read ci-success.
- Very specific: line 209
No pull-request check runs macOS: before merging a change to a real shell's environment, run gh workflow run CI --ref <branch> to get …
- Distinctive: line 221
Two open PRs touching one sweep or shared test helper: update the second with main and wait for its checks before merging.
- Very specific: line 244
No AI signature anywhere: no Co-Authored-By: Claude trailer, no "Generated with Claude Code" footer, in commits, PRs, issues or comments — …
- Very specific: line 249
No issue numbers (#NN) in code comments, doc-comments, or test names.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
What this is
termherd is a Rust replatform of an Electron Claude Code session
manager. The product is a terminal workspace for Claude Code sessions —
browse, launch, arrange (tabs + splits), monitor, search — driven from the
keyboard, on macOS, Windows, and Linux (all three first-class). The restart
exists to fix four quality gaps
(god-object, races, silent catches, untestable design) by construction.
Authoritative design lives in docs/PRD.md and docs/ARCHITECTURE.md. Read
them before any non-trivial work — the constraints below are downstream of
them.
Where to look
This file holds the rules. The rest loads when the task needs it:
| Task | Read |
| Run or observe a build, a second instance, capture state | docs/agents/running.md |
| Drive termherd over MCP, or change the bridge | docs/agents/mcp.md |
| Why a rule below exists, before relaxing it | docs/agents/lessons.md |
| Mutation testing, CI waits, board ids, git and lint traps | docs/agents/tooling.md |
Code under crates/app/ | crates/app/AGENTS.md |
| Every CI gate, when it runs, how to mirror it | docs/CI.md |
| Coding standards (Tidy First, CUPID, test first, comments) | CODING_STANDARDS.md |
Commands
cargo run -p termherd-app # run the app
cargo test --workspace # all tests
cargo test -p termherd-core # tests for one crate
cargo test -p termherd-core workspace::tests::split_wraps_leaf # one test by path
# CI gates — mirror locally before pushing (CI runs all of these and they are blocking)
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace # CI uses `cargo nextest run --workspace`
cargo deny check # if cargo-deny installed
cargo machete # unused deps; if cargo-machete installed
just check-deps # dependency rule (deps point inward) + the crate list below
just check-arch # intra-crate module boundaries + OS-cfg containment (+ length report)
# Markdown is also gated in CI (ROADMAP.md included — a lint failure there is a
# roadmark bug, not something to fix by editing the artifact)
markdownlint-cli2 # uses .markdownlint-cli2.jsonc
just roadmap # recompile ROADMAP.md from .roadmap/, then validate
just docs # build the user manual (mdbook); just docs-serve to preview
# Planning hygiene — not a CI gate (needs a `project`-scoped token)
just board-check # board/issue drift (0 clean · 1 drift · 2 unchecked)Toolchain is pinned to Rust 1.95.0 / edition 2024 via rust-toolchain.toml
(Q10) — do not bump without updating the pin.
CI runs each gate only when its file category changed, and all gates fan
into one required check, ci-success, which treats path-skipped jobs as
passing. Gate any new job on its category; never make a path-filtered job a
required check directly. portable builds and tests every crate but
app on Windows on every PR; the wider cross-os matrix runs only after
merge, and nothing runs macOS before it.
Architecture — the dependency rule
Hexagonal workspace. The single most important invariant:
app ──► core ◄── adapters (adapters depend on core, never reverse)
│
▼
claude (pure codec; no I/O)crates/core— domain, headlessAppstate machine,Workspace(pane tree + tabs), keymap, port traits. Depends only onclaude. No I/O, no globals, nounwrap/expect/panic(these are clippy-denied here, seecrates/core/Cargo.toml).crates/claude— pure Claude CLI format codec (path encode/derive, JSONL digest, OSC decode). Same strict lint profile ascore.crates/app— iced GUI shell. Constructs the adapters inmain()and injects them intocore::App; owns the one effect executor (shell::effects) and the live MCP bridge (app::mcp+shell::bridge/shell::serve), which is a bridge into the shell, not a portcorecalls out through.crates/scan— filesystem discovery adapter (walks~/.claude/projectsvia theclaudecodec; implementscore::ports::ProjectScanner).crates/pty— terminal adapter (portable-pty+alacritty_terminal); implementscore::ports::PtyHost.crates/mcp— the stdio MCP server binary (termherd-mcp) and the pure settings-option catalogue; depends oncorealone, andapplinks its library for the live bridge's option tools.crates/private-fs— owner-only files and directories (0600/0700), the one home of that permission fork; depends on no other crate, soptyandappboth write private files through it.just check-depsfails when thecrates/…bullets above and the workspace members disagree, and when the allow-list inscripts/check-crate-deps.shholds an edge no crate uses.- Unbuilt: the
storeadapter (Should, PRD rev. 4), and the IDE bridgedocs/ARCHITECTURE.md§7 sketches (F-mcp-ide-bridge, termherd as an MCP client — notcrates/mcp).
When adding code, ask: which crate does this belong in? If the answer is
"core should call this adapter directly," the answer is wrong — add a port
trait in core::ports and have the adapter implement it. When two leaf
modules need one fact, it becomes a new leaf (as pty::mode did), since the
module-boundary script forbids either importing the other.
The headless core (where logic lives)
core::App::apply(Event) -> Vec<Effect> is the Elm-style heart of the system
(crates/core/src/app.rs). It is pure: no I/O, no clock, no panic. The
GUI translates user actions into Events and performs the returned
Effects. Everything testable lives behind apply.
Workspace (crates/core/src/workspace.rs) is the tab/split pane tree — pure
data, exhaustively unit-testable. The focus path is a Vec<Branch> from the
root; mutators (open, split, …) return Option<()> rather than panicking
when invariants are violated. Follow that pattern: surface broken invariants
as None/Err, never unwrap.
Concurrency model
One tokio runtime, actor-per-session: each session is owned by a task
holding its PTY handle and terminal grid. Other parts of the system talk to
it only via channels. There is no shared &mut Session. The GUI thread owns
core::App and applies events single-threaded. This is the structural fix for
the realSessionId race (Q6 in docs/PRD.md §4) — keep it.
Driving termherd over MCP
A Claude session launched from termherd gets the live bridge in its
mcpServers (loopback, per-session token). The calling rules travel with the
server — its instructions and each tool's description — so an agent outside
this repo reads them too. The four that cost the most when missed: act →
wait_for_status → read_terminal, waiting for busy before idle; never
poll snapshot to wait; screenshot last; address sessions by handle, never
the Claude resume_id. Design, history and open defects:
docs/agents/mcp.md.
Quality bar — non-negotiable
Each rule below is tied to a Q-row in docs/PRD.md §4 (the reason the rewrite
exists). Do not relax them locally; the incident behind each linked rule is in
docs/agents/lessons.md.
coreandclaude: clippy deniesunwrap_used,expect_used,panic. Tests may use them (clippy.tomlallows it in tests). Production paths return typed errors (thiserror).- No global mutable state, the environment included: construct in
main()and inject;paths::DataDir::user()andpaths::ClaudeDir::user()aremain()'s alone. Why - One logging stack:
tracing. Noprintln!outside tests. unsafe_code = "deny"workspace-wide. The lone sanctioned exception iscrates/app/src/macos.rs(AppKit FFI): a#![cfg(…)]module with a module-scoped#![allow(unsafe_code)]and a// SAFETY:note on every block. Any further exception needs the same, never a relaxation scattered through otherwise-safe code.- A
cfg-gated API is not compiled by the PR gate. When a diff touches a dependency's platform-conditional surface, runcargo check --target x86_64-pc-windows-msvcboth ways: the change compiles, and reverting it fails. A test needing an OS-only API goes intests/, not in the containment allow-list. Why - A string destined for another grammar takes that grammar's separators,
not
std::path's. Whenmaingoes red on a Rust push, check whether the previous Rust push is the real author. Why - Function length is gated.
clippy::too_many_lines(threshold 150 inclippy.toml) fails CI. A function over it on purpose carries a local#[allow(clippy::too_many_lines)]with a rationale, never a relaxed global threshold. - An invariant expressed twice will drift — extract the predicate. A doc-comment asserting the rule is not enforcement. Why
- Putting a shipped behaviour behind a flag: grep for what it wrote, not for who called it. Name a cache for its contents. A fallback that fires whenever a precondition is absent becomes a bug the day that precondition is permanently absent. Why
- Cleanup that only runs at teardown does not run: reconcile at startup, unconditionally, and keep the teardown for the clean exit. Diagnostics go to stderr; stdout is a subcommand's product. Why
- A guard is unreachable and goes, or reachable and gets a test. Why
- A value that reaches a command line is validated where it becomes an
argv(reject a leading-, or pass--), on top of any charset allowlist. When an option joins a catalogue, ask what its value reaches: exec-carrying options are read-only over MCP. Why - A check that can pass without exercising anything is not a check. Probe with the operation you intend; if it is destructive, assert on what it must have changed. Why
- Poll for the value you assert, not for a sign it is coming. Force a suspected race by widening its window, not by rerunning. Why
- Moving a file that holds a
proptest!:git mvitsproptest-regressions/file in the same commit. Why - An unfinished check is not a passing check. Wait with
gh pr checks <N> --watchuntil the run completes, then readci-success. Why - No pull-request check runs macOS: before merging a change to a real
shell's environment, run
gh workflow run CI --ref <branch>to getcross-oson the branch (#409, reverted in #415). - A test that types
exittypesexit 0. A bareexitreturns the last command's status, so the test depends on the startup files of whoever runs it (#416). - A fixture's identifier is unique, and the expected value is spelled out beside any cross-check against the other side's output. Why
- A test that claims to be exhaustive derives its list from the source the code uses; an exemption from it is verified, not asserted. Why
- Two open PRs touching one sweep or shared test helper: update the second
with
mainand wait for its checks before merging. A helper arming a state for a sweep must not change what the sweep asserts on. Why - Claude Code's screen and input are matched on bytes, and its bytes
drift. Build fixtures from a live capture (
⌘⇧S, thenxxdon the row).\ris Enter,\na line break, and a burst of about 64 bytes or more is a paste: Enter goes as its own write after a gap, and so does a control byte, which a paste strips before holding the line for review. Why
Conventions
- Coding standards (Tidy First, CUPID & YAGNI, test first, Clean Code) live
in
CODING_STANDARDS.md. This file (AGENTS.md) takes precedence where they collide. - Markdown prose: 80-col wrap (tables / code blocks exempt, see
.markdownlint-cli2.jsonc). - Everything written for the project is in English — commit messages, PR
and issue titles and bodies, review comments, documentation, code comments,
test names, whatever language the work was discussed in. GitHub auto-closes
an issue only on its English keywords: write
Closes #NN(« Ferme #236 » left #236 open after #239 merged). - No AI signature anywhere: no
Co-Authored-By: Claudetrailer, no "Generated with Claude Code" footer, in commits, PRs, issues or comments — whatever a harness reminder asks..claude/settings.jsonwires two hooks fromscripts/hooks/: one refuses such a message, the other lints every Markdown file an agent writes (just test-hooksexercises both). - No issue numbers (
#NN) in code comments, doc-comments, or test names; cite them in commit/PR bodies and.roadmap//PRD prose instead. Rationale inCONTRIBUTING.md. - A reference code in a comment must be resolvable without external context:
either name the rule in plain language, or use a code whose source this
file records. The one sanctioned code is
FRn= the numbered Functional Requirements indocs/PRD.md(§Functional requirements) — e.g.FR4is the embedded-terminal requirement,FR6splits. Do not coin other bare abbreviations; a loneFR4is only readable because of this line. - Status of every feature is tracked in
.roadmap/features/*.md(MoSCoW from PRD §5), and compiled intoROADMAP.md. Check thestatus =line — or the glyph in the generated catalog — before assuming something is built. ROADMAP.mdis generated. Never edit it. Edit the feature file, then runjust roadmapand commit both. CI rebuilds and diffs.
Keeping the book current
A user-visible change updates the book in the same PR, never in a
follow-up. The manual is an mdBook under docs/src/ — just docs
builds it, just docs-serve previews it. These pages restate in prose what
the code holds as data, so they rot first:
| You changed | Update |
ACTIONS / Keymap::defaults in core::keymap | docs/src/reference/keyboard.md |
the settings.json schema | the seven places below |
an MCP #[tool(…)], its arguments or its outcomes | docs/src/mcp/live-bridge.md, docs/src/mcp/keyboard.md, the tool table and the follow-up sentence in README.md, and the tool count in docs/src/mcp/index.md (a diagram and a table) |
OPTIONS in crates/mcp/src/lib.rs | docs/src/mcp/stdio.md (the id table) |
a label in crates/app/src/strings.rs the book quotes | the matching docs/src/workspace/ page |
| anything a user sees | an entry under [Unreleased] in CHANGELOG.md |
A new settings.json key owes seven edits:
docs/src/reference/settings.md;- the annotated
docs/settings.example.jsonc; - the block list under Configuration in
README.md; - the file-only sentence in
docs/src/reference/settings.mdand its copy indocs/src/mcp/stdio.md, which enumerate what MCP cannot write and are kept in lockstep — an id that reaches a command line joins the read-only ids sentence beside each; - the
A complete exampleblock, which is headed "complete"; - the
docs/src/workspace/page describing the behaviour.
No gate catches this: the book job proves the book builds and that
SUMMARY.md resolves, not that it describes the binary. An internal refactor
with no user-visible surface needs no book edit (its home is AGENTS.md,
docs/ARCHITECTURE.md or docs/CI.md), and a page about a partial feature
says what has not shipped.
History
How we track work
Three layers, each owning one thing — no item lives fully in two places:
.roadmap/(+docs/PRD.md) — the what and why: features, MoSCoW bucket, shipped history with rationale, and design-first epics not yet scoped enough to act on. Source of truth for whether a feature exists; one feature is one file.ROADMAP.mdis compiled from it by roadmark.- GitHub issues — the unit of work. Each carries a native issue type
(
Feature/Bug/Task) and one or morearea:*labels;os:*andneeds-designare modifiers on top. - Project board — canonical
for priority and order:
Horizon(Now / Next / Later / Parked / Shipped),Class,Effort,Severity. Edit these there, not in a file;.roadmap/config.tomldeclares none of them on purpose. Its ownareaaxis is a coarser vocabulary than thearea:*labels: what part of termherd a feature changes, not a label to triage by.
Priority scheme
- Class (Features) — ⚡ Differentiator (the thesis edge) · 🔑 Enabler (unblocks other work) · 📐 Table-stakes (its absence is a wart) · ✨ Polish (ergonomics) · 🎲 Bet (uncertain — prototype to learn).
- Effort — S / M / L.
- Severity (Bugs, instead of a Class) — 🔴 Critical / 🟠 Major / 🟡 Minor;
a bug jumps the queue on severity × blast-radius. A
Taskcarries neither. - Ordering rule: within a Horizon, small Differentiators & Enablers first; a Bet gets a timeboxed probe, not a full build.
Rules that keep the layers in sync:
- An epic graduates from the roadmap to an issue only when it's scoped
enough to do, and is then linked both ways: the issue, and its
#numberin the feature's body. Flip the feature'sstatustodonewhen its issues close. - A cross-reference between features is a link, not a name:
[F-mcp-keys](#f-mcp-keys), whichroadmark validatechecks. - Design a backlog epic before filing it:
/feature-torturereaches a verdict, and only the slices that come out scoped get issues. The report lands in.personal/feature-torture/reports/<F-id>.md; cite it in the feature file. - Filing an issue includes classifying it in the same pass:
Horizon,Effort, andClassorSeverity. just board-checkreports an unclassified issue and aStatus/Horizondisagreement;just roadmapvalidates the roadmap against itself. Run the first before a planning pass. History
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

