Skip to content

Repository files navigation

agendo

agendo — a little console for launching and wrangling your Claude, Copilot and Codex agent sessions. (agenda + do; also the Latin root of agent.)

agendo

agendo manages your Claude, Copilot and Codex agent sessions as tabs in one tmux session, organized around your Azure DevOps work items or GitHub issues. It automatically finds every session you've ever started and matches each to the PRs and issues it belongs to — by branch name, PR/issue number, and the like — so you see what's assigned or open, with each item's PR and CI status, then jump into (or attach to) the agent already working on it, or spin up a fresh one in its own worktree, all from one keyboard-driven list. Wrangling those tmux sessions is the whole point, so it runs as a single tmux session by default.

Run

Requires:

  • bun — agendo runs on bun.
  • tmux — agendo manages your agents as tabs in one tmux session.
  • a backend CLI, auto-detected on your PATH:
    • az (with az login) for Azure DevOps, and/or
    • gh (with gh auth login) for GitHub.
bunx agendo # or: bun x agendo (npx agendo works too, if bun is installed)
bunx agendo --no-tmux # run the menu inline, without a tmux session

For Azure DevOps, set your org / project / team / tenant in ~/.agendo/config.json first (see Config). GitHub needs no config.

Running a pull request

To review or test a PR, run it straight from its branch — no clone, no checkout:

bunx github:MiniGod/agendo#pull/8/head # run agendo from PR #8

Handy for trying a change against your own real sessions before merging it. Swap 8 for the PR number; #HEAD gets you the tip of the default branch.

