long-running-jobs¶
Decide how an agent should wait for work that outlives a single turn — training runs, Slurm jobs, sweeps, long builds. The skill exists because the obvious approach is the expensive one.
The problem it solves¶
An agent babysitting an 8-hour training run tends to arm a scheduled check-in:
wake at 16:31, run squeue, see "still epoch 21", schedule the next wake-up.
It works, and it is the worst option available.
Polling is not the problem. Polling with the model inside the loop is.
Both poll. In a session carrying 400k+ tokens, only one of them costs a context window every 60 seconds. The skill's whole thesis is move the timer out of the context window — into the scheduler, a blocked shell, or onto disk.
The ladder¶
Pick the lowest-numbered tier that applies.
| Tier | Mechanism | Use when |
|---|---|---|
| 0 | Scheduler owns the chain (sbatch --dependency=afterok, pueue --after) |
There is a next step. Survives session death, compaction, laptop sleep. |
| 1 | One blocking wait, backgrounded (sbatch --wait, run-and-mark.sh) |
You must react at completion. Zero in-context polls. |
| 2 | Stream shell-filtered events | You must react mid-run — OOM, early stopping, a threshold. |
| 3 | Scheduled check-in | Last resort: a remote system you hold no handle to. |
Tier 0 is the one people skip. It requires knowing the next command before the first finishes — and when you do, the agent is removed from the loop entirely.
The invariant¶
Orthogonal to all four tiers: whatever waits, something must survive not waiting.
Temp-file-then-mv is a rename(2) within one filesystem, so a reader sees
either no marker or a complete one — never a half-written exit code read as
success.
The reading rule that falls out of it:
| Observed | Means |
|---|---|
marker, 0 |
succeeded |
| marker, non-zero | failed |
| no marker, never started | not started |
| no marker, but started | unknown — killed, node died, or still running |
Absence is never success and never failure. Treating "unknown" as "not done, so re-run" is how you end up with two copies of an eight-hour job.
| Surface | Question it answers |
|---|---|
run-and-mark.sh |
"Run this, block, and leave a record that outlives my session." |
check-runs.sh |
"What finished while I was away?" |
references/claude-code-mechanisms.md |
"Which harness tool implements each tier?" |
references/scheduler-chaining.md |
"How do I wire Phase A → Phase B so nothing has to stay awake?" |
references/completion-contracts.md |
"How do I record completion so it survives the queue and the session?" |
assets/chained.sbatch.template |
"Give me the dependency plumbing already correct." |
Two facts worth the skill's existence¶
Backgrounded shell commands wake the agent on exit. Verified by experiment against Claude Code 2.1.220, and stated in the harness's own guidance:
"Use the Monitor tool to stream events from a background process (each stdout line is a notification). For one-shot \"wait until done,\" use Bash with
run_in_backgroundinstead."
This makes Tier 1 a single call, not a loop. Public documentation summaries claiming the agent must poll for completion are stale — the skill says so explicitly, because acting on the stale version reintroduces the polling the skill is trying to remove.
A failed Slurm parent leaves its child pending forever. From sbatch(1):
"By default the job stays pending with reason DependencyNeverSatisfied"
"Once a job dependency fails due to the termination state of a preceding job, the dependent job will never be run, even if the preceding job is requeued"
So Phase B does not fail when Phase A does — it parks indefinitely, looking in
squeue exactly like a job waiting its turn, and requeueing Phase A does not
release it. Every chain in the skill therefore carries
--kill-on-invalid-dep=yes and --mail-type=INVALID_DEPEND. This one has its
own pitfall page.
When the skill triggers¶
- A job "takes hours" / "runs overnight" / "will finish tomorrow morning".
- "Check back when it's done", "wait for training to finish", "run B after A".
- You are about to arm a recurring check-in, or repeat a
squeue/nvidia-smi/ls checkpoints/poll to see whether something finished. That impulse is the trigger.
When it doesn't¶
- The command finishes in seconds — just run it.
- Metrics, curves, parameter tracking →
mlflow-tracking. - Recording what the run meant →
experiment-knowledge-harness. - Resource requests and GPU isolation →
slurm-hpc.
Structure¶
skills/local/long-running-jobs/
├── SKILL.md # the ladder, the invariant, 8 gotchas
├── scripts/
│ ├── run-and-mark.sh # bash 3.2; own the child, block, atomic marker
│ └── check-runs.sh # bash 3.2; marker reader, exit 0/3/4
├── references/
│ ├── claude-code-mechanisms.md # which tool per tier; blocked foreground sleep; cron limits
│ ├── scheduler-chaining.md # Slurm dependency matrix + traps; pueue; DVC
│ └── completion-contracts.md # atomic markers; sacct states; wait-primitive portability
└── assets/
└── chained.sbatch.template # Phase A/B chain, correct by construction
Own the process, or own a marker¶
The portability table in completion-contracts.md drives one design decision
worth calling out:
| Primitive | Works on |
|---|---|
wait "$PID" |
everywhere — children of the current shell only |
tail --pid=PID |
GNU coreutils only; BSD/macOS tail rejects it |
pidwait |
Linux (procps-ng) |
inotifywait |
Linux; races if the marker predates the watch |
flock / fswatch |
not on stock macOS |
There is no portable way to wait on a foreign PID. So run-and-mark.sh runs
its command as its own child, which makes plain wait — the one primitive
available everywhere — sufficient.
Verification¶
Every claim in the skill was tested rather than assumed:
bash skills/local/skill-author/scripts/lint-skill.sh --strict skills/local/long-running-jobs
# exit-code contract, under stock macOS bash 3.2
/bin/bash scripts/run-and-mark.sh --marker-dir .r --name ok -- /bin/sh -c 'exit 0' # 0
/bin/bash scripts/run-and-mark.sh --marker-dir .r --name oom -- /bin/sh -c 'kill -9 $$' # 137
/bin/bash scripts/check-runs.sh --marker-dir .r --json | python3 -m json.tool
- Backgrounding
run-and-mark.shfires a real completion notification carrying the command's exit code — the Tier 1 mechanism, proven end to end. SIGKILLis preserved as 137, so an OOM stays distinguishable fromexit 1— whichsbatch --waitalone cannot do (it collapses every signal death to1).- A stubbed
sbatchemitting999;clusyields--dependency=afterok:999, confirming the${JID%%;*}cluster-suffix strip. check-runs.shreturns4for unknown ahead of3for failed — unknown is the state that needs a human, so it outranks.