Add multi-repo task support to agman. Currently, tasks have a strict 1:1:1 relationship between a git branch, a git worktree, and a tmux session. We want to generalize this so a single task can span multiple git repos, while keeping the same simple flow model (one agent at a time, sequential execution).
When creating a new task in the TUI wizard, the user can select a non-git directory (e.g. the ~/repos/ dir itself) instead of a specific repo. When this happens:
- The user enters a branch name and description as usual
- The flow starts with a repo-inspector agent — a dedicated first-step agent whose only job is to inspect the git repos under the chosen directory and determine which repos are involved in the task. It writes its findings back to TASK.md (e.g. a
# Repossection listing the selected repos with brief rationale). - After the repo-inspector finishes, agman reads the repo list from TASK.md, creates one worktree per repo (all using the same branch name), and creates one tmux session per repo.
- The flow then continues with the normal agents (prompt-builder → planner → coder↔checker loop), but these agents are aware they're working across multiple repos.
For single-repo tasks, the behavior is essentially identical to today — there's just one entry in the repos list.
Do not build this as two separate solutions. The data model and code should handle both single-repo and multi-repo tasks with one unified design. No backwards compatibility concerns — we can freely break the existing data model. Do not add any migration utilities; existing tasks will simply break and users will recreate them.
Decision: Move TASK.md from the worktree to ~/.agman/tasks/<task_id>/TASK.md for ALL tasks.
Currently TASK.md lives at worktree_path/TASK.md. For multi-repo tasks there's no single worktree. Moving TASK.md to the task dir solves this cleanly and is actually simpler — no need for git excludes, no risk of accidentally committing it. This applies to single-repo tasks too (unified model).
Implications for the codebase:
Task::write_task()(currently attask.rs:288-292) changes fromself.meta.worktree_path.join("TASK.md")toself.dir.join("TASK.md")Task::read_task()(currently attask.rs:377-380) changes the same wayTask::ensure_git_excludes_task()(attask.rs:298-358) can be simplified — only REVIEW.md needs excluding now, not TASK.mdAgent::build_prompt()(atagent.rs:27-87) reads TASK.md viatask.read_task()which will transparently use the new location- The coder agent currently reads/writes TASK.md in the worktree — the task dir path must be communicated to the agent via the prompt so it can find TASK.md
- In
use_cases::delete_task()(atuse_cases.rs:153-176), theDeleteMode::TaskOnlybranch currently removesworktree_path.join("TASK.md")— this should instead just delete the task dir (TASK.md is already there)
Decision: Replace singular repo_name/worktree_path/tmux_session with a Vec<RepoEntry> in TaskMeta.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoEntry {
pub repo_name: String,
pub worktree_path: PathBuf,
pub tmux_session: String,
}
pub struct TaskMeta {
pub name: String, // repo name for single-repo, parent dir name for multi-repo
pub repos: Vec<RepoEntry>, // replaces repo_name, worktree_path, tmux_session
pub branch_name: String, // shared across all repos
pub status: TaskStatus,
pub flow_name: String,
pub current_agent: Option<String>,
pub flow_step: usize,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub review_after: bool,
pub linked_pr: Option<LinkedPr>,
pub last_review_count: Option<u64>,
pub review_addressed: bool,
}For single-repo tasks, repos has exactly one entry. The name field is used for task ID generation (name--branch). For single-repo tasks, name equals the repo name. For multi-repo tasks, name is the parent directory name (e.g. repos if the user selected ~/repos/).
Important: The current code accesses task.meta.repo_name, task.meta.worktree_path, and task.meta.tmux_session in many places. All of these must be updated. Add convenience methods like:
task.meta.primary_repo()→&RepoEntry(first entry)task.meta.is_multi_repo()→bool(repos.len() > 1)task.meta.task_id()→ usesself.nameinstead ofself.repo_name
The existing Config::task_id(repo_name, branch_name) static method stays the same — callers just pass name (the parent dir name or repo name) instead of always repo_name.
Decision: Task ID uses parent directory name. Format remains <name>--<branch>. For single-repo tasks, <name> is the repo name (no change). For multi-repo tasks, <name> is the parent directory name (e.g. if user selected ~/repos/, the name is repos). Keep it simple — no special naming in the wizard.
Decision: One agent instance works in the parent directory. For multi-repo tasks, set the agent's working directory (current_dir in Agent::run_direct() at agent.rs:129) to the parent directory (the non-git dir the user selected). The agent navigates into individual repo worktrees as needed. The prompt lists all repo worktree paths explicitly.
For single-repo tasks, the working directory remains the worktree of the single repo entry (no change from today's behavior).
This means Agent::run_direct() needs to determine the working directory based on the task:
- Single-repo:
task.meta.repos[0].worktree_path - Multi-repo: the parent directory (needs to be stored somewhere — see "Multi-repo parent path" below)
The task needs to know the parent directory path for multi-repo tasks (to set as agent working directory). Store this as an Option<PathBuf> field on TaskMeta:
pub struct TaskMeta {
// ... existing fields ...
/// For multi-repo tasks: the parent directory containing all repos.
/// None for single-repo tasks.
pub parent_dir: Option<PathBuf>,
}When parent_dir is Some, the agent's working directory is that path. When None, the agent works in repos[0].worktree_path.
Agent::build_prompt() (at agent.rs:27-87) will include information about all repos in the task. For multi-repo tasks, the prompt will include:
- A
# Repossection listing all repos with their worktree paths - The task dir path (so agents can read/write TASK.md at
~/.agman/tasks/<task_id>/TASK.md)
For single-repo tasks, the prompt will include the task dir path (since TASK.md has moved there) but the repos section is optional/minimal.
For git context (get_git_diff, get_git_log_summary), these currently run in self.meta.worktree_path. For multi-repo tasks, they should run in each repo's worktree and concatenate the results (with repo name headers).
Decision: Add a post-step hook mechanism to the flow runner. After the repo-inspector agent finishes (outputs AGENT_DONE), agman needs to:
- Parse the
# Repossection from TASK.md - Create a worktree for each listed repo (using
Git::create_worktree_quiet) - Create a tmux session for each repo (using
Tmux::create_session_with_windows) - Populate
task.meta.reposwith the new entries and save
Implementation approach: Add an optional post_hook field to AgentStep in flow.rs:
- agent: repo-inspector
until: AGENT_DONE
post_hook: setup_repos # new fieldpub struct AgentStep {
pub agent: String,
pub until: StopCondition,
pub on_blocked: Option<BlockedAction>,
pub on_fail: Option<FailAction>,
pub post_hook: Option<String>, // new field
}In the flow runner (run_flow_with at agent.rs:260), after an agent completes and before advancing the flow step, check if post_hook is set. If it's "setup_repos", run the repo-setup logic. This keeps the hook system generic (just a string identifier) but only implement the one hook we need right now.
The setup_repos hook logic should be a function in use_cases.rs that:
- Reads TASK.md from the task dir
- Parses the
# Repossection (expects a list of repo names — the repo-inspector agent writes these) - For each repo: creates a worktree via
Git::create_worktree_quiet, creates a tmux session viaTmux::create_session_with_windows - Populates
task.meta.reposwithRepoEntryfor each repo - Saves the updated meta
A new agent prompt at ~/.agman/prompts/repo-inspector.md. This agent:
- Receives the task description and the parent directory path
- Inspects git repos under that directory (can look at READMEs, code structure, etc.)
- Writes a
# Repossection into TASK.md listing which repos are involved and why - Outputs
AGENT_DONE
The prompt should be added as a const in config.rs (like existing DEFAULT_FLOW, etc.) and written by init_default_files().
The # Repos section format in TASK.md should be machine-parseable. Suggested format:
# Repos
- repo-name-1: Brief rationale for including this repo
- repo-name-2: Brief rationale for including this repoThe parser in the setup_repos hook extracts repo names from lines matching - <name>: under the # Repos heading.
Add a NEW_MULTI_FLOW constant in config.rs and write it in init_default_files():
name: new-multi
steps:
- agent: repo-inspector
until: AGENT_DONE
post_hook: setup_repos
- agent: prompt-builder
until: AGENT_DONE
- agent: planner
until: AGENT_DONE
- loop:
- agent: coder
until: AGENT_DONE
on_blocked: pause
- agent: checker
until: AGENT_DONE
until: TASK_COMPLETEEach repo in a multi-repo task gets its own tmux session, using the same naming convention: (<repo>)__<branch>. The TUI's "attach" action (Enter key in preview) for a multi-repo task should present a selection list letting the user choose which repo's session to attach to. For single-repo tasks, it attaches directly (no change).
The TUI wizard's scan_repos() (in app.rs) currently only returns git repos. Changes needed:
scan_repos()should also return non-git directories that contain git repos (i.e., parent directories like~/repos/). These should be visually distinguished in the list (e.g., prefixed with[multi]or similar).- When a non-git dir is selected:
- Skip the branch-source step entirely (always
NewBranch, since there's no single repo to pick existing branches from) - Use
flow_name = "new-multi"instead of"new" - The
namefor task ID is the selected directory's name
- Skip the branch-source step entirely (always
- The wizard state machine (
NewTaskWizard) needs to handle this new path through the steps.
When the wizard creates a multi-repo task:
use_cases::create_task()is called withname = parent_dir_name,parent_dir = Some(path),repos = vec,flow_name = "new-multi"- TASK.md is written to
~/.agman/tasks/<name>--<branch>/TASK.mdwith the goal description - No worktree or tmux sessions are created yet (repos haven't been determined)
- A single temporary tmux session is created for the repo-inspector agent to run in (working dir = parent directory)
agman flow-run <task_id>is sent to the tmux session- The repo-inspector agent runs, writes
# Reposto TASK.md, outputs AGENT_DONE - The
setup_repospost-hook fires: creates worktrees + tmux sessions for each repo, populatestask.meta.repos - The flow continues with prompt-builder → planner → coder↔checker
For step 4, the initial tmux session for the repo-inspector can use a session name based on the task name (e.g., (repos)__<branch>). After repos are set up, the individual repo sessions are created. The initial session can remain (it becomes the "parent" session) or be killed.
When deleting a multi-repo task with DeleteMode::Everything, iterate over all repos entries and remove each worktree/branch/tmux session. The existing delete_task in use_cases.rs (line 153) needs to loop over task.meta.repos instead of using the single repo_name/worktree_path.
Task::get_git_diff() and Task::get_git_log_summary() (at task.rs:696-720) currently run in self.meta.worktree_path. For multi-repo tasks, they should iterate over all repos in self.meta.repos, run git commands in each worktree, and concatenate results with repo name headers. For single-repo tasks, behavior is unchanged.
In AgentRunner::run_agent() (at agent.rs:216-229), the .pr-link file is checked in task.meta.worktree_path. For multi-repo tasks, check in each repo's worktree path, or in the parent directory. The linked_pr field on TaskMeta should probably become repo-specific eventually, but for now keeping it task-level is fine — just check all worktree paths.
src/task.rs—TaskMetastruct (addRepoEntry,repos,name,parent_dir; removerepo_name,worktree_path,tmux_session),Task::read_task()/write_task()(useself.dir),get_git_diff()/get_git_log_summary()(multi-repo support),ensure_git_excludes_task()(simplify to REVIEW.md only),delete()methodsrc/config.rs— AddNEW_MULTI_FLOWandREPO_INSPECTOR_PROMPTconstants, updateinit_default_files(), no changes totask_id()/parse_task_id()(they stay generic)src/flow.rs— Addpost_hook: Option<String>toAgentStepsrc/agent.rs—Agent::build_prompt()(add repos section, task dir path, multi-repo git context),Agent::run_direct()(determine working dir from task),AgentRunner::run_flow_with()(check and execute post-hooks after agent steps)src/use_cases.rs— Generalizecreate_task()for multi-repo (acceptname,parent_dir, initialrepos), generalizedelete_task()to loop over repos, addsetup_repos_from_task_md()function for the post-hooksrc/tui/app.rs— Wizard changes (scan_reposto include non-git parent dirs, skip branch step for multi-repo, usenew-multiflow), attach logic (session selection for multi-repo), create_task_from_wizard changessrc/tui/ui.rs— Display multi-repo tasks differently (show repo count or[multi]indicator)src/tmux.rs— May need a helper to create sessions for multiple repos in a tasksrc/main.rs—cmd_flow_runmay need minor adjustments for multi-repo working directorytests/use_cases_test.rs— Add happy-path tests for multi-repo task creation, deletion, and the setup_repos hook
- TUI-only: No new user-facing CLI subcommands. The
flow-runhidden command is fine. - Testing: Every new use-case function needs a happy-path test in
tests/use_cases_test.rs. No mocking — useTempDir+ real filesystem. - Logging: All state changes need structured logging with
task_id. Log where errors are handled, not where they propagate. - Avoid over-engineering: Keep the implementation minimal — don't add features beyond what's needed for multi-repo support.
- No migration: Existing tasks will break. That's acceptable.
- Data model:
RepoEntry,TaskMetawithname/repos/parent_dir, convenience methods (primary_repo(),is_multi_repo(),has_repos()) - TASK.md moved to task dir (
self.dir.join("TASK.md")) for all tasks -
ensure_git_excludes_task()simplified to REVIEW.md only, iterates all repos -
get_git_diff()/get_git_log_summary()multi-repo support with repo name headers -
post_hook: Option<String>field onAgentStepin flow.rs -
.pr-linksidecar checks all repo worktree paths + parent_dir -
delete_task()iterates all repos for worktree/branch/tmux cleanup -
new-multiflow,repo-inspectorprompt in config.rs,init_default_files()updated -
setup_repos_from_task_md()use case — parser + idempotent setup with incremental saves - Post-hook execution in
run_flow_with()viaexecute_post_hook() -
build_prompt()adds# Task Directoryand# Repo Worktreessections - Multi-repo task creation:
TaskMeta::new_multi(),Task::create_multi(),create_multi_repo_task()use case - TUI wizard:
scan_repos_with_parents(),is_multi_repotracking,create_task_from_wizard()multi-repo path - All
primary_repo()calls in app.rs guarded (30+ sites: delete, stop, resume, restart, attach, PR poll, branch picker, set_linked_pr) -
[M]visual indicator for multi-repo tasks in TUI - Test helpers and existing tests updated for new data model
- 7 new multi-repo tests (creation, parsing, deletion, flow, setup_repos) — all 80 pass
- Made tmux calls in
setup_repos_from_task_md()non-fatal (warn + skip instead of propagating errors) - Guard setup-only task creation against multi-repo selection — shows error "Multi-repo tasks require a description"
- Multi-repo attach session selection —
View::SessionPickeroverlay lists repo tmux sessions, user picks which to attach to - Fixed stale comments referencing "worktree" for TASK.md location (now writes to task dir)
(No remaining items — all planned features are implemented.)
Build: Compiles. Tests: All 80 pass.
What was done:
-
Setup-only + multi-repo guard: Added
is_multi_repocheck before the empty-description branch inwizard_next_step(). Multi-repo parent dirs now show error "Multi-repo tasks require a description" instead of attempting a single-repo setup-only task that would fail. -
Multi-repo attach session picker: Added a new
View::SessionPickerwith full TUI overlay:app.rs: New state fields (session_picker_sessions,selected_session_index,attach_session_name), event handler (handle_session_picker_event), updated preview attach logic to open picker for multi-repo tasks with >1 repo.ui.rs: Newdraw_session_picker()function rendering a centered popup with repo name list, keybinding hints shared withRebaseBranchPicker.run_tuiloop: Checksattach_session_namefirst (set by picker) before falling back to primary_repo logic.
-
Stale comments: Fixed 3 comments in
task.rsandtests/use_cases_test.rsthat said "worktree" where TASK.md now lives in the task dir.