Every session agendo starts is told how to re-invoke agendo (that's the command in its --llm guide and system prompt), and it now inherits the invocation you typed rather than a reconstructed one — bunx exposes the original spec, so a PR build hands bunx github:MiniGod/agendo#pull/8/head down to the sessions it spawns, and they pass it on to theirs. So the whole chain stays on the PR build and you are testing the branch's agent-facing surface (launch/list/send/ wait/close, the guide text, the on-disk state formats), not just its TUI.

Caveat for npx: npm does not expose the spec it was given, so agendo falls back to the copy in npm's own _npx cache — the build actually running, and correct for the life of the session, but invalidated by an npm cache clean. Prefer bunx for running a PR.

Features

Azure DevOps & GitHub backends

Both are auto-detected from the CLIs on your PATH; switch between them — and see each one's live auth status — from the settings page (,). Azure DevOps lists the work items in your team's current sprint with their linked PRs; GitHub lists issues scoped to the repos discovered across your local sessions.

One tmux session, one tab per agent

agendo lives in a single canonical agendo tmux session: the menu is tab 1, and every agent you open or resume becomes another tab in the same session. Re-running agendo attaches to it rather than spawning a second, so there's only ever one. (--no-tmux runs it outside tmux, where each agent is a detached session you attach to.)

Browser-style session restore

It remembers the agent tabs you had open and lazily restores them next launch — each reappears in the tab strip but stays unloaded until you switch to it and press a key, so startup never spawns a fleet of agents.

Orchestrator agents that spin up their own worktrees

Every Claude agendo starts is given a small system prompt pointing at agendo launch/list/status/send/open/wait/close. So an agent can spin off new sessions — each in its own fresh worktree — for separate pieces of work that deserve their own PR, then monitor, steer and finally close them through the same commands. One orchestrator session can fan a large task out across many worktrees and coordinate them, instead of hand-rolling tmux and git worktree. The sessions it starts inherit the same ability.

To follow them, an orchestrator should be told, not poll. agendo wait blocks until a watched session settles — a non-busy state, or its window closing — so it can be run in the background with its exit as the notification. A session parked at its usage cap is the one thing that stops without being done: the wait wakes on it promptly, but exits non-zero with woke: "blocked" and the session's limitResetAt, so a capped session is never mistaken for finished work (an explicit --state/--not is never pre-empted that way, so you can still wait through a cap):

agendo wait --repo myapp --any --json --timeout 30m

--any returns on the first of several sessions to settle, so one long-running session can't hide the others; --json says why it woke and gives each session's from → state, so the wake needs no follow-up list. --state <s> waits for one exact state — e.g. --state limited to hear the moment a session hits its usage cap. The alternative — re-running status on a guessed cadence — either fires too often or finds out too late.

--state dialog means a question awaiting your decision; the Claude CLI's own resume dialog isn't one (see resumeDialogChoice — it reads ready), so it won't wake that wait. When a wake does find a session parked there, --json says so with resumeDialog: true: nothing has run yet, so the activity is the previous run's.

When a session is finished with, agendo close <id> ends its window and only that — the worktree, branch and commits stay on disk, and agendo resume <id> brings it back — so no one has to reach for a raw tmux kill-window. A wait on a session closed underneath it doesn't hang: the window vanishing settles that session as exited.

Messages that queue instead of waiting for an idle pane

agendo send delivers to a running Claude session over the messaging socket that session advertises, rather than typing into its tmux pane. The difference is that the receiver queues it: you can message a session mid-turn and it picks the prompt up when it next reads input, instead of send refusing because the pane isn't idle. It is addressed by session id, so a recycled pid can't misdeliver into someone else's session — and a session running outside agendo entirely (a plain terminal) is reachable too, with no tmux window involved.

This is an internal, undocumented channel, so agendo treats it as an optimization rather than a dependency: a session that doesn't advertise it (Copilot, older Claude builds) or whose socket refuses gets the prompt typed into the pane exactly as before. A session at its usage limit is refused either way — nothing would read the queued message until the cap resets.

Delivering a message and answering a dialog stay separate jobs. A frame arrives as a peer message, which the receiver won't accept as the answer to a pending prompt — so when a session is parked on claude's own resume dialog, send still answers that with keystrokes first and only then delivers, by whichever route. The socket is an alternative for the delivery, never for the dialog.

What send promises over the socket is handover, not reading: the frame is queued for a session that is still running. So agendo close on that session discards anything it hadn't read yet. Once a session is closed it stops being a peer at all — its process is gone, so send refuses outright rather than queueing into a socket nobody is left to read.

Because the two routes mean different things, send always says which one it took — ▸ queued via socket to … versus ▸ pasted into pane …, and route: "socket" | "pane" (plus queued) on --json. Queued means the message may sit unread for a while in a session that is mid-turn; pasted means it is on screen now, and the pane had to be idle to accept it. Nothing about the session afterwards distinguishes the two, and the socket isn't guaranteed to exist, so the route is reported rather than inferred.

Turning the socket off

The socket speaks an internal, undocumented claude protocol. agendo gates on the version claude advertises and falls back to the pane when the socket refuses — but neither catches the failure that would actually matter: a build that still advertises the same version and still accepts the frame, having changed what it does with it. From this side that write simply succeeded. So there is a switch:

// ~/.agendo/config.json — the durable preference
{ "peerSocket": false }
AGENDO_PEER_SOCKET=0 agendo send <id>""# one-off override

The variable wins over the config file in both directions, so AGENDO_PEER_SOCKET=1 re-enables the socket for a single command against a "peerSocket": false config. Either one set to off forces the tmux keystroke path outright — no registry discovery, no socket write — which is exactly how send behaved before this path existed: a non-idle pane is refused again, and a session with no tmux window is unreachable. (Unset or empty means "not set"; any other value the variable is given counts as off, since it is a switch you reach for when something has gone wrong.)

Telling a finished session from a stalled one

A session that fell over mid-task 22 hours ago and one that answered cleanly 20 seconds ago both sit at a ready prompt. So agendo list/status also report how long since a session last did anything, and mark a live, non-busy one that has been silent past a threshold (4h by default — stalledAfterMinutes in ~/.agendo/config.json, or --stalled-after <dur>) with ⚠stalled. That flag only ever means "nothing has happened for that long"; agendo cannot know whether the work finished. Alongside it, --json carries idleSeconds and whether the checkout holds commits the remote doesn't — read straight from its .git refs, never by shelling out to git — which is usually enough for an orchestrator to spot a parked session without reading its transcript. It is the same "has it stopped working?" test wait uses, so the two agree by construction: wait tells you a session settled, and the stall marker tells you one settled a long time ago and nobody came back. A session parked on the resume dialog is the one exception: it reads ready and its recorded activity is hours old, but it hasn't run yet, so it is never marked stalled — --json carries resumeDialog: true to say why. A session parked at its usage cap is excluded for the same reason: limited means waiting on a quota reset (the row shows when it lifts), not hung, so it is never marked stalled either.

What that does and doesn't catch, from real sessions:

Session stateReported asCaught?
Finished its turn, sitting at an empty input boxready + idle age, ⚠stalled past the thresholdYes, and it is the case the feature exists for — but only by duration. Under the threshold, done-20-minutes-ago and wedged-20-minutes-ago are still the same row.
Parked at a usage caplimited + limitResetAtYes — and deliberately never ⚠stalled: it resumes on its own.
Rewriting its own contextcompacting + compactionPercentYes — blocked but progressing, and the percentage off the pane's own bar says whether to wait.
Parked on Claude's resume dialogready + resumeDialog: trueYes — never ⚠stalled; nothing has run yet, so the idle age is the previous run's.
Feedback survey on screen (numbered options above a live input box)readyYes — a menu above a live input box is not a dialog; pinned as a negative test.
Busy-waiting: until [ -f /sentinel ]; do sleep 30; done, for an hourbusyNo — known gap. The pane is genuinely active, so neither readiness nor idle age moves. A session can spin forever and look like one that is working. Detecting it needs a signal this PR doesn't have (no assistant turn despite an active pane), and is the obvious next step.

The honest summary: ⚠stalled answers "has anything happened lately", not "is this finished" and not "is this making progress". It catches the session that stopped; it does not catch the session that is busy doing nothing.

Orchestrator mode, one keypress away

Press O in the Sessions view — or run agendo launch --orchestrator "<goal>" — to start a session that is only a coordinator. It gets the orchestrator instructions injected into its system prompt: write no project code, split the goal into units, launch one background session per unit (each running an implement → sub-agent review → fix loop until a review pass comes back clean), keep a live task list, parallelize independent units, monitor via list/status and steer via send, then squash-merge each finished branch into the main branch — no PRs. The framing is re-injected on resume, so a restored orchestrator doesn't quietly turn back into an implementer.

Unlike every other launch, an orchestrator runs in the repo's main checkout rather than a worktree: git allows the main branch in only one working tree, and that's where its merges have to land. It writes no project code, so it needs no isolation of its own — pass --worktree (or pick "New git worktree") if you want it anyway.

Because it acts on your main checkout and spawns further sessions, an orchestrator also keeps its approval prompts — it's the one background launch that isn't auto-approved. Add --unattended to waive them once you're happy to let it run on its own. For the same reason --orchestrator is documented here and in --help, but deliberately left out of agendo --llm: that guide is injected into every launched session, and a worktree-sandboxed agent shouldn't be able to read its way into starting an orchestrator in your main checkout.

Fresh sessions in isolated worktrees

Pick "start a fresh session", choose the agent and repo, and agendo creates a git worktree off the repo's default branch and launches the agent there — so new work never disturbs your current checkout.

Three agents, one list

Claude Code, Copilot CLI and Codex CLI sessions are all discovered from disk and resumed natively (claude --resume, copilot --resume=<id>, codex resume <id>); the agent picker offers all three for a fresh session. Codex assigns its own session id rather than accepting one, so a codex session appears in the list once it has started, and agendo launch --codex prints no id up front. Autonomous codex sessions run under --approve-for-me — the analogue of Claude's auto mode, where each approval is decided by codex's own classifier instead of being asked, still inside the workspace-write sandbox.

Readiness (what agendo status reports and what send/wait gate on) is read from each pane's own TUI, and codex's looks nothing like Claude's, so it gets its own classifier. One thing to know: codex's footer is configurable via /statusline, and the run-state field (Ready / Working / Thinking) is the only positive evidence a codex session is idle. Leave it enabled. With it switched off a running turn is still detected — the • … (25s • esc to interrupt) line above the box gives it away — but an idle pane reads unknown rather than ready, and send refuses instead of guessing. That's deliberate: codex accepts typing mid-turn (it queues it) and parks a dim example prompt in the box, so an empty-looking box is never on its own permission to send.

Cross-agent continue (Claude ↔ Copilot)

Hover a session and press c to continue it in the other agent: agendo converts the transcript to that agent's on-disk format and resumes it, so a conversation can move between Claude and Copilot without losing context. (Claude and Copilot only — the converter has no Codex format.)

Move a session between Claude profiles

If you run more than one Claude login — ~/.claude, ~/.claude-work, anything matching ~/.claude* with a projects/ folder — a session sometimes lands in the wrong one. Hover it and press m to pick another profile; agendo relocates the transcript, its sidecar dir (tool results, sub-agents, workflow runs) and the session's session-env/ + tasks/ state, so --resume finds it under the right subscription. It refuses rather than clobber anything already at the destination, falls back to copy-then-delete across filesystems, and won't touch a session that is currently running — exit it first.

Config

Azure DevOps connection details live in ~/.agendo/config.jsonorg, project, team, tenant. There are no baked-in defaults and nothing is auto-discovered, so set them for your own setup (see src/config.ts for the shape); the token is fetched via az, no PAT needed. GitHub needs no config — it scopes to the github.com repos found across your local sessions. The stall threshold (stalledAfterMinutes, default 240) lives in the same file, as does peerSocket (see Turning the socket off). Your selected backend is remembered in ~/.agendo/state.json.

Opening a PR or work item in a browser (the o key, or agendo open <id>) uses your platform's default opener — xdg-open, open, or start. Set AGENDO_BROWSER to the executable to use instead, for hosts where that default isn't right (containers, WSL). Where nothing can be launched at all, agendo open still prints the full URL.

resumeDialogChoice

Resuming a large session, the Claude CLI first asks how to reload it ("Resume from summary (recommended)" / "Resume full session as-is"). agendo reports a session parked there as ready, not blocked, and answers the dialog itself the next time you send to it — then waits for the input box to actually come back before delivering your message.

// ~/.agendo/config.json
{ "resumeDialogChoice": "summary" } // default: whatever Claude marks (recommended)
{ "resumeDialogChoice": "as-is" } // resume the full session, at full token cost

The dialog's third option, "Don't ask me again", is deliberately not offered: it changes your global Claude CLI behaviour permanently, which is your call to make.

Testing

Browser-rendered integration tests live in e2e/: they spawn the TUI in a PTY, render it in a real headless browser via wterm, and drive it with Playwright against a fully mocked environment — Azure DevOps, on-disk sessions, tmux, and git are all faked, so nothing real is touched.

bun run test:e2e:setup # one-time: download Chromium
bun run test:e2e

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages