ai-dispatch 5.7.0

Multi-AI CLI team orchestrator
## v1.4 — 6 improvements from usage feedback

[[task]]
name = "agent-profiles"
agent = "opencode"
skills = ["implementer"]
prompt = """
Enhance `aid config agents` to show rich capability profiles for each agent.

GOAL: `aid config agents` outputs a detailed roster card per agent showing strengths, cost, best-fit keywords, and streaming mode.

EXISTING CODE:
- src/cmd/config.rs has ConfigAction::Agents handler (currently just lists agent names)
- src/agent/mod.rs has detect_agents() and Agent trait with .streaming()
- src/agent/selection.rs has RESEARCH_TERMS, SIMPLE_EDIT_TERMS, FRONTEND_TERMS, COMPLEX_TERMS
- src/cost.rs has model_pricing() with per-model rates

IMPLEMENTATION:

1. In src/cmd/config.rs, replace the simple listing with rich output:
   For each AgentKind (not just detected ones), print:
   - Name + installed status (checkmark or cross)
   - "Strengths:" line describing what it's good at
   - "Cost:" line with typical cost range
   - "Best for:" line with keywords that trigger selection
   - "Mode:" streaming or buffered

   Use a helper function agent_profile(kind: AgentKind, installed: bool) -> String.
   Hardcode the profile data as const arrays — don't import from selection.rs.

2. Also implement ConfigAction::Pricing:
   Print a table of known model prices from cost.rs data:
   Model, Input/M, Output/M, Blended/M

CONSTRAINTS:
- Only modify src/cmd/config.rs
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "feat/agent-profiles"
verify = "auto"

[[task]]
name = "show-diff-vs-main"
agent = "opencode"
skills = ["implementer"]
prompt = """
Fix `aid show --diff` to show changes vs main branch, not just uncommitted changes.

BUG: When agent commits in a worktree, `git diff` shows nothing (clean tree).
The fallback `git diff HEAD~1` only shows the last commit, missing earlier ones.

EXISTING CODE:
- src/cmd/show.rs has diff_stat() and full_diff() functions (~line 340)
- Both call git_output(wt_path, &["diff", ...])
- Task struct has worktree_branch field (the feature branch name)

FIX:

1. In diff_stat(), change the git command to:
   First try: git diff main...HEAD --stat
   If that's empty, fall back to: git diff --stat
   If still empty: git diff HEAD~1 --stat

2. In full_diff(), same pattern:
   First try: git diff main...HEAD
   Fallback: git diff
   Fallback: git diff HEAD~1

This ensures committed changes on feature branches show up in review.

CONSTRAINTS:
- Only modify src/cmd/show.rs
- Only change diff_stat() and full_diff() functions
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "feat/show-diff-fix"
verify = "auto"

[[task]]
name = "dag-dependency-scheduling"
agent = "codex"
skills = ["implementer"]
prompt = """
Change batch DAG scheduling from level-based to true dependency-based dispatch.

BUG: Currently dispatch_parallel_with_dependencies() processes tasks by topological levels.
A task in level 2 waits for ALL level 1 tasks even if its specific dependency finished early.

EXISTING CODE:
- src/cmd/batch.rs has dispatch_parallel_with_dependencies() at ~line 104
- Uses batch::topo_levels() which returns Vec<Vec<usize>> (levels)
- Each level is dispatched together, then wait_for_task_ids blocks until all complete

NEW APPROACH:
Replace the level-based loop with a dependency-ready loop:

1. Remove the topo_levels() call
2. Use a loop that:
   a. Find all tasks whose dependencies are ALL satisfied (done/failed in outcomes)
   b. Skip tasks whose dependencies failed (mark as Skipped)
   c. Dispatch all ready tasks in parallel
   d. Wait for ANY of the dispatched tasks to complete (not all)
   e. Update outcomes, repeat until all tasks processed

Key change: use tokio::select! or poll individual tasks to detect completion one-by-one,
then immediately check if new tasks become ready.

Simpler alternative if tokio::select is complex:
Keep dispatching in rounds, but each round finds ALL tasks with satisfied deps (not just one level).
This is still better than level-based because tasks from different levels can dispatch together.

CONSTRAINTS:
- Only modify src/cmd/batch.rs
- Keep batch::topo_levels() and batch::dependency_indices() as-is (they may be used elsewhere)
- Do NOT reformat existing code
- Add test that verifies early dispatch when deps are satisfied
- cargo check and cargo test must pass
"""
dir = "."
worktree = "feat/dag-scheduling"
verify = "auto"

