2026-07-18 · Updated 2026-08-27 · 5 min read

Run kanban tasks safely in parallel

Admit only dependency-ready tasks, cap concurrency below provider limits, isolate each worker, and review per-run evidence before closing tasks.

By Juno AI INC · parallel-runner · yylo-ledger

Parallelism is safe only when tasks do not compete for an ordering boundary, a mutable file, or an external quota they cannot share. That test becomes four decisions you make before launching Parallel Runner: which tasks may start (readiness), how many start at once (the worker cap), what each worker touches (isolation), and what the run leaves behind (evidence). Each has its own failure mode, and each has a concrete check.

Make readiness the only admission rule

The dependency graph, not arrival order, decides what may run concurrently. Declare the edges before the batch exists, then admit work only through ready:

sh
./.juno_task/scripts/kanban.sh deps add --id UI_TASK --blocked-by API_TASK
./.juno_task/scripts/kanban.sh ready --sort asc
./.juno_task/scripts/kanban.sh deps TASK_ID

ready lists tasks whose declared blockers are all resolved. A related task is context, not a dependency edge — related_tasks links work without ever blocking it, so use it for reading lists, never for ordering. When a task you expected is missing from the list, deps TASK_ID names the blockers still holding it.

Feed the runner from that admission set. The dependency-aware path resolves ready IDs first and passes them explicitly; list filters select the queue but do not check readiness, so run them together with a ready check, not instead of one:

sh
./.juno_task/scripts/kanban.sh ready --tag frontend --limit 6 -f json --raw
./.juno_task/scripts/parallel_runner.sh --kanban T1,T2,T3 --parallel 2
./.juno_task/scripts/parallel_runner.sh --kanban-filter "--tag frontend --status todo --limit 6" --parallel 2

Keep the filter quoted so it forwards as one argument. If one task needs another task's response — not just its completion — it is not parallel work; the ordering belongs to Workflow Runner instead.

Cap the pool below every real limit

--parallel N is a hard ceiling on concurrent subprocesses (the default is 3). Each admitted task runs as one bounded agent invocation, so N is also your concurrent provider-session count: keep it below provider concurrency and rate limits, and below what the machine can serve. The cap is a rate limiter, never a correctness mechanism — correctness comes from the readiness rule above.

sh
./.juno_task/scripts/parallel_runner.sh -s pi --subagent-args "--on-hourly-limit wait" --kanban T1,T2 --parallel 2

--subagent-args reach every worker invocation as raw, shell-split arguments, so quota flags ride along with the whole pool: --on-hourly-limit wait parks workers on hourly provider limits instead of failing them mid-batch. One mode has a fixed ceiling of its own — Pi live mode under --tmux is supported only with --parallel 1, because an interactive pane cannot be shared.

Give each worker its own boundary

Every task runs as its own process with its own materialized prompt file, its own task_<TASK_ID>.log, and its own structured result; the shared parallel_runner.log is only a combined view. The per-task prompt file is why the task placeholder matters:

sh
./.juno_task/scripts/parallel_runner.sh \
  --kanban T1,T2 \
  --prompt-file .juno_task/prompts/implement-task.md \
  --parallel 2

A custom prompt does not inject the task automatically — include {{task_id}} (for example Do ##{{task_id}}) so each worker reads its own task body, and keep the prompt file reviewable.

Process separation isolates workers from each other, not from shared state. Two tasks that edit the same file, or draw on the same external quota, are coupled in fact even when the Kanban declares no edge between them: declare the dependency or serialize the work. Tmux modes make the boundaries visible — --tmux windows, panes, or tabs — and --tmux-handoff dedicates one pane per task that is never reused after completion, with --max-panes-per-session N splitting large lists into auditable child sessions:

sh
./.juno_task/scripts/parallel_runner.sh --tmux panes --tmux-handoff \
  --max-panes-per-session 4 --kanban T1,T2,T3,T4,T5

Name deliberate sessions with --name (the tmux session becomes pc-<name>) and stop them just as deliberately: --stop ends the selected session and --stop-all is only for intentional broad shutdown.

Judge the run by its artifacts, not its exit code

The run's exit code tells you only that something failed — which item, why, and what to continue next lives in the artifacts under the printed output directory (--output-dir, or /tmp/yylo-sessions/<date>/<run_id> by default):

  • parallel_runner_status.json — the run's completion and exit truth; the wait helper reads exactly this file.
  • Per-task JSON — the exit code, session_id, wall time, cost, worker ID, and extracted response of each task.
  • aggregation_*.json — one batch summary: totals, succeeded and failed counts, parallelism, and per-task session rows.
sh
nohup ./.juno_task/scripts/parallel_runner.sh --kanban T1,T2,T3 --parallel 2 >/tmp/parallel.out 2>&1 &
./.juno_task/scripts/parallel_runner_wait.sh --timeout 7200 --verbose

The wait helper turns a background launch into a deterministic step, and --verbose streams the combined log while it waits. When items must return structured output, --strict --file-format json writes each task's fenced response to <task_id>.json and marks the task failed when the block is missing — extraction failures surface as task failures, not silent gaps.

Inspect before retrying. A failed task's JSON carries its exit code and session ID, so continue exactly that conversation with yy continue SESSION_ID or rerun that task ID alone once the failure is understood; do not relaunch the successful majority. Close each task only on evidence you have actually read — the response, the diff, and the validating commit — never on a batch summary alone.