Skip to main content

project-manager-tui

A terminal task board for people running coding agents in git worktrees.

Tasks live in plain Markdown that you and the agents both edit. pm-tui renders them as one list ordered by what needs you: agent requests waiting on an answer first, then your open tasks, with everything an agent is busy with hidden until you ask to see it. One keypress opens a task's worktree, starts its agent, or releases its branch — the TUI shells out to a script you supply for all of it, so it knows nothing about tmux, ssh or your remote host.

Install

pip install project-manager-tui
pm-tui

Requires Python 3.9+.

Configure

~/.project-manager-tui.toml:

# Required.
agent_worktree_script = "/path/to/agent-worktree.sh"
projects_dir          = "/efs/me/projects"

# Optional, with their defaults.
projects_filename = "Projects.md"
git_repo_dir      = "."          # the cwd `go` is run in
sync_interval     = 300          # seconds between `sync-master` runs
editor            = "$EDITOR"    # falls back to vim
approvals_dir     = ""           # default: {projects_dir}/../approvals
drive_idle_seconds = 30          # before a driven task's idleness is reported
drive_lease_hours  = 12          # how long a drive grant lives
drive_command      = ""          # default: pm-drive (see Driving a task)

# Optional. Which agent a NEW worktree starts on, cycled in the UI with `m`.
# Short name -> "<cli>-<model>", split at the FIRST dash: everything before it
# is the CLI to run, everything after is the model handed to that CLI. The
# first entry is the default, and the table's order is the cycle order.
#
# Keep the tables LAST in the file: a TOML table swallows every bare key that
# follows it, so a `models` section in the middle silently steals the settings
# under it.
[models]
fable = "claude-fable"          # claude --model fable
astra = "codex-gpt-6-astra"     # codex  -m gpt-6-astra
pro   = "gemini-gemini-3-pro"   # gemini -m gemini-3-pro

# Optional. The same for project agents (below), so the two levels can run
# different CLIs -- gemini on tasks and claude on projects, or the reverse.
# Without it project agents use [models].
[project_models]
fable = "claude-fable"

Without [models] the picker is empty, m does nothing, and every worktree starts the way it did before: claude, on whatever the container defaults to. m cycles the picker for what is selected: the project one in project view or on a project agent row, the task one otherwise.

agent_worktree_script is the only real dependency, and writing one is the bulk of the setup — see the contract below.

Layout

projects_dir/
  Projects.md              ## Active / ## Future / ## Archived, one slug per line
                           (optionally "slug /dir": see Agents on their own directory)
  my-project/
    Project.md             the task list
    tasks/3/Task.md        one task's brief, created on demand

A task is a numbered line in Project.md:

1. [ ] rewrite the fill parser
2. [ ] backfill september >1          blocked until task 1 closes
3. [ ] chase the vendor >2026-09-20   pinned to a date
4. [x] delete the old path

The state character drives everything:

state meaning
[ ] todo
[i] an agent is running /ready on it
[t] tested
[r] released — branch pushed to master
[d] delivered — deployed
[v] verified — you have seen it work
[x] complete
[s] [z] snoozed / snoozed forever

[x] and [v] are what unblock a dependent task. [i] [t] [r] [d] mean underway, so the task is not offered as new work.

Keys

Task view:

j/k  navigate                 z  snooze: 1H/2H/4H/8H/1D/2D…
t    vim Task.md              u  unset: clear state + time snooze
p    vim Project.md           c  complete
v    view worktree            r  release (mark [r])
g    go: start its agent      d  delete task
s    switch to worktree       V/Z/B/W  show verified/snoozed/blocked/working
D    drive (see below)        i  read what the driver has sent
m    cycle the model
P    project view             ?  help, q  quit
/    filter by project name, live (Enter keeps it, Esc clears)

The project view lists Active and Future projects; archived ones are hidden (e opens Projects.md to move one back).

Approval rows (!) — requests an agent has filed and cannot execute itself:

Enter  review + run it        i  inspect the request JSON
x      reject, with a reason  t/p  vim the filing task's Task.md/Project.md
v      view the agent         z  snooze,  u  clear a hold nothing is paying

Project agents

A project agent is a session on a whole project rather than on one task: ask it what the project is about, have it check a task's results, or have it write the next task. It does no coding -- that is what tasks are for -- and it has no branch or worktree; it runs in the same sandbox as the task agents, with the project directory as its working directory.