[[task]]
name = "zombie-recovery"
agent = "opencode"
skills = ["implementer"]
prompt = """
Add worktree change preservation when detecting zombie tasks.

GOAL: When check_zombie_tasks detects a dead worker with a worktree, auto-commit any uncommitted changes before marking FAILED.

EXISTING CODE:
- src/background.rs has check_zombie_tasks_with() at ~line 188
- It calls record_failure() to mark tasks as failed
- src/commit.rs has has_uncommitted_changes(dir) and auto_commit(dir, task_id, prompt)
- Task struct has worktree_path and prompt fields
- Store has get_task(task_id) method

IMPLEMENTATION:

In check_zombie_tasks_with(), before calling record_failure():
1. Load the task from store: store.get_task(task_id)
2. If task has a worktree_path that exists on disk:
   a. Check has_uncommitted_changes(worktree_path)
   b. If yes, call auto_commit(worktree_path, task_id, &task.prompt)
   c. Log: eprintln!("[aid] Preserved uncommitted changes for zombie task {task_id}")
3. Then proceed with record_failure() as before

This preserves partial work from crashed agents.

CONSTRAINTS:
- Only modify src/background.rs
- ~10 lines of new code
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "feat/zombie-recovery"
verify = "auto"

[[task]]
name = "batch-archive"
agent = "opencode"
skills = ["implementer"]
prompt = """
Auto-archive batch TOML files after dispatch to ~/.aid/batches/.

GOAL: After `aid batch file.toml` finishes dispatching, copy the file to ~/.aid/batches/{timestamp}-{stem}.toml.

EXISTING CODE:
- src/cmd/batch.rs has run() function
- crate::paths has aid_dir() returning ~/.aid path

IMPLEMENTATION:

In src/cmd/batch.rs run(), after all dispatching is done (before the final println):
1. Create archive dir: let archive_dir = crate::paths::aid_dir().join("batches");
2. std::fs::create_dir_all(&archive_dir)
3. Generate filename: {YYYYMMDD-HHMMSS}-{stem}.toml
4. Copy the original batch file to archive_dir
5. eprintln!("[aid] Archived batch to {dest}")
6. If copy fails, just eprintln the error and continue (don't fail the command)

CONSTRAINTS:
- Only modify src/cmd/batch.rs
- ~8 lines of new code
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "feat/batch-archive"
verify = "auto"

[[task]]
name = "completion-hook"
agent = "opencode"
skills = ["implementer"]
prompt = """
Add task completion hook support via --on-done flag.

GOAL: `aid run codex "task" --on-done "notify-send 'done'"` executes the command when the task finishes.

EXISTING CODE:
- src/main.rs Commands::Run has the CLI args
- src/cmd/run.rs has RunArgs struct and run() function
- src/background.rs has BackgroundRunSpec and run_task() function
- run_task() calls run_task_inner() then cleans up

IMPLEMENTATION:

1. In src/main.rs Commands::Run, add:
   #[arg(long)]
   on_done: Option<String>,

2. In src/cmd/run.rs RunArgs, add:
   pub on_done: Option<String>,
   Wire it in main.rs match arm.

3. In src/background.rs BackgroundRunSpec, add:
   #[serde(default)]
   pub on_done: Option<String>,

4. In src/cmd/run.rs where BackgroundRunSpec is created, pass on_done.

5. In src/background.rs run_task(), after run_task_inner() completes (success or fail):
   if let Some(ref cmd) = spec.on_done {
       let status = if result.is_ok() { "done" } else { "failed" };
       let _ = std::process::Command::new("sh")
           .args(["-c", cmd])
           .env("AID_TASK_ID", task_id)
           .env("AID_TASK_STATUS", status)
           .spawn();
   }

CONSTRAINTS:
- Only modify src/main.rs, src/cmd/run.rs, src/background.rs
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "feat/completion-hook"
verify = "auto"