pueue-job-queue¶
Drive Nukesor/pueue — a daemon-backed
shell job queue — for queued, parallel, scheduled, and lightly-DAG'd shell
work. The skill is a CLI bridge, not a custom scheduler: it wraps
pueue add --after, pueue status --json, and pueue log --json so an
agent can submit, batch, chain, wait, and retry without losing track of
what's running.
| Surface | Question it answers |
|---|---|
check-daemon.sh |
"Is pueued running, and where do logs live on this OS?" |
submit.sh |
"Submit one task and give me a parseable {task_id, label, group, after}." |
submit-dag.py |
"Submit this whole fan-out / fan-in pipeline with deps wired — in a fresh isolated group sized to the DAG width." |
wait.py |
"Block until these tasks finish, then summarize success/failure." |
cleanup.sh |
"Reclaim disk + status latency: prune old tasks, empty groups, log files." |
references/cli-cheatsheet.md |
"What un-wrapped pueue subcommand do I reach for?" |
references/json-schema.md |
"What does pueue status --json actually look like? What's the QUERY DSL syntax?" |
references/dag-patterns.md |
"How do I express fan-out / fan-in / diamond shapes?" |
references/daemon-and-config.md |
"How do I auto-start pueued on macOS / Linux?" |
references/remote-daemon.md |
"The daemon is on another machine — can I drive it, and should I?" |
The skill exists to keep three things out of an agent's way:
- Bookkeeping — task ids, dependency wiring, group autocreate, defensive
id parsing of
pueue add --print-task-id. - Schema drift —
pueue status --jsonuses serde tagged enums ({"Done": {"result": "Success" | "DependencyFailed" | "Killed" | {"Failed": <int>}}}).wait.pyclassifies all variants and returns predictable exit codes (0ok,5any failure,6timeout). - Footguns —
pueue add -- bash -c 'sleep 60'does NOT preserve the inner quote (pueue re-shells);pueue kill --allalso pauses every group;pueue restart(default) creates a NEW id;pueue clean(default) wipes failures too. The SKILL.md has a 10-item gotcha list covering each of these.
When the skill triggers¶
- "Run these 30 commands, max 4 at a time" → set group parallelism, loop
submit.sh --group sweep. - "Kick off a long training job and let me close my laptop" →
submit.sh -- ./train.sh(pueue persists across reboots). - "Run task B only after task A succeeds" →
submit.sh --after $A_ID -- ./b.sh. - "Fan out 4 trainings, then evaluate" →
submit-dag.py dag.yaml. - "Schedule this for tonight" →
submit.sh --delay 6h -- ./nightly.sh. - The user says pueue / pueued / pueue add / pueue queue / pueue group or asks about a "task queue for shell jobs".
When it doesn't¶
- One short shell command — just run it. Pueue adds daemon overhead.
- Cross-host scheduling, OR-deps, conditional branching, retry-with-backoff,
typed task IO — escalate to Airflow / Prefect / Dagster / DVC / Slurm.
references/dag-patterns.mdhas a decision table. - Long-running services — that's
systemd/launchd.
Structure¶
skills/local/pueue-job-queue/
├── SKILL.md # ~275 lines
├── scripts/
│ ├── check-daemon.sh # bash; daemon health + auto-start
│ ├── submit.sh # bash; submit-one wrapper, JSON out, group autocreate
│ ├── wait.py # PEP 723; block until terminal, state-change events, JSON summary
│ ├── submit-dag.py # PEP 723 (pyyaml); DAG submitter with --isolated-group / --auto-parallel
│ └── cleanup.sh # bash; prune tasks + empty groups + old log files
├── references/
│ ├── cli-cheatsheet.md # un-wrapped commands
│ ├── json-schema.md # observed status --json shape (4.0.2) + QUERY DSL + jq recipes
│ ├── dag-patterns.md # fan-out / fan-in / diamond + escalation table
│ ├── daemon-and-config.md # pueued setup per OS, config knobs, log paths
│ └── remote-daemon.md # SSH socket forwarding, TCP+TLS, the client-resolved-cwd trap
├── assets/
│ ├── dag.example.yaml # 5-task fan-out / fan-in fixture
│ └── pueue.yml.example # config with `pause_group_on_failure: true`
└── tests/
├── conftest.py # isolated pueued fixture, skip if pueue absent
├── test_submit.py # submit.sh paths + dependency failure
├── test_dag.py # topo invariant, cycle/unknown/missing-cmd, isolated-group
├── test_wait.py # success/failure/timeout exit codes
├── test_contracts.sh # bash --help/error-code contract
└── fixtures/simple-dag.yaml # 4-task diamond
Always pass a label¶
pueue status shows the label column before the command. In a busy
queue, scanning labels is the only way to tell train.py runs apart at a
glance. The skill's gotchas section opens with:
Always pass
--labelwhen submitting, and bias toward names that distinguish this task from its siblings.
Convention: <verb>-<subject>-<key> (≤30 chars). Encode the discriminator
(seed, dataset slice, model variant) — not the command:
| Good | Bad |
|---|---|
train-baseline-seed1 |
task1 |
eval-prod-2026q1 |
python eval.py --quarter 2026q1 |
nightly-featurize (DAG) |
step-2-of-5 |
submit-dag.py --label-prefix <run>- produces <run>-<task_name> so
wait.py --label-prefix <run>- selects the whole graph and pueue clean
can be filtered later.
Empirical schema¶
Pueue's JSON output isn't formally documented in the wiki. The skill ships
the observed shape on pueue 4.0.2 in references/json-schema.md,
including the diagnostic snippet to re-verify on a different major. Key
shape:
{
"tasks": {
"<id>": {
"id": 17,
"label": "train-baseline-seed1",
"group": "ml",
"dependencies": [12, 14],
"status": {
"Done": {
"enqueued_at": "...", "start": "...", "end": "...",
"result": "Success" // or "DependencyFailed" | "Killed" | {"Failed": 1}
}
}
}
},
"groups": {
"default": {"status": "Running", "parallel_tasks": 1}
}
}
wait.py classifies every variant; if a future pueue release adds one
(say Suspended), the test suite catches it before the skill goes silent.
Verification¶
The skill ships 22 pytest cases that spin up an isolated pueued
under a tempdir-scoped config — your real queue is never touched — plus a
bash exit-code contract test. Both auto-skip if pueue / pueued aren't
on PATH. lint-skill --strict is clean (0 errors, 0 warnings).
uv run --extra dev pytest skills/local/pueue-job-queue/tests/ -q
bash skills/local/pueue-job-queue/tests/test_contracts.sh
bash skills/local/skill-author/scripts/lint-skill.sh skills/local/pueue-job-queue --strict
The pytest fixture uses pueue reset --force between tests because plain
pueue kill --all also pauses every group — a non-obvious failure mode
worth documenting (see SKILL.md gotchas).
Remote daemons: yes, but submit over SSH¶
Pueue can't schedule across hosts, but a local client can drive a single
remote pueued — with full control (add, kill, log, follow), not just
status. references/remote-daemon.md covers the setup; the short version is
that the recommended path changes nothing on the server:
# forward the daemon's own unix socket — no server reconfiguration, no TLS
ssh -f -N -L ~/.config/pueue/remote/remote.sock:/run/user/1000/pueue_you.socket myhost
pueue -c ~/.config/pueue/remote/client.yml status
Verified end to end with a macOS client (4.0.4) against a Linux daemon (4.0.2). Findings worth knowing before you try it:
read_local_logs: falseis mandatory on the client, orlog/followlook for the daemon's logs on your own disk and find nothing.- A version mismatch warns but works —
Different protocol version detected '0.30.1'on every command, then normal behaviour. Filter it from stderr; stdout JSON stays clean. - Submitting is the part that doesn't travel.
pueue addrecords the working directory and canonicalizes it on the client, so a remote submit fails three ways: the local cwd gets sent and the task endsFailedToSpawn; a remote-only path is rejected outright (Failed to canonicalize given working directory path); and on macOS/tmpis silently rewritten to/private/tmp. Only a path that exists and resolves identically on both machines works.
So the honest split is ssh host 'pueue add ...' to submit, forwarded client
to read and control.
That investigation also surfaced a real bug in this skill, now fixed: pueue's
result is a tagged enum and FailedToSpawn is dict-shaped
({"FailedToSpawn": "<os error>"}), but wait.py matched against an
enumerated list of bad variants — so it fell through and wait.py returned
exit 0 for a task that never started. It now allowlists Success and fails
closed on anything else, which also makes it safe against future pueue
variants. See
the pitfall.