Project view:   v  open the project's agent (creates it the first time)
                c  close it
Task view:      an open agent is a row, `<slug>-pm  project agent`, that
                behaves like a worktree row for status: hidden while it works,
                listed (and counted by the light) while it waits for you.
                v/g open it, t/p vim Project.md, c close it.

Closing keeps the session: the next open resumes it. Which agent a NEW project agent starts on is [project_models] (or [models] without it), cycled with m in project view.

Agents on their own directory

A line in Projects.md can name a directory after the slug:

## Active
my-project
all /efs/me/projects
podcasts /efs/me/source/podcast_transcripts

<slug>-pm then runs in that directory instead of projects_dir/<slug>, and is otherwise an ordinary project agent: v in project view opens it, it is a row while open, c closes it. A slug with no Project.md has no tasks; p on it in project view opens its line in Projects.md, and n gives it a Project.md like any other.

Where the directory points decides what the agent is told:

  • projects_dir itself (all above) -- the all-projects agent, for the questions and the tidying that span projects: what is in flight everywhere, which project a new task belongs in, what Projects.md should say. It drives nothing: a drive belongs to the project agent of the task's own project.
  • anywhere else -- a directory agent, working in that tree as you ask. It is not given the project-agent role, so unlike a project agent it may write code there.

The directory is a single whitespace-free token: the script reads it with awk. Archiving moves the line with its directory.

Driving a task

D on a task row hands that task to the project agent: it is told what it now has, told again whenever the task goes idle, and its answers are typed into the task's session. The row is orange for as long as the grant holds, with ⇄10h·3 beside it -- ten hours of lease left, three messages sent -- so a drive is never something happening quietly. D again hands it back, i reads everything the driver has said, and D on the project agent's own row stops every drive at once.

Telling the project agent to drive something works as well as the key does: it writes the same grant, and the row turns orange within a poll either way. What it cannot do is undo your revoke -- that leaves a tombstone only D clears.

A grant lasts drive_lease_hours (12) and has no message cap; the count on the row is what makes a runaway obvious. Driving hides no task row: a driven task that is working is hidden like any other, and a driven task waiting on someone is on screen, in orange, whether or not its driver is awake. It does hide the driver: a project agent whose driven tasks are every one of them busy is waiting on machines rather than on you, so it goes behind W with them, and comes back the moment one of them stops. An agent driving nothing is unaffected.

The project agent's half is pm-drive, installed with this package -- so unlike agent-worktree.sh it is not something you write. Install it wherever your agents run and set drive_command if it is not simply on their PATH. The project directory has to be reachable from both sides, which is the one thing that fails quietly: the row goes orange and nothing is ever delivered. The paths need not match, only the directory.

The driver may talk, and may have a stopped task agent started to hear it. It may not set state letters, release or complete anything. It is also told not to answer a task sitting in a question widget -- it cannot know what a keystroke would select there -- and send refuses to.

A driven task should not reach a widget at all. pm-drive route-question is the task agent's half: a pre-tool-use hook that, when the agent reaches for its question tool and a live grant exists, writes the question into the task's Task.md with its options and denies the tool, telling the agent to stop. The driver then finds it under ASKING in peek, and send answers it and ticks it off. With no grant it does nothing and the widget opens as it always did -- with nobody driving, you are the right answerer -- which is also what keeps it out of every other session on the machine, so register it per launch rather than in a shared settings file. Have your launcher register it for whichever CLI owns the worktree:

claude   PreToolUse / AskUserQuestion      {"type": "command", "command": "pm-drive route-question", "timeout": 10}
codex    PreToolUse / request_user_input|request_user_input_async
                                           the same entry, in $CODEX_HOME/hooks.json
gemini   BeforeTool / ask_user             the same entry, with "timeout": 10000

Two traps. Gemini's timeout is in milliseconds where the other two read seconds, and a hook that times out there fails open into the widget. And do not also deny the question tool by policy for gemini: a policy deny removes the tool from the model's declared toolset, so the hook never fires and nothing is routed. Set PM_DRIVE_PROJECTS_DIR where the agents run, since a worktree knows only its own name and cannot work out whose projects to look under. Every failure inside the hook is silent and exits 0, so a broken one costs one question rather than a session.

That silence is also why you should check the registration once rather than trust it. Two checks, cheapest first. Dry, with no agent involved -- feed the hook a payload by hand from a task's worktree, with a grant on that task (D in the TUI, or pm-drive grant <slug>-<N> from the project directory):

