ai-dispatch 7.2.2

Multi-AI CLI team orchestrator
## v1.6 — Quality hardening: refactoring, bug fixes, test coverage

[[task]]
name = "fix-truncate-dup"
agent = "opencode"
skills = ["implementer"]
prompt = """
Fix unsafe byte-level truncate in src/cmd/run.rs by replacing it with the existing safe truncate_text from src/agent/truncate.rs.

BUG: src/cmd/run.rs line 343-349 has a `truncate()` function that does `&s[..max.saturating_sub(3)]` which panics on multi-byte UTF-8 characters. Meanwhile, src/agent/truncate.rs already has a safe `truncate_text()` using `floor_char_boundary`.

FIX:
1. In src/cmd/run.rs, delete the local `truncate()` function (lines 343-349)
2. Replace all calls to `truncate(...)` in that file with `crate::agent::truncate::truncate_text(...)`
3. The truncate_text function also replaces newlines with spaces, which is fine for prompt display

CONSTRAINTS:
- Only modify src/cmd/run.rs
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "fix/truncate-dup"
verify = "auto"

[[task]]
name = "fix-bg-parent-id"
agent = "opencode"
skills = ["implementer"]
prompt = """
Add parent_task_id to BackgroundRunSpec so background retries preserve the parent link.

BUG: BackgroundRunSpec in src/background.rs (lines 18-36) has no parent_task_id field.
When retry_if_needed runs from run_task_inner (background mode), the retry's RunArgs gets
parent_task_id set at src/cmd/run.rs:270, but when that retry itself runs in background mode,
the parent_task_id is lost because BackgroundRunSpec doesn't carry it.

FIX:
1. In src/background.rs BackgroundRunSpec struct, add:
   #[serde(default)]
   pub parent_task_id: Option<String>,

