td-barrage
v0.2.4
Published
A persistent, dependency-aware task queue for the pi coding agent.
Readme
td-barrage
A persistent, dependency-aware task queue for the pi coding agent. It loads a JSON task file, runs ready tasks through pi, saves state after every transition, and resumes interrupted work on the next invocation.
Requirements
- Node.js 22.19 or newer
- npm
- pi authenticated with a model provider (
pi, then/login), or a supported provider API key in the environment
Install
In a target project:
npm install --save-dev td-barrageOr run ad-hoc without installing:
npx td-barrage --tasks queue.jsonOnce installed, the td-barrage CLI is on the project PATH:
npx td-barrage --tasks queue.jsonFor local development on this package:
npm install
npm run buildTask File
Task files use a top-level tasks array. A bare array is also accepted when loading.
{
"tasks": [
{
"id": "build",
"prompt": "Create .queue/results/build.json with {\"status\":\"ok\"}.",
"success": { "strategy": "result_json", "path": ".queue/results/build.json" }
},
{
"id": "verify",
"prompt": "Verify the build result and create .queue/results/verify.json with {\"status\":\"ok\"}.",
"dependsOn": ["build"],
"timeoutMs": 600000,
"maxAttempts": 2,
"success": { "strategy": "result_json", "path": ".queue/results/verify.json" }
}
]
}Supported task fields:
id: unique task idprompt: prompt sent to pidependsOn: dependency idsstatus:pending,running,done,failed, orblockedattempts: persisted attempt countermaxAttempts: per-task retry limittimeoutMs: per-task prompt timeoutcwd: per-task working directorysuccess:default,file_exists, orresult_jsonrecoveryNotes,error,startedAt,finishedAt: runtime metadata
CLI
With no path given, the queue is read from .agents/skills/barrage/queue.json, resolved against --cwd (the process working directory by default):
npx td-barragenpm run dev -- --tasks queue.json
npm run dev -- queue.json --max-attempts 3 --cwd /path/to/work
npm run dev -- --tasks queue.json --dry-run
npm run dev -- --tasks queue.json --json
npm run dev -- --tasks queue.json --verbose --timestampsOptions:
--tasks, -t <path>: task file (also accepted as a positional argument); defaults to.agents/skills/barrage/queue.jsonunder--cwd--max-attempts <n>: global retry limit (per-taskmaxAttemptsoverrides it)--timeout-ms <ms>: global per-prompt timeout, default 600000 (per-tasktimeoutMsoverrides it)--cwd <path>: working directory for tasks that do not set their own--dry-run: print the topological plan (one id per line) and exit without running--no-commit: skip the git commit stage that otherwise runs after each successful task--json: emit newline-delimited JSON events instead of human output
Exit codes:
0: all reachable tasks completed1: at least one task failed or became blocked2: CLI usage or missing task-file error75: the run paused on a provider quota / rate-limit wall (see below)
Pi sessions
Each task runs in its own in-memory pi agent session scoped to the task working directory. Barrage uses pi's existing authentication and model settings. Terminal provider errors from pi's final assistant state feed into the pause/resume flow below. Sessions are disposed after their task prompt settles.
Pausing on quota / rate limits
Running out of provider tokens is treated as a property of the run, not of any
task. When a prompt fails with a quota, rate-limit, or overload signal (HTTP
402/429/503/529, or matching error wording), the runner:
- rolls back the attempt it charged — the task did not really run, so its retry budget is untouched,
- leaves the task
pending(exactly as crash recovery does), - stops the rest of the sweep immediately, since every ready task would hit the same wall, and
- exits
75(EX_TEMPFAIL) with the reason on stderr.
Because task state and workspace changes are persisted, resuming is just
re-running the same command once tokens are back. The pending task starts a fresh
in-memory pi session and can inspect the work already on disk; conversation
history is not restored. No failed or blocked task is recorded for the pause.
The distinct exit code lets a wrapper or scheduler tell "try again later" apart
from 1 ("something is broken"). A Retry-After header, when present, is
surfaced as a suggested wait in the pause message.
Telemetry & output
By default the runner is deliberately verbose, narrating progress as it happens:
a startup header (version, task file, cwd, limits), the resolved
execution plan, a line as each task starts, a running account of what the model
is doing inside the task (below), and a line as each task finishes with
its duration and overall progress (2/5 complete). Failures, retries, and blocks
are always reported, along with a closing summary and total elapsed time.
In-progress activity
A single task can run for minutes. Rather than leave that as a silent spinner, Barrage subscribes to the pi session for the life of the prompt and reports each step the model takes:
▶ add-parser starting (attempt 1/2)
↳ thinking…
↳ I'll look at the existing lexer first, then add the parser beside it.
↳ grep tokenize
↳ read src/lexer.ts
↳ write src/parser.ts
↳ bash npm test -- --run
↳ bash failed: FAIL test/parser.test.ts — 2 failed
✔ add-parser done in 1m12s · 1/1 completeFour kinds of step are reported: thinking (the model is working but has
produced nothing yet), the assistant's own prose, a tool call with the argument
that best identifies it (a command, a search pattern, or a path), and a tool
call that failed. Successful tool calls get no line of their own — the next step
supersedes them. Arguments and prose are collapsed to a single line and clipped
to 80 characters, and raw model thinking is never printed.
On an interactive terminal these fold into the live spinner line, so the newest
step replaces the last rather than stacking rows; piped or in CI they are plain
↳ lines as above. They appear at normal verbosity and are suppressed by
--quiet. In --json mode each one is a task_activity event carrying the
task id and a structured activity object, so a log aggregator can reconstruct
the trace.
On that live line a step holds its place for at least four seconds, so a burst of
quick tool calls is read one at a time instead of flashing past. Steps that
arrive inside that window queue behind it and are shown in turn; a task that
outruns the window for long keeps only the three most recent waiting steps, so
the line stays close to what the model is doing now rather than drifting minutes
behind it. When a task ends, whatever is still waiting is dropped and the result
line lands at once. Piped output and --json are unpaced — every step is written
as it happens.
Verbosity flags:
--verbose, -v: add per-step traces — the agent session id and the success-check result for every task--quiet, -q: show only failures, blocks, and the final summary--timestamps: prefix each line with a wall-clock time--color/--no-color: force or disable ANSI color (auto-detected from the TTY otherwise; theNO_COLORandFORCE_COLORenvironment variables are honored)
--json emits every event — including the enriched fields above — as one JSON
object per line, regardless of verbosity, which is the format to pipe into log
aggregators or jq. --dry-run always prints a bare, uncolored id list so it
stays easy to pipe.
Success Checks
default succeeds unconditionally.
file_exists succeeds when the configured path exists relative to the task cwd.
result_json succeeds when the configured JSON file parses and contains "status": "ok".
Commits
After a task passes its success check, the orchestrator stages everything with
git add -A and commits it in the task's cwd with the message feat: <id>. A
clean working tree (nothing to commit) is a quiet no-op, and a commit that fails
outright is reported as a warning but never flips the task back to failed — the
task already succeeded. Pass --no-commit to skip this stage entirely.
Recovery
On startup, any task persisted as running is reset to pending, annotated in recoveryNotes, and retried. The attempts counter is preserved, so a task that crashed mid-run still counts that attempt.
Scripts
npm test
npm run test:watch
npm run typecheck
npm run build
npm run test:integrationIntegration tests are excluded by default. Run them with:
RUN_INTEGRATION=1 npm run test:integrationThe integration test runs real pi sessions using your configured authentication and model, so it may consume provider credits.
Architecture
The orchestrator accepts its dependencies as arguments:
client: pi-compatible agent client seamfs: filesystem seamclock: timer seam for timeout testslog: human or JSON event logger
Pi's session event stream is translated into Barrage's small progress vocabulary
in src/activity.ts, which is pure and typed against unknown at the boundary:
an unfamiliar or newly added pi event is ignored rather than crashing a run. The
minimum time a step holds the live spinner row lives in src/pace.ts, a generic
queue with an injected timer seam, so its rules are tested on a hand-wound clock
rather than in real time.
This keeps most behavior covered by fast unit tests using a fake client and in-memory filesystem.
