Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when launching anything that runs longer than a minute, anything that streams logs, anything you want to keep running while doing other work — or when terminal_exec auto-backgrounded on you and returned a job_id. Teaches the start→poll→wait pattern with terminal_job_logs offset bookkeeping, the `wait_until_exit=True` blocking-poll idiom, the truncated_bytes_dropped resumption signal, the merge_stderr decision, the SIGINT→SIGTERM→SIGKILL escalation ladder via terminal_job_manage, and the hard
.claude/skills/aden-hive-hive-terminal-tools-job-control/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | -8% | 0% |
| case-09 | ✗→✓ | ▲ Improved | -28% | 0% |
| case-05 | ✗→✓ | ▲ Improved | -9% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 13% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 49% | 0% |
Background jobs are how you do things that take time without blocking your conversation. Three tools cover the surface: terminal_job_start, terminal_job_logs, terminal_job_manage.
terminal_exec (you have a job_id; pivot to this skill's idioms)For one-shot work expected to finish quickly, terminal_exec is simpler. The auto-promotion mechanic in terminal_exec is your safety net — start with terminal_exec, take over with this skill if needed.
terminal_job_start(command, ...)
→ { job_id, pid, started_at }
terminal_job_logs(job_id, since_offset=0, max_bytes=64000)
→ { data, offset, next_offset, status: "running"|"exited", exit_code, ... }
# Repeat with since_offset = previous next_offset until status == "exited"
# Or block once with wait_until_exit=True:
terminal_job_logs(job_id, since_offset=N, wait_until_exit=True, wait_timeout_sec=60)
→ blocks server-side until exit or timeoutAfter exit, the job is retained for inspection (terminal_job_manage(action="list")) until evicted by FIFO (50 most recent exits kept).
The job's output lives in a 4 MB ring buffer per stream. Each call to terminal_job_logs returns:
data — bytes between since_offset and next_offsetnext_offset — pass this as since_offset on your next calltruncated_bytes_dropped — non-zero when your since_offset was older than the ring's floor (you fell behind)Always carry next_offset forward. Don't replay from 0 — that's an offset reset, you'll see the same data twice and miss the part that fell off.
When truncated_bytes_dropped > 0, the buffer evicted N bytes between your last call and now. Treat it as a signal that the job is producing output faster than you're consuming. Either poll more often or accept the gap and read from next_offset going forward.
merge_stderr=False → two streams, request "stdout" or "stderr" by name
merge_stderr=True → one stream ("merged"), order preservedPick merge_stderr=True when:
Pick merge_stderr=False when:
terminal_job_manage(action="signal_int", job_id=...) # graceful (Ctrl-C-equivalent)
terminal_job_manage(action="signal_term", job_id=...) # polite kill (SIGTERM)
terminal_job_manage(action="signal_kill", job_id=...) # forced kill (SIGKILL, uncatchable)The idiom: signal_int → wait 2-5s → signal_term → wait 2-5s → signal_kill. Most well-behaved processes handle SIGINT (graceful) and SIGTERM (cleanup, then exit). SIGKILL bypasses cleanup — use only when the process is truly unresponsive.
After signaling, check exit with terminal_job_logs(job_id, wait_until_exit=True, wait_timeout_sec=2).
terminal_job_manage(action="stdin", job_id=..., data="some input\n")
terminal_job_manage(action="close_stdin", job_id=...)For tools that read stdin to EOF, close_stdin after writing flushes them. For interactive tools that read line-by-line, just write each line.
When terminal_exec returned auto_backgrounded: true, job_id: <X>, the process is already in the JobManager with its output flowing into the ring buffer. Your transition is seamless:
# Already saw the start of output in terminal_exec's stdout/stderr.
# Pick up reading where the env left off — use the byte count of the
# initial stdout as your since_offset, OR just request tail output:
terminal_job_logs(job_id="job_xxx", tail=True, max_bytes=64000)Or block until exit and grab everything:
terminal_job_logs(job_id="job_xxx", since_offset=0, wait_until_exit=True, wait_timeout_sec=120)nohup + terminal_exec to detach into the system's process tree and track the PID yourself.TERMINAL_TOOLS_MAX_JOBS, default 32). Past the cap, terminal_job_start returns an error. Wait for jobs to exit or kill old ones.See references/signals.md for the full signal catalog.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 11,000 | 17,571 | +60% | 1 | 1 | 0% | 1,553 | 2,009 | +29% | 0 | 0 | — |
case-02 | fail→fail | 10,648 | 5,015 | -53% | 1 | 1 | 0% | 1,836 | 1,645 | -10% | 0 | 0 | — |
case-03 | fail→fail | 10,592 | 4,556 | -57% | 1 | 1 | 0% | 281 | 1,664 | +492% | 0 | 0 | — |
case-04 | fail→pass | 9,869 | 1,989 | -80% | 1 | 1 | 0% | 1,796 | 1,659 | -8% | 0 | 0 | — |
case-09 | fail→pass | 14,768 | 2,585 | -82% | 1 | 1 | 0% | 2,591 | 1,865 | -28% | 0 | 0 | — |
case-05 | fail→pass | 12,489 | 3,603 | -71% | 1 | 1 | 0% | 2,188 | 1,985 | -9% | 0 | 0 | — |
case-06 | pass→pass | 3,919 | 2,113 | -46% | 1 | 1 | 0% | 672 | 1,696 | +152% | 0 | 0 | — |
case-07 | fail→pass | 10,028 | 4,650 | -54% | 1 | 1 | 0% | 1,895 | 2,148 | +13% | 0 | 0 | — |
case-08 | pass→pass | 4,134 | 2,301 | -44% | 1 | 1 | 0% | 750 | 1,765 | +135% | 0 | 0 | — |
case-10 | fail→pass | 6,636 | 1,669 | -75% | 1 | 1 | 0% | 1,070 | 1,595 | +49% | 0 | 0 | — |
case-11 | pass→pass | 9,836 | 3,628 | -63% | 1 | 1 | 0% | 1,566 | 2,009 | +28% | 0 | 0 | — |
case-12 | fail→pass | 9,922 | 1,620 | -84% | 1 | 1 | 0% | 1,794 | 1,574 | -12% | 0 | 0 | — |
case-13 | fail→pass | 9,578 | 1,405 | -85% | 1 | 1 | 0% | 1,606 | 1,558 | -3% | 0 | 0 | — |
case-14 | fail→pass | 7,446 | 2,211 | -70% | 1 | 1 | 0% | 1,305 | 1,784 | +37% | 0 | 0 | — |
case-15 | fail→pass | 10,281 | 1,654 | -84% | 1 | 1 | 0% | 1,833 | 1,641 | -10% | 0 | 0 | — |
case-16 | pass→pass | 5,456 | 1,874 | -66% | 1 | 1 | 0% | 970 | 1,647 | +70% | 0 | 0 | — |
case-17 | fail→pass | 4,342 | 2,499 | -42% | 1 | 1 | 0% | 783 | 1,747 | +123% | 0 | 0 | — |
case-18 | fail→pass | 49,096 | 1,442 | -97% | 1 | 1 | 0% | 3,369 | 1,535 | -54% | 0 | 0 | — |
case-19 | pass→pass | 13,148 | 3,535 | -73% | 1 | 1 | 0% | 2,440 | 1,959 | -20% | 0 | 0 | — |
case-20 | pass→pass | 3,209 | 2,491 | -22% | 1 | 1 | 0% | 480 | 1,777 | +270% | 0 | 0 | — |
case-21 | pass→pass | 12,602 | 5,152 | -59% | 1 | 1 | 0% | 2,227 | 2,283 | +3% | 0 | 0 | — |
case-22 | pass→pass | 8,206 | 3,457 | -58% | 1 | 1 | 0% | 1,459 | 1,957 | +34% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +50 percentage points is the difference between those two pass rates over the 19 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.