cd /path/to/worktrees/<slug>-<N>
echo '{"hook_event_name":"PreToolUse","tool_name":"AskUserQuestion",
       "cwd":"'"$PWD"'","tool_input":{"questions":[{"question":"3 or 5?",
       "header":"Retries","options":[{"label":"3"},{"label":"5"}]}]}}' \
  | PM_DRIVE_PROJECTS_DIR=/path/to/projects pm-drive route-question

A deny on stdout and a new - [ ] line under ## Questions in the task's Task.md means the hook can find the project and the grant from where the agents run. Nothing on stdout means it cannot: check PM_DRIVE_PROJECTS_DIR, the worktree name, and that the grant is live (pm-drive status). For gemini send "hook_event_name":"BeforeTool","tool_name":"ask_user" and expect a top-level {"decision": "deny", ...} instead.

Live, once per CLI, because the dry run proves the hook and not the registration: start a task agent on a granted task and tell it, in its own window, to ask you something using its question tool. Within a turn its Task.md should carry the question with its options, the agent should say it is waiting and end its turn with no widget on screen, pm-drive peek from the project directory should list it under ASKING, and a send should tick it. A widget on screen means the hook did not fire -- the wrong event name or tool matcher, a gemini timeout in seconds, or a policy deny on the tool. Do it again after any CLI upgrade; all three contracts have moved before.

The agent's standing instructions live in {projects_dir}/CLAUDE.md, an ancestor of every project directory, so one file serves all of them; the package ships a starter (project-agent-role.md, printed the same way as the contract below) that awt-remote.sh also hands to Codex and Gemini, which read no CLAUDE.md. Copy it there once; without it a project agent starts with only the per-project prompt the launcher builds.

Status light

pm-tui --web            # TUI plus the status page
pm-tui --web-only       # just the page, no TUI

Serves a full-window light on port 8899: green when the default view is empty, amber with a count when something wants you, grey when the count is too old to trust. --web-port, --web-bind and --web-interval move it; /status.json is the same thing as JSON. Park it on a second screen and stop checking.

The agent-worktree.sh contract

The TUI never touches git, tmux or ssh itself. It invokes your script with list, add, rm, switch, view, go, tell, close, release, approve and sync-master, and the shipped contract document specifies each one — the exact list output format, which status tokens the working filter and the status light read, what may prompt and what may not, and the approval queue's on-disk format, statuses and leases.

It ships inside the package:

python -c "import importlib.resources as r, sys; \
  sys.stdout.write((r.files('project_manager_tui') / 'agent-worktree-contract.md').read_text())"

Read it before writing a script. Several of the TUI's behaviours — hiding busy worktrees, the green light, a go that does not steal your terminal — are conventions your script has to hold up its end of, and a script that ignores them still runs, just quieter than it should.

Release files for project-manager-tui 0.1.28

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for project-manager-tui 0.1.28
File Size Uploaded
project_manager_tui-0.1.28.tar.gz 100.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for project-manager-tui 0.1.28
File Interpreter ABI Platform
project_manager_tui-0.1.28-py3-none-any.whl Python 3 none any Details

Total release size: 202.1 kB

Release files / project_manager_tui-0.1.28.tar.gz

Download URL project_manager_tui-0.1.28.tar.gz
Size 100.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d463a549d4a415cf863ccb0339310d875e63d4875785449b7f7615ce9ac54e21
BLAKE2b-256 checksum
How to use checksums
8406fc41c912933d80ff9d24b868bdf1b5ed0a60ff5a5f2b6c1df40fa76fb613
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.8

Release files / project_manager_tui-0.1.28-py3-none-any.whl

Download URL project_manager_tui-0.1.28-py3-none-any.whl
Size 101.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0a70d40327c79a76c924d3b7b7a156db90dfeb77cb33edb94c67410fbd2d56c0
BLAKE2b-256 checksum
How to use checksums
b66e6a19bc7afba78d22bfa67b562f7ef5af381dd522a441793adff424782a3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.8

Release history Release notifications | RSS feed

This release

0.1.28 This release

2 release files

0.1.26

2 release files

0.1.25

2 release files

0.1.24

2 release files

0.1.23

2 release files

0.1.22

2 release files

0.1.21

2 release files

0.1.19

2 release files

0.1.18

2 release files

0.1.17

2 release files

0.1.16

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page