2. In src/cmd/run.rs where BackgroundRunSpec is constructed (search for BackgroundRunSpec {),
   add: parent_task_id: args.parent_task_id.clone(),

3. In src/background.rs run_task_inner() where RunArgs is constructed from the spec
   (search for RunArgs {), add: parent_task_id: spec.parent_task_id.clone(),

CONSTRAINTS:
- Only modify src/background.rs and src/cmd/run.rs
- ~3 lines of actual change
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "fix/bg-parent-id"
verify = "auto"

[[task]]
name = "fix-gemini-model"
agent = "opencode"
skills = ["implementer"]
prompt = """
Extract model name from Gemini JSON output so usage reports show the actual model instead of None.

CURRENT: src/agent/gemini.rs parse_completion() returns CompletionInfo with model: None.
The extract_tokens() function parses the JSON but never looks for the model field.

FIX: Add an extract_model() function and use it in parse_completion().

1. Add to src/agent/gemini.rs after extract_tokens():
   fn extract_model(v: &serde_json::Value) -> Option<String> {
       // Try common paths in gemini CLI JSON output
       for path in ["/modelVersion", "/model", "/stats/models/0/model"] {
           if let Some(m) = v.pointer(path).and_then(|v| v.as_str()) {
               return Some(m.to_string());
           }
       }
       None
   }

2. In parse_completion() (around line 38), after calling extract_tokens(),
   also call extract_model() and set it on CompletionInfo:
   model: extract_model(&v),

3. Add a unit test that verifies extract_model parses a sample gemini JSON blob.

CONSTRAINTS:
- Only modify src/agent/gemini.rs
- ~15 lines of new code
- Do NOT reformat existing code
- cargo check and cargo test must pass
"""
dir = "."
worktree = "fix/gemini-model"
verify = "auto"

[[task]]
name = "refactor-run-retry"
agent = "codex"
skills = ["implementer"]
prompt = """
Extract retry logic from src/cmd/run.rs into a separate module to reduce file size (504 lines → under 400).

CURRENT STATE:
- src/cmd/run.rs has retry-related functions at lines 229-273 and 393-438:
  - retry_if_needed (lines 229-273): async, re-dispatches failed tasks
  - read_stderr_tail (lines 393-404): reads last N lines of stderr log
  - retry_depth (lines 406-417): walks parent chain to count depth
  - backoff_for_attempt (lines 419-425): exponential backoff calculator
  - root_prompt (lines 427-438): walks parent chain to find original prompt

REFACTOR:
1. Create src/cmd/retry_logic.rs with these functions moved from run.rs:
   - retry_if_needed (pub(crate))
   - read_stderr_tail (pub(crate) — also used by show.rs explain)
   - retry_depth (private)
   - backoff_for_attempt (private)
   - root_prompt (private)

2. The file header should be:
   // Retry logic for failed tasks: depth tracking, backoff, re-dispatch.
   // Called from run.rs on task failure when --retry > 0.

3. retry_if_needed calls run::run() recursively via Box::pin. To avoid circular imports,
   have retry_logic.rs accept a closure or function pointer for the re-dispatch,
   OR keep the Box::pin(run(...)) call in run.rs and have retry_logic.rs return
   a RetryAction enum (Retry(RunArgs) | Skip | Stop) that run.rs acts on.

   Preferred approach: retry_logic.rs exports prepare_retry() that returns Option<RunArgs>.
   run.rs calls prepare_retry(), and if Some(retry_args), calls Box::pin(run(store, retry_args)).

4. Update src/cmd/mod.rs to add: pub mod retry_logic;

5. Update run.rs to import and call retry_logic::prepare_retry() instead of the moved functions.

6. Move the two tests for effective_skills that are in run.rs's #[cfg(test)] — leave them in run.rs (they test run.rs code). Add new tests in retry_logic.rs for:
   - backoff_for_attempt returns increasing values
   - retry_depth returns 0 for task with no parent

CONSTRAINTS:
- Create src/cmd/retry_logic.rs (under 120 lines)
- Modify src/cmd/run.rs (should drop to under 400 lines)
- Modify src/cmd/mod.rs (add the module)
- Do NOT reformat existing code in run.rs beyond what's needed for the extraction
- cargo check and cargo test must pass
"""
dir = "."
worktree = "refactor/run-retry"
verify = "auto"

[[task]]
name = "refactor-show-explain"
agent = "codex"
skills = ["implementer"]
prompt = """
Extract the explain subsystem from src/cmd/show.rs into src/cmd/explain.rs to reduce file size (463 lines → under 350).

CURRENT STATE:
- src/cmd/show.rs lines 171-314 contain the explain feature:
  - run_explain (async fn, lines 171-208): dispatches a gemini task to analyze another task
  - build_explain_context (lines ~210-240): gathers task info, events, stderr, log
  - build_explain_prompt (lines ~242-270): formats the analysis prompt
  - format_task_info (lines ~272-295): formats task metadata
  - format_events (lines ~297-314): formats event list

REFACTOR:
1. Create src/cmd/explain.rs with all 5 functions moved from show.rs:
   - run_explain (pub(crate) async)
   - build_explain_context (pub(crate) for testing)
   - build_explain_prompt (pub(crate) for testing)
   - format_task_info (private)
   - format_events (private)

2. File header:
   // AI-powered task explanation: dispatches a research agent to analyze task artifacts.
   // Called from show.rs when `aid show --explain` is used.

3. explain.rs will need these imports from show.rs:
   - load_task, read_tail (these are shared helpers in show.rs)
   - Make load_task and read_tail pub(crate) in show.rs so explain.rs can use them

4. Update src/cmd/mod.rs to add: pub mod explain;

5. In show.rs, replace the call to run_explain with cmd::explain::run_explain.

6. Move the build_explain_prompt test from show.rs's #[cfg(test)] to explain.rs's #[cfg(test)].

CONSTRAINTS:
- Create src/cmd/explain.rs (under 150 lines)
- Modify src/cmd/show.rs (should drop to under 350 lines)
- Modify src/cmd/mod.rs (add the module)
- Do NOT reformat existing code beyond what's needed
- cargo check and cargo test must pass
"""
dir = "."
worktree = "refactor/show-explain"
verify = "auto"

[[task]]
name = "test-retry-logic"
agent = "codex"
skills = ["implementer", "test-writer"]
depends_on = ["refactor-run-retry"]
prompt = """
Add comprehensive tests for the retry logic in src/cmd/retry_logic.rs.

After the refactor-run-retry task extracted retry logic into retry_logic.rs,
add thorough unit tests for the extracted functions.

TESTS TO ADD (in retry_logic.rs #[cfg(test)]):

1. test_backoff_exponential — verify backoff increases: attempt 1 → 2s, 2 → 4s, 3 → 8s, capped reasonably
2. test_retry_depth_no_parent — task with no parent_task_id → depth 0
3. test_retry_depth_with_chain — create 3 tasks in store with parent links, verify depth = 2
4. test_prepare_retry_skips_on_success — task with status Done → returns None
5. test_prepare_retry_skips_on_zero_retries — retry count 0 → returns None
6. test_prepare_retry_returns_args — failed task with retry > 0 → returns Some(RunArgs) with correct prompt containing error context
7. test_prepare_retry_stops_on_identical_stderr — same stderr as parent → returns None

Use Store::open_memory() for in-memory DB in tests.

CONSTRAINTS:
- Only modify src/cmd/retry_logic.rs (add #[cfg(test)] block)
- Tests must use real Store (open_memory), not mocks
- cargo test must pass
"""
dir = "."
worktree = "test/retry-logic"
verify = "auto"

[[task]]
name = "test-e2e-workflow"
agent = "codex"
skills = ["implementer", "test-writer"]
prompt = """
Add E2E tests that exercise core aid workflows via the compiled binary.

EXISTING E2E TESTS: tests/e2e_test.rs has basic CLI tests (--help, board, watch, config, group CRUD).
EXISTING PATTERN: tests use `Command::cargo_bin("aid")` style via std::process::Command pointing to cargo build output.

ADD these E2E tests in a new file tests/e2e_workflow.rs:

1. test_run_with_fake_agent — Create a fake agent script that echoes output, run `aid run <fake> "test prompt" --dir .`,
   verify task appears in `aid board` output and `aid show <task-id>` works.

   Fake agent setup (use a temp dir with a shell script):
   - Create a script called "fake-agent" that prints "[MILESTONE] started" then "Hello from fake agent" then exits 0
   - Set PATH to include the temp dir
   - This requires setting AID_HOME to a temp dir too

2. test_show_output_mode — Run a task with `--output /tmp/test-output.txt` using the fake agent,
   then verify `aid show --output <task-id>` reads the file content.

3. test_batch_dispatch — Create a simple batch TOML with 2 tasks using fake agent,
   run `aid batch test.toml --parallel --wait`, verify both tasks appear in board as Done.

4. test_board_filters — Create tasks, then verify `aid board --today` shows them
   and `aid board --mine` filters correctly.

Set AID_HOME to a temp directory in each test to isolate state.
Use #[ignore] attribute on tests that require the aid binary to be built first (cargo test -- --ignored to run them).

CONSTRAINTS:
- Create tests/e2e_workflow.rs
- Use std::process::Command to invoke the aid binary
- Each test must clean up its temp dirs
- Tests should be #[ignore] since they need a built binary
- cargo test (without --ignored) must still pass
- cargo test -- --ignored should pass the new tests
"""
dir = "."
worktree = "test/e2e-workflow"
verify = "auto"