2026-10-16 · Tom Nguyen
Status (2026-10-20): 0.2.1 is released on PyPI (see CHANGELOG.md). Part 1 milestones M0–M6 are done and M7 is merged. M17 Phone (Telegram) is merged; its live check is pending. See milestones.md.
Build a terminal app where each agent (Claude, Codex) is a pixel pet walking around a retro office. Its movement shows what the agent is doing. The app reads live status from herdr, which is already installed, and the task board from GitHub Issues (or TASKS.md), so v1 needs no change to the agent setup.
- Look: a retro game in a character grid, with movement one cell at a time.
- Screen: designed for an 11-inch tablet in Termux, landscape first.
- Options: frame rate, sprite style (emoji, ASCII, block pixels), and a pet for each agent.
- Built with: Python + Textual, running in its own herdr tab.
Two ways to use it, built in this order:
| Phase | What | When |
|---|---|---|
| 1. CLI | tomville / tmv: the office in the terminal, plus one-shot commands |
The two-day weekend build |
| 2. Publish | One-line install, like herdr: curl -fsSL …/install.sh | sh, plus PyPI |
Right after the weekend |
| 3. Web UI | The same office in a browser: tmv web |
Later |
Both the CLI and the web UI use one shared core (event reader, world model, sprites, settings), so the web UI adds a new front end, not a second app. See Roadmap.
All in place on the development tablet (Ubuntu 26.04 in Termux PRoot, aarch64):
- herdr 0.9.5 in
~/.local/bin, onPATH - herdr integrations for Claude and Codex (
herdr integration install claude|codex) - herdr agent skill in
~/.claude/skills/herdr/SKILL.md - Claude Code and Codex CLI, both logged in
-
ghlogged in, with access to this repo
Still needed at build time: Python 3.11+ and uv (install steps in Install).
Things learned on the tablet that every setup there needs:
| Problem | What you see | Fix |
|---|---|---|
npm Codex starts through a Node launcher, so herdr sees node (process name MainThread), not codex |
herdr agent start … --kind codex times out; the agent never gets its name |
A ~/.local/bin/codex wrapper that execs the real binary (tmv setup launcher codex); ~/.local/bin first on PATH |
| Codex's background server can't run in PRoot | Codex panics with Function not implemented (os error 38) and says to rerun with --no-daemon |
Start Codex with --no-daemon (also for resume and fork) |
| Codex's sandbox can't start in PRoot | Every command fails with app-server socket directory has an unsupported host mount |
Start Codex with --sandbox danger-full-access; approvals then follow --ask-for-approval |
| A crashed Codex leaves its session locked | "This conversation is open in another app" | codex --no-daemon fork <session> keeps the context in a new, unlocked session |
| Claude Code asks whether to trust a new folder | herdr agent start reports the agent blocked during startup |
Answer it yourself in that pane; Tomville never answers it |
A shell that already ran codex remembers the old path |
The launcher is still used after the wrapper is installed | hash -r, or start in a fresh pane (tmv up always does) |
| Agents start slowly | herdr agent start times out at its 30 s default |
--timeout 120000; Tomville uses start_timeout (120 s) |
Landscape on an 11-inch tablet, about 140 × 40 cells:
┌ Tom's ville · myproject ────────────────────────────────────────┬ Team ──────────────────────────┐
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ 🐱 claude boss working │
│ ▓ ┌ BOARD ───────────────────┐ 🐱 ▓ │ 🐶 codex1 worker BLOCKED ! │
│ ▓ │ Todo Doing Review │ ┌──────────┐ ▓ │ 🐑 codex2 worker idle │
│ ▓ │ #14 #12 #9 │ │ BOSS │ ▓ ├ Events ────────────────────────┤
│ ▓ │ #15 │ └──────────┘ ▓ │ 10:42 codex1 needs approval │
│ ▓ └──────────────────────────┘ ▓ │ 10:40 claude → codex1: #12 │
│ ▓ ▓ │ 10:40 #12 → Doing (codex1) │
│ ▓ ┌────────┐ ┌────────┐ ┌────────┐ ▓ │ 10:31 #9 → Review (codex2) │
│ ▓ │ codex1 │ │ codex2 │ │ │ ▓ │ │
│ ▓ └────────┘ └────────┘ └────────┘ ▓ │ │
│ ▓ 🐶❗ ▓ │ │
│ ▓ ▓ │ │
│ ▓ LOUNGE 🛋 🐑 z z REVIEW ▭ DOOR ▯ ▓ │ │
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │
└─────────────────────────────────────────────────────────────────┴────────────────────────────────┘
[F]ps 10 [S]prites emoji [G] board check 60s [Tab] next agent [Enter] open pane [?] help
The boss sits at the top, behind the boss desk, facing the workers. The task board hangs on the wall next to the boss. Each worker has a desk with its name on it, facing the boss. Empty desks wait for new workers.
In portrait, the sidebar moves under the office floor.
| herdr status | Pet goes to | Animation | Extra |
|---|---|---|---|
working |
Its desk | Typing: 2 frames, keys flicker | Task ID in a speech bubble |
blocked |
Stays where it is | Jumps, ❗ blinks |
Name turns red in the Team list; event logged |
done |
Review zone | Walks over, drops its card | Card appears on the board's Review column |
idle |
Lounge | Wanders slowly, then sleeps (z z) |
Sleeps after 60 s idle |
unknown |
Stays where it is | Stands still, ? bubble |
|
| Agent gone | Leaves through the door | Walks off-screen | Removed from the Team list |
When the lead assigns a task (an issue gets status:doing and a worker's agent: label), the lead pet walks to the worker's desk and hands over a card.
herdr has no idea of a "team": any agent can prompt any other. Tomville supports two common shapes in v1 and draws a different office for each. You pick the shape in settings.toml or with --team.
One lead (the boss) hands tasks to workers. The office in What it looks like is this shape: the boss at the top behind the boss desk with the board beside it, and a named desk per worker, facing the boss.
The handover, step by step:
| Step | What happens | How Tomville knows | What you see |
|---|---|---|---|
| 1 | Boss puts a task on GitHub for a worker | Issue created or labelled agent:codex1 (from tmv task at once; from elsewhere on the next board check) |
Card appears in Todo on the board |
| 2 | Boss tells the worker | tmv send codex1 "…" in the message log |
Boss pet stands up, walks to codex1's desk; bubble: "#12 is yours" |
| 3 | Worker checks the board | Worker reads GitHub Issues (or TASKS.md) |
Boss walks back; worker bubble: "checking issues…" |
| 4 | Worker takes it | tmv task take 12: status:doing |
Card moves to Doing; bubble: "on it: #12"; worker types |
| 5 | Worker finishes | Worker done; issue status:review |
Worker walks to the boss desk; bubble: "#12 ready for review"; card to Review |
| 6 | Boss reviews and closes | Issue closed (Fixes #12) |
Card to Done; boss bubble: "merged ✓" |
Changes made through tmv task and tmv send move pets at once, because Tomville logs them locally. Changes made elsewhere (the GitHub website or app) show up on the next board check.
Team rules (in each project's AGENTS.md):
- Only the lead prompts other agents, with
tmv send. - Workers never use herdr, not even to read: no
herdrcommands, notmv up,tmv downortmv send, no creating or closing panes, tabs, workspaces or worktrees. Only the lead does, after the user approves. A Check that needs real herdr goes in the PR for the lead to run. - An optional secretary (Claude Haiku) writes no code. When the lead asks, it files GitHub issues and emails the user at each step (PR opened for review, PR merged, worker blocked, milestone done), always with what changed: the changes in plain words, each file with its added and removed lines, and the worker's full summary comment. It emails only the one address in its
[agents.secretary]table.
Each agent does one step and passes the work on, for example planner → coder → tester. The office is a row of stations joined by a conveyor belt:
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓ PLAN CODE TEST DONE ▓
▓ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ▓
▓ └──┘ └──┘ └──┘ └──┘ ▓
▓ 🐱 z z 🐶⌨ 🐑❗ ▓
▓ ═════▶══════════[T1]▶═════════════▶════════════════▶═══ ▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
| What happens | How Tomville knows | What you see |
|---|---|---|
| A step finishes and the next starts | Stage N goes done and stage N+1 goes working within 30 s |
The card rides the belt to the next station |
| A step is stuck | That stage is blocked |
Its pet jumps with ❗; the belt stops behind it |
| The last step finishes | Last stage goes done |
Card drops into DONE |
The status table above still applies to each pet; the shape only changes where desks are and how cards move.
[team]
shape = "lead" # lead | pipeline
lead = "claude" # lead shape: who sits at the front desk
# pipeline shape: stage order, left to right
# shape = "pipeline"
# stages = ["planner", "coder", "tester"]If [team] is missing, Tomville uses the lead shape with the first agent it sees as the lead. Agents not listed in stages wait in the lounge.
Other shapes (pair/reviewer, parallel fan-out with worktrees, peer group, across machines) are in Later.
The board's main source is GitHub Issues: each issue is a card. The board is a plug-in, like team shapes, so a project can use TASKS.md instead (offline, instant) by changing one setting.
| Board idea | GitHub Issues (default) | TASKS.md |
|---|---|---|
| Card | An open issue, #12 Fix login test |
A row |
| Column | Label status:todo, status:doing, status:review; closed = done |
The row's status |
| Who has it | Label agent:codex1 (agents aren't GitHub users, so no assignees) |
The row's agent |
| Finished | A commit or PR with Fixes #12 closes it |
Row set to done |
| Notes | Issue comments (gh issue comment) |
None |
| Updates | Checked every check_seconds (you choose) |
Instant, on file change |
[board]
source = "github" # github | tasks-md
check_seconds = 60 # how often Tomville checks GitHub; you choose (minimum 10)
repo = "" # owner/name; empty = the project's git remotecheck_secondsis the user's trade-off: lower moves cards sooner, higher saves battery and network. Change it in the app (Gkey cycles 15/30/60/120 s), with--board-check SEC, ortmv config set board.check_seconds 30.- Each check is one GitHub API call for issues updated since the last check, sent with the last
ETag; an unchanged board returns304and does not count against GitHub's rate limit. - When GitHub can't be reached, the board keeps the last cards and shows
offline since 10:42; pets keep moving from herdr data. - Agents use
tmv taskinstead of longghcommands:tmv task new "Fix login test",tmv task take 12(addsstatus:doingand the agent's ownagent:label),tmv task move 12 review,tmv task list. They callghunderneath, and the change shows on the next check. tmv setupcreates the labels in the repo and adds the board rules toCLAUDE.md/AGENTS.md.
For the offline source, set [board] source = "tasks-md" or run tmv --board tasks-md.
The project file uses this table (extra columns are allowed):
| id | title | status | agent |
| --- | --- | --- | --- |
| T1 | Fix login test | doing | codex1 |
| T2 | Review changes | review | claude |Status is todo, doing, review, or done; use - for no agent. The reader
checks the file's modified time every poll_seconds and keeps the last cards if
it becomes unreadable. --board PATH selects another Markdown file.
Agents and Tom share Markdown docs. Project docs (README, docs/, AGENTS.md, any .md in the repo) change through the normal branch, PR and review. Team notes live in notes/ of the project's main checkout, outside branches; every worktree finds it through git rev-parse --git-common-dir, and notes are edited live.
tmv doc rules (safety):
- Only
.mdfiles, only under the folders in[docs] roots(default[".", "notes"]: the repo andnotes/), never outside the project, and never through a symlink that points out of it. tmv doc read PATH --section "Setup > Linux"picks a heading path; a section ends at the next heading of the same or higher level.#lines inside fenced code blocks are not headings. A missing section or a name that matches several headings is an error that lists the choices.- Every read prints a version tag for the whole file;
tmv doc edit --base TAGsays which version you edited from.
Safe edits (nobody silently overwrites anybody):
-
tmv doc read notes/plan.md --section Beta, thenprintf '## Beta\nnew\n' | tmv doc edit notes/plan.md --section Beta --base TAG. Without--sectionthe whole file is edited;--appendadds to the end of the section or file. -
The write takes a lock (
flockon a file in.tomville/doc-locks/), writes a temp file,fsyncs andos.replaces it. The text of every version you read or write is kept by tag in.tomville/doc-objects/(also the history for the change log). -
File unchanged since
TAG: written. Changed by someone else (an agent, or Tom saving in any editor, which needs no lock): your edit is merged against the new content when the changes touch different lines. Same lines: refused with exit code 3 and the other change shown; read again and redo it. -
--mergewrites git-style markers (<<<<<<< ours/=======/>>>>>>> theirs) instead and marks the doc conflicted. Everyeditis refused until the markers are fixed andtmv doc resolve PATHpasses. Change log: -
Every
doc new,edit,resolveandrevertappends a line to.tomville/doc-log.jsonl: time, who, path, section, op, version before and after. Who is the agent whose herdr pane runs the command (astmv taskfinds it), else--as NAME, elsetom. -
A save made outside tmv (Tom's editor) moves the hash with no log line. The next doc command that sees it logs it as
tom (editor); the firstdoc readof a doc records a startingseenline so later outside edits can be told apart. -
tmv doc listtakes last editor and last change from the log (git, then file time, for docs not in it yet). -
Old versions live in
.tomville/doc-objects/. The last[docs] keep_versions(default 50) versions of each doc are kept; versions that were read but never logged expire after a week. -
Project docs (outside
notes/) are written in your own worktree, so the PR flow still applies; onlynotes/is shared in the main checkout..tomville/doc-*and/notes/are git-ignored.
The team sees doc changes:
tmv watchalso exits when the doc log gets a new entry or someone saves a file innotes/outside tmv (new files too). It printsDoc notes/plan.md changed by codex1 · section Rollout(created by,reverted by,resolved conflicts infor the other operations). The lead handles it like a board change. Changes the watching agent made itself are skipped. The first run is a silent baseline; its state is.tomville/watch-docs.json.- Office: the Events panel shows
codex1 edited notes/plan.md › Rollout, and a 📄 slides from the writer's desk (or the door, for Tom) to the shelf, the review area, and disappears. - Secretary: its emails get a
Doc changespart (see AGENTS.md, "The secretary"). - Telegram (when on):
/doc read PATH [section]sends the text, trimmed to 3000 characters;/doc watch PATHpings📄 codex1 edited notes/plan.md › Rolloutwhen that doc changes;/doc unwatch PATH,/doc watching. It reads only whattmv doc readmay read.
[docs]
roots = [".", "notes"]
keep_versions = 50 # versions of each doc kept for diff and revertherdr shows what each agent is doing, but not who sent a prompt to whom: when the lead runs herdr agent prompt codex1 "...", only the lead gets a reply, and herdr's event stream has no "prompt sent" event or sender field (checked against the herdr 0.9.5 API schema). Tomville records messages itself, in a log both shapes use:
| Way | How it works | Reliability |
|---|---|---|
tmv send |
Agents send work with tmv send codex1 "Fix the login test". Tomville finds the sender from HERDR_PANE_ID (herdr sets it in every pane), calls herdr agent prompt itself and appends the message to the log |
Exact, when agents use it. tmv setup adds a short rule to the project's CLAUDE.md and AGENTS.md telling agents to use it |
| Claude Code hook | A PreToolUse hook on Bash catches any herdr agent prompt … Claude runs directly and logs it with the sender's pane |
Exact for Claude as sender, even if it forgets tmv send. tmv setup installs it. Codex: check whether it has an equivalent hook |
| Timing guess | Sender was working, then the target became working within a few seconds |
Fallback only; shown as ? → codex1 |
The log is one JSON line per message in .tomville/messages.jsonl in the project:
{"time": "2026-10-17T10:42:03Z", "from": "claude", "to": "codex1", "text": "Fix the login test", "via": "send"}What you see:
- The sender's pet walks to the target's desk carrying
✉; a speech bubble shows the start of the prompt. - The Events panel shows
10:42 claude → codex1: Fix the login test. - In the pipeline shape, a logged handoff moves the card at once; the timing guess is used only when there is no log line.
- An unknown sender shows as
?, so the office never claims more than it knows.
herdr events (status changes) ─┐
messages.jsonl (tmv send, hook)├─▶ Event reader ─▶ World model ─▶ Renderer (Textual) ─▶ screen
GitHub Issues / TASKS.md ─┘ pets, zones, sprite frames,
targets, paths frame timer
▲
settings.toml ──────┘ (fps, sprite style, pets)
| Part | Job |
|---|---|
| Event reader | Runs herdr agent list once at start, then listens to herdr's live event stream (pane.agent_status_changed, pane.agent_detected, pane.closed) instead of polling. Also follows the message log and reads the board (GitHub Issues every check_seconds, or TASKS.md when it changes). Turns them into events: "codex1 became blocked", "claude → codex1: Fix the login test", "T1 moved to review". Falls back to polling herdr agent list every poll_seconds if the stream drops |
| World model | Holds each pet's position, target zone and current animation. Events change targets; it does not draw anything |
| Movement | Each tick, a pet steps one cell toward its target: first along x, then along y, around walls |
| Renderer | A Textual widget that draws the office map, then each pet's current frame at its position. Textual only redraws changed cells |
| Settings | settings.toml, changed in the app with hotkeys or from the shell with tmv config set |
The renderer never calls herdr directly, and the event reader never draws. That keeps the game part testable with fake events and no agents running.
Movement speed is set in cells per second and does not depend on fps. Changing fps only changes how smooth it looks, not how fast pets move.
| Option | Feel | Use when |
|---|---|---|
| 5 fps | Choppy, very retro | Battery saver |
| 10 fps (default) | Classic handheld game | Normal use |
| 15 fps | Smooth walking | Tablet plugged in |
| 30 fps | Smoothest | Testing; heavy on battery under PRoot |
Two automatic savings:
- When every agent is idle and every pet is asleep, the app drops to 2 fps.
- When the app's tab is not visible, it stops animating and only keeps polling.
Press F to cycle through the options; the choice is saved.
The core rule: every map tile is 2 cells wide. Emoji are 2 cells wide in most terminals, so one emoji fills one tile. ASCII and block sprites also use 2-cell tiles. All three styles share the same map and stay aligned.
How it stays aligned:
- Fixed pet list. Pets come only from a tested list of 2-cell emoji (🐱 🐶 🐑 🐸 🦊 🐼 🤖 👾). Skip emoji built from several parts (skin tones,
🧑💻), because they render at different widths on different fonts. - Width check at start. The app measures each sprite with Rich's
cell_len. Anything that is not exactly 2 cells is replaced by its ASCII version, with a note in the Events panel. - First-run calibration screen. It shows a ruler and a row of every pet:
If the bars line up, press Enter. If not, the app switches to ASCII sprites. This catches fonts that measure differently from what Python expects.
|..|..|..|..|..|..|..|..| |🐱|🐶|🐑|🐸|🦊|🐼|🤖|👾| - Screen size. The map is 64 tiles × 24 rows (128 × 24 cells) on an 11-inch landscape screen at a normal Termux font size. Smaller windows get a 40 × 20 tile map, and the sidebar moves below it. Pinch-zoom in Termux changes the cell count, and the app re-lays out on resize.
Press S to cycle through styles. Each pet has the same frames in each style.
| Style | Idle | Walk frames | Typing | Notes |
|---|---|---|---|---|
| Emoji | 🐱 |
🐱 / 🐱 with a dust · behind it |
🐱⌨ |
Default on the tablet |
| ASCII | ^^ |
^^ / ^- |
^^# |
Works on every font |
| Block pixel | ▟▙ |
▟▙ / ▙▟ |
▟▙▒ |
Most "retro game" |
Sprites are plain text files, one folder per pet. Built-in pets ship inside the package; your own go in ~/.config/tomville/sprites/ (see Custom pets and colours):
src/tomville/data/sprites/
cat/
emoji.txt # frames separated by a line with ---
ascii.txt
blocks.txt
dog/ ...
~/.config/tomville/settings.toml:
fps = 10
style = "emoji" # emoji | ascii | blocks
idle_sleep_seconds = 60
poll_seconds = 1
[pets.claude]
sprite = "cat"
[pets.codex1]
sprite = "dog"
[pets.codex2]
sprite = "sheep"Colours and your own pet designs are covered in Custom pets and colours.
| Key or touch | Action |
|---|---|
Tap a pet, or Tab |
Select it; the Team list highlights it |
Enter on a selected pet |
Switch herdr to that agent's pane (to approve, read output) |
F |
Cycle frame rate |
S |
Cycle sprite style |
B |
Show the board full-screen |
T |
Cycle colour themes |
G |
Cycle how often the GitHub board is checked (15/30/60/120 s) |
? |
Help |
v1 only watches. Approvals still happen in the agent's own pane: the blocked pet tells you where to go, and Enter takes you there. Sending prompts from the app comes later (see Later).
You can design both the pet and its colours. How much depends on the sprite style:
| Style | Shape | Colour |
|---|---|---|
| Emoji 🐱 | Pick from the tested emoji list | Fixed by the font. Only the name label takes a colour |
ASCII ^^ |
Any characters | One colour per pet |
Block pixel ▟▙ |
Draw pixel by pixel | Any colour, per pixel. Best for your own designs |
Termux supports 24-bit colour, so any hex colour works.
Each frame has two grids side by side: the shape on the left, colour letters on the right. A [palette] maps each letter to a hex colour. Frames are separated by ---.
~/.config/tomville/sprites/fox/blocks.txt:
# shape # colours
▄▀▀▄ OOOO
█▄▄█ OWWO
▀ ▀ B B
---
▄▀▀▄ OOOO
█▄▄█ OWWO
▀▀ BB
[palette]
O = "#ff8800" # orange fur
W = "#ffffff" # white face
B = "#5a3a1a" # brown feet
Rules:
- A pet is a whole number of 2-cell tiles wide; this fox is 2 tiles (4 cells).
- Every frame of one pet has the same size.
- A space in the colour grid means "no colour".
- The first frame is used when standing still; all frames play in a loop when moving or working.
Your own pets go in ~/.config/tomville/sprites/<name>/ and override a built-in pet with the same name. Run tomville pets list to preview them all, and tomville pets check <name> to find mistakes in a pet file.
In ~/.config/tomville/settings.toml:
theme = "night" # night | game-boy | green-screen
[pets.claude]
sprite = "cat"
color = "#f5a97f" # the agent's colour: ASCII pet, name, board cards, events
[pets.codex1]
sprite = "fox" # your own designThree built-in themes. Press T in the app to cycle them.
You answer a few questions once; Tomville builds the team in herdr. Nobody types herdr commands. Lead + workers shape only for now.
tmv new # the questions, saved in .tomville/ville.toml
tmv up # builds the team (no ville yet? it asks the tmv new questions first)
tmv down # closes only the office; agents keep working
tmv down --agents # confirms, then stops all configured agents and the officeQuestions. For each agent: its name, its kind (claude, codex, gemini, … any kind herdr agent start --kind knows), its model (free text passed as --model; empty means the CLI's own default, so the list never goes stale) and its place:
| Place | What Tomville makes |
|---|---|
here |
The pane you are in (the lead only) |
worktree |
A git worktree on its own branch, in a new tab (default for workers) |
tab |
A new tab in the project folder |
pane |
A split of the current pane |
Then: "Add a secretary (Claude Haiku) to file issues and email you when work is finished?" and the email address it may write to (default: git config user.email). The answers become [agents.NAME] tables:
[agents.claude]
kind = "claude"
model = "claude-opus-5-5"
place = "here"
[agents.codex1]
kind = "codex"
place = "worktree"
[agents.secretary]
kind = "claude"
model = "claude-haiku-5-5"
place = "tab"
role = "secretary"
email = "tom@example.com"tmv new ends with "Start the team now?". Answering yes starts the saved team immediately; --yes saves the defaults without starting agents. With an existing ville, tmv up asks no questions and starts agents sequentially, then opens the office. Running it again reuses running agents and the office. Pane workers stack beside the lead: the first splits right from the lead, and each later worker splits down from the previous pane worker.
Who is the lead. Run tmv up inside an agent's pane and that agent is the lead; no second lead is started. Run it in a plain shell and Tomville starts the lead too, in a new tab.
If herdr can't detect an agent, Tomville names the pane, the running process and the last lines, with the known fix: a script launcher in front of the real binary (tmv setup launcher codex), a startup prompt such as folder trust (answer it yourself), a CLI that is not installed, or a shell that remembers an old path. tmv doctor runs the same checks without starting anything. It also reports the installed herdr version and warns outside the tested range. A herdr usage or argument error reports a CLI version mismatch instead of suggesting agent detection fixes.
tmv down and tmv down --office close only the office tab recorded in .tomville/team.json; every agent keeps working. tmv down NAME stops one agent owned by tmv up and keeps everything else.
tmv down --agents stops every running agent configured in the ville, including agents that were already running before tmv up, then closes the office. It lists their statuses first, marks working agents as busy and blocked agents as waiting for an answer, and asks once Stop them? [y/N]. Pressing Enter declines. Use --yes to skip confirmation; non-interactive use requires it. Workers stop first, then the secretary, then the lead. The caller's pane is reported as kept: lead/caller and left running.
Owned panes are closed after their agent quits. Pre-existing panes stay at their shell, reported as stopped (pane kept). Worktrees and agents outside the ville always stay. The git-ignored ownership record is updated for successful stops; panes with replaced or uncertain identities are retained for inspection. --office, --agents and NAME are mutually exclusive.
If startup stops at a trust or login question, answer it in the named pane yourself and retry tmv up. The pending first message is sent once the agent is ready. A failed start whose pane is still open is retained for inspection; use tmv down NAME before starting a replacement. Failed agents do not prevent the rest of the team from starting.
tmv setup launcher codex supports npm Codex's native binary and preserves its package environment. It asks before writing ~/.local/bin/codex, keeps an existing file as codex.bak, and refuses to overwrite an existing backup. --yes accepts the write. Other launcher kinds report that automatic resolution is unsupported instead of guessing a binary or its environment. Put ~/.local/bin first on PATH and use a fresh pane afterwards. Startup uses start_timeout seconds (default 120); herdr requires at least 3001 milliseconds, so smaller configured values use that minimum for the agent-start call.
Talk to your ville from your phone. It is optional: with Telegram off, nothing about it runs, asks or shows (tmv doctor only says "Telegram: not set up (optional)").
Lead + workers only, for now. For any other team shape, tmv new skips the question and says Sorry, we haven't done Telegram for the <shape> team yet.
One bot per ville. Each ville gets its own bot, so it is its own chat on your phone, named after the ville. Why not one shared bot: Telegram lets only one program at a time receive a bot's messages, so two villes on one tablet would steal each other's messages. With one bot per ville each runs on its own, the chat is never mixed up with another project's, and stopping one ville never touches another.
- Last question of
tmv new("Connect a Telegram chat to this ville? (no)"), or any time latertmv telegram setupin the project. - In Telegram, talk to @BotFather:
/newbot, a name (Tomville suggests<project> ville), a username ending inbot. It gives you a token. - Paste the token when asked (hidden input). Tomville checks it and shows the bot's username.
- Send the 6-digit code Tomville shows to that bot from your phone. Only your private chat with it is paired.
- Choose who may message you here (
from, defaultsecretaryandlead) and whether to allow approvals from the chat.
The ville keeps no secret: [telegram] enabled, bot, id, approvals, from in .tomville/ville.toml. The token and your chat ID live in ~/.config/tomville/telegram/<id>.toml, mode 600. tmv up opens a telegram tab with the bot. tmv telegram off stops it and deletes the token.
| You type in the ville's chat | Goes to |
|---|---|
Plain text, e.g. start issue #12 |
The lead |
/claude2 check the board layout |
That agent (one /<agent> per agent; codex-1 becomes /codex_1) |
/status, /board, /pending |
Team, board and waiting agents |
/pause, /resume, /help |
Stop and restart forwarding and pings; help |
The Telegram command menu lists them. Every message is logged as from tom (via Telegram); only the lead answers, with tmv reply.
- Answers from the lead (
tmv reply "text"). - Summaries from the secretary: the same content as its emails (what changed, files, the worker's summary), in bold and italic; when code changed,
tmv reply --diff PRsends the diff per file in code blocks. Email stays bold and italic only. - Pings from the bot itself when an agent is blocked, or a card moves to review or done.
- Only agents in
[telegram] fromcan post;tmv replyfrom anyone else says so.
With approvals = true, a blocked agent's exact question arrives with ✅ Approve and ❌ Decline. Before the key is sent, the bot reads the pane again and checks it is still the same question; if anything changed the tap does nothing and says so. Buttons expire after 10 minutes, and only your paired chat counts. A question it cannot read clearly gets no buttons.
- Your message shows as a 📱 coming in at the door and walking to the target's desk, with a bubble of the start of the text. You have no pet.
- A reply shows as the sender's bubble
📱 → tom: …; an approval as a ✅ or ❌ bubble over the agent. - The Events panel shows
📱 tom → codex1: ….
Only your private chat is answered. The bot has no shell: it forwards text to agents and presses a key for an approval, nothing else. It uses long polling from the tablet, so there is no server, no open port and nothing to host.
One command installs it, and tomville doctor shows the whole setup in the terminal: what is ready, what is missing, and the command to fix it.
After publishing (phase 2), one line, the same way herdr installs:
curl -fsSL https://raw.githubusercontent.com/DuongAnh1201/tomville/main/install.sh | shOr, for people who already use Python tools:
uv tool install tomville # or: pipx install tomvilleUpdate with tmv update; remove with tmv uninstall. Settings in ~/.config/tomville/ stay.
During the weekend build (phase 1), install straight from GitHub:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install git+https://github.com/DuongAnh1201/tomvilleWhile the repo is private, this works only where gh is logged in with access, as on the development tablet.
tomville works like claude and codex: run it bare for the interactive app, add options to change how it runs, or use a command for a one-shot job that prints and exits. The installer also adds a short alias, tmv.
tomville [options] open the office (interactive)
tomville <command> [options] run one command and exit
tomville <command> --help help for one command
Every option is optional. With none, settings come from settings.toml, then built-in defaults.
| Option | Default | What it does |
|---|---|---|
-C, --project DIR |
current folder | Project whose board is shown |
--config FILE |
~/.config/tomville/settings.toml |
Use another settings file |
--json |
off | Machine-readable output, for scripts and agents |
--no-color |
off | Plain text; also on when NO_COLOR is set |
-q, --quiet / -v, --verbose |
normal | Less or more output |
-h, --help |
Show help | |
-V, --version |
Show the version |
| Option | Values | What it does |
|---|---|---|
--fps N |
5 10 15 30 |
Frame rate for this run |
--style S |
emoji ascii blocks |
Sprite style |
--theme T |
night game-boy green-screen |
Colour theme |
--pet AGENT=PET |
e.g. codex1=fox |
Pet for one agent; repeat for more |
--board SOURCE |
github tasks-md FILE |
Board source for this run; a path means a TASKS.md-style file |
--board-check SEC |
number | How often to check GitHub Issues (default 60) |
--layout L |
auto landscape portrait |
Force a layout |
--team SHAPE |
lead pipeline |
Team shape for this run (see Team shapes) |
--poll SEC |
number | Polling interval if herdr's event stream is unavailable (default 1) |
--demo |
Fake agents and tasks; works without herdr. Good for trying pets and themes | |
--still |
No animation; pets jump straight to their zone |
| Command | What it does | Useful options |
|---|---|---|
send AGENT TEXT |
Prompt another agent and log who sent it (see Who talks to whom). For agents to use instead of herdr agent prompt |
--wait, --timeout MS (passed to herdr) |
status |
Print the team and board once, then exit. Like claude -p: no app opens |
--json, --watch [SEC] (reprint every N s), --agent NAME |
doctor |
Check the setup and print fixes | --json |
new [NAME] |
Create a new ville for this project: name, team shape, agents (kind, model, place), secretary, pets, board, emoji check, and last "Connect a Telegram chat?" (default no). Saved in .tomville/ville.toml. Bare tomville offers this when the project has no ville yet |
--yes (accept defaults), --claude-settings FILE (hook file, default ~/.claude/settings.json) |
telegram setup / run / status / pause / resume / off |
Optional phone chat for this ville (see Phone (Telegram)). off stops the bot and deletes its token |
|
reply TEXT |
Send a reply to Tom's chat. For the lead and the agents in [telegram] from |
--diff PR, --diff-file FILE (diff per file in code blocks) |
doc list [DIR] |
List docs with title, last editor and last change (default notes/) |
--json |
doc read PATH |
Print a doc and its version tag (a short content hash of the whole file) | --section "A > B" (that heading down to the next one of the same or higher level), --json |
doc new PATH |
Create a doc from a small template; refuses if it exists | --title TEXT (required), --json |
doc edit PATH |
Edit a doc or one section, new text on stdin. --base TAG is the version you read; if the file changed meanwhile, the edit is 3-way merged. Overlapping changes are refused (exit 3, shows the other change). tmv doc new and edit are the only write paths |
--section "A > B", --base TAG (required), --replace (default; give the section's heading line too) / --append, --merge (write conflict markers instead of refusing), --as NAME, --json |
doc resolve PATH |
Clear the conflicted mark once no <<<<<<< / >>>>>>> lines are left |
--json |
doc log PATH |
Who changed what: time, who, operation, section, version before → after | -n N (last N, default 20; 0 = all), --json |
doc diff PATH |
Unified diff from a stored version to the file now | --since TAG (default: the version before the latest change), --json |
doc revert PATH TAG |
Restore an old version as a new edit: same lock, merge and conflict rules as edit, logged |
--base TAG, --merge, --as NAME |
up |
Build the team in herdr from the ville (see Start your team); with no ville it asks the new questions first. Safe to run again |
|
down [NAME] [--office | --agents] [--yes] |
Default/--office: close only the office. NAME: one owned agent. --agents: confirm, then stop all ville agents and the office; pre-existing panes and the caller stay |
|
setup launcher KIND |
Write a ~/.local/bin wrapper that runs the real binary, so herdr can detect it (asks first) |
|
setup |
Redo parts of setup for this ville: emoji calibration, labels, agent rules, hook | --yes (no questions; skips the emoji test unless --style is given), --style, --theme, --claude-settings FILE |
pets list |
Show every pet, animated | --style |
pets show NAME |
Show one pet's frames and palette | --style, --fps |
pets new NAME |
Create a pet file to edit | --from cat (copy a built-in), --size 2x3 (tiles × rows) |
pets check NAME |
Check a pet file: sizes, palette letters, emoji width | |
theme list |
Preview all themes | |
config path / config show |
Print the settings file location or contents | |
config get KEY / config set KEY VALUE |
Read or change one setting, e.g. config set fps 15 |
|
update (phase 2) |
Upgrade to the latest release, like herdr update |
--check (only report) |
uninstall (phase 2) |
Remove the app; keep or delete settings | --purge (also delete ~/.config/tomville/) |
web (phase 3) |
Serve the office in a browser | --port 8787, --host 127.0.0.1 |
completion SHELL (later) |
Print tab-completion for bash, zsh or fish |
The highest line wins:
- Command-line options (
--fps 15) - Environment variables:
TOMVILLE_FPS,TOMVILLE_STYLE,TOMVILLE_THEME,TOMVILLE_CONFIG ~/.config/tomville/settings.toml- Built-in defaults
So --fps 30 tries 30 fps once, and tomville config set fps 30 keeps it.
| Code | Meaning |
|---|---|
0 |
OK |
1 |
Problem found (doctor, a broken pet file) or runtime error |
2 |
Wrong command or option |
Same convention as herdr, so scripts can check the result.
tomville # open the office
tmv --demo --theme game-boy # try it with fake agents
tmv --style blocks --pet codex1=fox --fps 15 # one-off look
tmv status # quick text summary
tmv status --watch 5 # live summary in a small pane
tmv status --json # for scripts and agents
tmv watch # wait for a GitHub issue/PR change, then exit
tmv watch --json # the same changes as a JSON array
tmv doctor
tmv pets new fox --from cat && tmv pets check fox
tmv config set theme nighttmv status output:
myproject · 3 agents · 3 tasks
🐱 claude lead working T2 Write tests for login
🐶 codex1 worker blocked T1 Add login form ← needs approval
🐑 codex2 worker idle
Todo 1 · Doing 1 · Review 1 · Done 0
Because status --json prints the team and board as JSON, Claude can read it with one command. An optional line for the duo skill:
- To see the whole team at once, run `tomville status --json`.GitHub watching is opt-in, with [board] watch = false by default. tmv new asks whether to enable it (default no); tmv config set board.watch true enables it later for the current ville and refreshes its generated rules. When enabled, the lead keeps tmv watch running in the background. Manual tmv watch works even when the setting is off. Telegram blocked/review/done pings are unchanged. It reads issues and PRs every [board] check_seconds (the same interval chosen by tmv new), exits with changes, and should be restarted after the lead handles them. The first successful read creates a silent baseline in the ignored .tomville/watch.json. Later runs compare with it, including new/edited/closed items, reviews and CI red/green. GitHub failures print to stderr and retry at the next interval; --json keeps stdout machine-readable.
The watcher makes only read-only gh issue list / gh pr list calls. It includes closed/merged records so a closure is explicit, never guessed from a missing row. If its default history limit of 1,000 items per kind is reached, it reports this instead of saving a partial baseline; restart with a larger --limit. Forks and authors other than the repository owner are marked outside/unverified because the list API does not expose collaborator membership. The lead reviews and reports those PRs to Tom, and never merges them without his approval. Team PRs still require conflict checks, tests and review before merging; new issues need labels/assignment or a question to Tom. tmv doctor shows the watcher interval.
Tomville doctor · v0.2.1
Environment
✔ Python 3.12.3
✔ Terminal 142 × 41 cells, 24-bit colour
✔ Emoji width calibrated (emoji style)
herdr
✔ herdr 0.9.5 at /root/.local/bin/herdr
✔ Running inside herdr (HERDR_ENV=1)
✔ Claude integration installed
✔ Codex integration installed
Agents
✔ claude (cat, #f5a97f)
✖ codex1 not running → herdr agent start codex1 --kind codex --pane <pane>
Board
✔ GitHub Issues: DuongAnh1201/tomville (3 open, labels ready)
Settings ~/.config/tomville/settings.toml
Sprites ~/.config/tomville/sprites/ (1 custom pet)
1 problem found.
Required: Python, herdr, and running inside herdr. Everything else is a warning; the office still starts and shows what it can.
Kept small so it fits in two days.
| Layer | Choice | Why |
|---|---|---|
| Language | Python 3.11+ | Already on Ubuntu; tomllib built in |
| Terminal UI | Textual + Rich | Layouts, touch, animation timers, 24-bit colour, cell_len for emoji width |
| CLI | argparse (standard library) with subcommands; two entry points, tomville and tmv |
Options and commands with built-in --help; no extra dependency |
| Settings | TOML: tomllib to read, tomli-w to write |
Human-editable, same format as Codex and herdr |
| Agent status | herdr agent list (JSON via subprocess) |
Uses the existing setup; no extra server |
| Board | GitHub Issues via gh api (with ETag) or TASKS.md by modified time |
Uses the existing gh login; no extra library |
| Packaging | pyproject.toml + hatchling; uv |
One command to install, update, remove |
| Tests | pytest for the world model, sprite loader and doctor | The game logic is tested with fake events, no agents needed |
Dependencies: textual and tomli-w only.
tomville/
pyproject.toml
README.md
install.sh # one-line installer (phase 2)
CHANGELOG.md
src/tomville/
cli.py # argparse: office options + status, doctor, setup, pets, theme, config
core/ # shared by CLI and web; no UI code here
events.py # herdr event stream + messages.jsonl + board -> events
board/ # board plug-ins: github.py (default), tasks_md.py
messages.py # tmv send, message log, hook entry point
world.py # pets, zones, targets, movement
sprites.py # load sprite files, palette, width check
settings.py # read/write settings.toml
doctor.py # checks shared by doctor and setup
tui/ # phase 1: terminal front end
app.py # Textual app, layout, keys
office.py # renderer: office map + sprites widget
status.py # status output, text and JSON
web/ # phase 3: browser front end
data/
sprites/ # built-in pets: cat, dog, sheep
themes/ # night, game-boy, green-screen
office.txt # the floor plan, drawn as text
tests/
User files live outside the package, so updates never overwrite them: ~/.config/tomville/settings.toml and ~/.config/tomville/sprites/.
The build is now tracked in two parts in milestones.md: Part 1 (fundamentals, lead + workers and pipeline) and Part 2 (other team shapes, custom pets, themes, publishing). The table below is the original plan; where they differ,
milestones.mdwins.
Day 1: a live, moving office
| Block | Build | Done when |
|---|---|---|
| Morning | Project skeleton, pyproject.toml, office map, Textual layout (office + Team + Board + Events) |
tomville opens the empty office |
| Midday | Event reader: herdr agent list + herdr event stream + GitHub Issues board into events |
Team list and board show real data |
| Afternoon | World model: zones, status-to-behaviour table, cell-by-cell movement, fps option (F); lead + workers shape |
Pets walk to their desk when you start Claude or Codex |
| Evening | Emoji and ASCII sprites, built-in cat, dog and sheep | Style switch (S) works; tests for the world model pass |
Day 2: your own pets, setup and install
| Block | Build | Done when |
|---|---|---|
| Morning | Pipeline shape: conveyor office map, [team] settings, --team; tmv send, message log, Claude Code hook; handoff detection |
tmv send moves an envelope and a card from planner to coder to tester |
| Midday | Block-pixel sprite format with palettes; custom pets folder; pets list, pets show, pets new, pets check |
Your own fox walks around in its colours |
| Afternoon | Themes (T), agent colours, emoji width check and calibration screen |
Map stays aligned on the 11-inch screen |
| Evening | CLI: office options, status (with --json, --watch), doctor, setup, config, theme list; tmv alias |
tmv status --json and tmv doctor work; --help lists every option |
| Late | Tap to select, Enter to open the pane, idle power saving; install test with uv tool install from GitHub |
Fresh install works end to end |
If day 1 runs late, drop block-pixel sprites from day 2 first: emoji and ASCII are enough to use the app.
Each day ends with a usable app.
Goal: anyone installs it with one line, updates with tmv update, the same experience as herdr.
| Step | What | Done when |
|---|---|---|
| 1. Name | tomville is free on PyPI and GitHub (checked 2026-10-16); pick a licence (MIT suggested) |
Name and LICENSE in the repo |
| 2. Package | Version 0.1.0 in pyproject.toml, CHANGELOG.md, project links |
uv build makes a wheel that installs cleanly |
| 3. Installer | install.sh: checks OS and arch, installs uv if missing, runs uv tool install tomville, prints the PATH line if needed |
Works on Ubuntu (x86_64 and aarch64), macOS, Termux PRoot |
| 4. Update and uninstall | tmv update and tmv uninstall call uv tool upgrade/uninstall |
Update from 0.1.0 to 0.1.1 works |
| 5. CI | GitHub Actions: lint and tests on every push | Green check on main |
| 6. Release | Push tag v0.1.0 → Actions builds, publishes to PyPI (trusted publishing, no token) and creates a GitHub Release with the changelog |
uv tool install tomville works on a clean machine |
| 7. Go public | Make the repo public; README starts with the one-line install | The install line works for anyone |
How it compares to herdr:
| herdr | Tomville | |
|---|---|---|
| One-line install | curl -fsSL https://herdr.dev/install.sh | sh |
curl -fsSL https://raw.githubusercontent.com/DuongAnh1201/tomville/main/install.sh | sh |
| What it installs | A Rust binary per platform | A Python app via uv, any platform with Python 3.11+ |
| Self-update | herdr update |
tmv update |
| Releases | GitHub Releases | GitHub Releases + PyPI |
A short install domain (like herdr.dev) and standalone binaries without Python are optional extras for later.
The same office in a browser, for a bigger screen or to watch from another device.
| Step | What |
|---|---|
| 1. Quick version | tmv web runs the existing Textual app in the browser with textual serve. Almost no new code |
| 2. Real web UI | A small server (FastAPI) streams world events from core/ over a WebSocket; the page draws pets on a pixel canvas, with real pixel art instead of characters |
| 3. Extras | Drag-and-drop board, pet editor in the browser, view from a phone on the same Wi-Fi |
Rules for phase 1 that keep phase 3 easy:
- All logic lives in
core/;tui/only draws and handles keys. - The world model emits plain events (dicts), so the web server can send the same events as JSON.
status --jsonuses the same event format, so it doubles as the web API's data shape.
- Pet editor: paint pets by tapping cells.
- Pixel-art pets from online sets:
tmv pets import cat.png --frames 4 --size 8x8 --name gingerturns a sprite sheet into a block-pixel pet (Pillow + half-block▄rendering, as in Rich Pixels); a "big mode" office for full-size sprites; a community pet gallery repo (tmv pets install fox). Bundle only CC0 art (e.g. OpenGameArt "Cat & Dog Free Sprites", itch.io CC0 cat and dog sheets); not vscode-pets art, whose licence is unclear. - Prompt box that sends tasks to the lead (
herdr agent prompt claude "..."). - GitHub Projects (kanban) view on top of the Issues board; needs the
projectscope ongh. - Full setup wizard with live previews; more themes.
- Shell tab-completion (
tomville completion bash|zsh|fish). - More team shapes: pair/reviewer (two facing desks), parallel fan-out with git worktrees, peer group, and agents on other machines (
herdr --machine).
- Name: Tomville. Command
tomville, short aliastmv; settings in~/.config/tomville/. - First run: the user creates a new ville (
tmv new), like starting a new game. One ville per project. - Lead + workers layout: boss at the top behind the boss desk, facing named worker desks; the board on the wall beside the boss.
- The task board is GitHub Issues by default, checked every
check_seconds(user's choice);TASKS.mdis the offline plug-in. - Messages are logged by Tomville (
tmv send+ Claude Code hook), because herdr does not record senders. - Status comes from herdr's event stream, with polling as a fallback.
- v1 team shapes: lead + workers, and pipeline. Other shapes come later.
- Tomville builds on top of herdr, not as a replacement. herdr still runs the panes, sessions and agent status; Tomville reads that status and draws it, running in its own herdr tab.
- v1 is watch-only; the prompt box comes later.
- Built-in pets: cat, dog, sheep. Built-in themes: night, game-boy, green-screen.
- Landscape first; portrait stacks the sidebar under the office.
- Which pet and colour for each agent?
- Licence: MIT, or another?
With the lead + workers team shape, Telegram enabled and paired, tmv setup installs Claude Code hooks for the lead’s permission requests, auto-mode safety blocks, structured questions and plain-text questions at the end of a turn. Workers and subagents are ignored. Enable telegram.approvals in the ville to answer permission and structured question dialogs; the bot must be running. Workers’ old screen-based approval buttons have been removed; Tomville never sends approval keys to a pane.
Permission requests show the exact tool input and cwd, with edits also shown as diff blocks. Choose Allow once or Deny; replying to that message denies with your reason. Structured questions have option buttons, multi-select, and Other via a typed reply to the particular question message. Tap Submit answers after answering every question. Unanswered permission requests are denied after ten minutes; unanswered questions are cancelled. There is no persistent allow option. Pausing the bot prevents delivery and answers; accepted hook requests still reach their ten-minute deny deadline. The installed hook timeout is 660 seconds so the 600-second decision can return first.
Safety blocks show the action and reason. Approve, retry sends Tom’s message to the lead; the safety check still decides on the retry. Leave it sends no prompt. Plain-text final messages with a question in any prose line or sentence are forwarded in full, with question lines bold and basic italic/code formatting preserved. Question marks in code, URLs and quoted text are ignored; replying reaches the lead normally.
The hooks exchange requests and answers through private, ignored .tomville/approvals/ files. Only the bot polls Telegram. Request IDs are random, answers are consumed once, late taps are ignored, and decisions/questions are logged in messages.jsonl as telegram-approval / telegram-question. Only the paired private chat can answer. Hooks look up their pane in the private, mode-600 ~/.config/tomville/leads.json registry, then verify that the ville’s own state names this pane as lead and Telegram is enabled. A stale or invalid registry entry gets no answer. If the pane is not registered, hooks search cwd and its parents for the first matching ville. Lead startup/adoption and Telegram pairing register the pane; confirmed tmv down --agents and tmv telegram off remove that ville’s entries. Registered leads therefore keep using their ville from any folder, including outside its directory tree. Workers and other panes remain excluded.
Other team shapes are not supported: setup stops before token entry; a running bot replies with the unsupported-shape message and sends no notifications or questions. Telegram can be set up again after switching back to lead + workers.
Coverage follows Claude Code’s hook events: PermissionDenied observes auto-mode denials, not manual denials, deny rules or another PreToolUse hook’s block. Sandboxed network permission prompts do not emit PermissionRequest and still require an in-pane answer. The implementation uses the documented hook contracts, checked against installed Claude Code 2.1.305; tests use synthetic contract fixtures, not live captures. Live hook/UI validation belongs to the lead after Tom’s approval.
Telegram polling and approval decisions run separately from slow herdr commands, notifications and request sends. Callback taps are acknowledged first; acknowledgement errors do not discard a still-pending answer. Request locks cover only local state changes, with a one-second lock deadline; a busy request is skipped by the scan and retried later. Slow lanes have bounded waits and one in-flight worker each, so an outstanding send cannot occupy the polling/decision workers. Bot API requests use 10-second I/O timeouts (30 for the 20-second long poll, 3 for callback acknowledgement), and Telegram-to-agent prompt subprocesses have a 10-second deadline. Request reads no longer rewrite unchanged queue files.
tmv telegram status shows the last successful poll in UTC and its age. tmv doctor warns when no successful poll has been recorded or it is over two minutes old, with a hint to inspect the bot log/restart it. An active loop iteration taking over 30 seconds logs a heartbeat warning. These diagnostics do not expose the token.
