---
name: Background Watch Hook
slug: background-watch-hook
category: Automation
description: Background Watch Hook uses `vibe watch` to run a managed waiter that brings the agent back to the same conversation later. Use it for reviews, CI, files, logs, and other wait-now-continue-later workflows.
github: "https://github.com/avibe-bot/avibe/tree/master/skills/background-watch-hook"
language: Python
stars: 494
forks: 76
install: "npx degit https://github.com/avibe-bot/avibe/tree/master/skills/background-watch-hook ~/.claude/skills/background-watch-hook"
installs_to: ~/.claude/skills/background-watch-hook
source_path: skills/background-watch-hook/SKILL.md
collection_size: 5
category_size: 1523
collection_url: "https://dirskills.com/collections/avibe-bot/avibe"
added: 2026-08-26T05:12:57.154Z
last_synced: 2026-08-26T05:12:57.154Z
canonical_url: "https://dirskills.com/skills/background-watch-hook"
---

# Background Watch Hook

Background Watch Hook uses `vibe watch` to run a managed waiter that brings the agent back to the same conversation later. Use it for reviews, CI, files, logs, and other wait-now-continue-later workflows.

**Install:**

```bash
npx degit https://github.com/avibe-bot/avibe/tree/master/skills/background-watch-hook ~/.claude/skills/background-watch-hook
```

## README

# Background Watch Hook

Use this skill when the job is "wait now, continue later in the same conversation".

What it gives the agent:

- a managed background task instead of manual polling
- a clean way to come back to the same channel or thread later
- a reusable pattern that works for reviews, CI, files, logs, and process completion

Good trigger scenarios:

- PR reviews or comments may arrive later
- CI, deployments, or exports need time to finish
- a file, log line, or process exit should wake the agent up later

Prefer `vibe watch` when the wait should be inspectable, pausable, resumable, or removable later.

## Main Tools

- `vibe watch add`
  Main entrypoint. Starts a managed background watch and creates a follow-up Agent Run after the waiter succeeds or reaches a terminal failure.
- `vibe watch list`, `vibe watch show`, `vibe watch update`, `vibe watch pause`, `vibe watch resume`, `vibe watch remove`
  Use these to inspect and manage the watch after creation.
- `scripts/wait_pr.py`
  Bundled GitHub waiter for PR activity, with optional exact-head Actions monitoring.

## Use `vibe watch` First

Use `vibe watch add` first. Most tasks only need:

1. a short action-oriented message
2. a blocking waiter command

Generic shape:

```bash
vibe watch add \
  --message "<what the next Agent Run should do>" \
  --name "<optional label>" \
  -- \
  <waiter command ...>
```

Default behavior:

- returns immediately
- keeps the waiter managed by Avibe
- lets the agent inspect or stop the watch later
- creates a follow-up Agent Run after the waiter succeeds or reaches a terminal failure

Use `--forever` when the same waiter should re-arm after each detected event instead of exiting after one follow-up.

## `vibe watch` Parameters To Remember

- `--message`: the instruction template for the follow-up Agent Run created from waiter output
- `--name`: optional label for later management
- `--session-id`: only when the follow-up should continue a different explicit Agent Session
- `--create-session --same-scope`: create a visible sibling Session for the follow-up instead of continuing this conversation
- `--create-session --scope-id <scopes.id>`: create the follow-up Session in a specific existing scope
- `--forever`: re-arm after each detected event
- `--timeout`: per-cycle timeout
- `--lifetime-timeout`: whole-watch lifetime cap, mainly for forever watches

Management commands:

- `vibe watch list`
- `vibe watch show <watch-id>`
- `vibe watch update <watch-id> --name '...'`
- `vibe watch pause <watch-id>`
- `vibe watch resume <watch-id>`
- `vibe watch remove <watch-id>` hides the watch while keeping prior run history

## Waiter Contract

Write waiters to follow this contract:

- `exit 0`: event detected; final summary printed to `stdout`
- `exit 64` **plus the line `avibe-watch: no-event` on `stderr`**: cycle completed with nothing worth reporting; **no follow-up Agent Run**, the watch ends (`once`) or re-arms (`--forever`)
- `exit 124`: timeout; still send a timeout follow-up
- other non-zero: failure; the watch stops and sends a failure follow-up

Exit 64 is the token-saving path. Every other terminal exit costs one Agent turn,
so a waiter whose normal outcome is uninteresting — green CI, review chatter that
was filtered out — should end on 64 rather than reporting "nothing to do". It is a
clean ending, so a `once` watch that retires on 64 reads as completed rather than
failed, and whatever the waiter wrote to `stderr` is logged beside the watch id.

The marker is not optional. 64 is also BSD `sysexits` EX_USAGE, so a watched command
that rejects its own arguments exits with it — and it must keep failing loudly rather
than being read as a quiet cycle and, in `--forever`, rerun indefinitely. A bare 64
is therefore treated as a failure; only 64 with the marker is a no-event cycle. In
the bundled waiters, `_github_wait_common.no_event("<summary>")` prints the summary
and the marker to `stderr` and returns the code, so `return no_event(...)` is the
only place the contract has to be spelled out.

Keep the output split clean:

- `stdout`: final summary for the next turn
- `stderr`: polling logs and diagnostics

## Generic Examples

Delay:

```bash
vibe watch add \
  --name "Delay follow-up" \
  --message "The delayed check completed. Continue from the result below." \
  -- \
  bash -lc 'sleep 120; echo "Timer finished after 120 seconds."'
```

File appears:

```bash
vibe watch add \
  --name "Wait for export file" \
  --message "The export file is ready. Inspect it and continue." \
  -- \
  bash -lc 'while [ ! -f /tmp/export.json ]; do sleep 10; done; echo "Detected /tmp/export.json"'
```

Log match:

```bash
vibe watch add \
  --name "Watch app log" \
  --message "The expected log pattern appeared. Inspect the event and continue." \
  --forever \
  -- \
  bash -lc 'tail -Fn0 /tmp/app.log | while read -r line; do case "$line" in *READY*) echo "$line"; break;; esac; done'
```

## Session Targeting

Use the current Avibe context:

- Inside an Avibe-injected Agent shell, omitting the target continues this conversation.
- Use `--session-id <id>` only when the follow-up should continue a different existing Agent Session.
- Use `--create-session --same-scope` when follow-ups should run in one visible sibling Session under the same Workbench project or IM scope.
- For `--forever` watches that need a separate visible Session for each event, use `--create-session-per-run --same-scope`.
- Use `--create-session --scope-id <scopes.id>` when follow-ups should run in one Session under a specific existing scope.
- For separate visible Sessions in a specific existing scope, use `--create-session-per-run --scope-id <scopes.id>`.
- If `--cwd` is omitted while creating a Session, Avibe uses the command's current working directory.

## Timeout And Lifecycle

For `vibe watch add`:

- `--timeout` is the waiter timeout for one cycle
- default is `21600` seconds
- `0` means no per-cycle timeout
- `--forever` means re-arm after each detected event
- an allowed `--retry-exit-code` keeps either mode waiting; a once Watch stops after its first event
- `--lifetime-timeout` limits the whole long-running watch; default is `0` meaning run until killed

This separation matters: a once Watch may now have several retry cycles, and a forever
Watch can still use a bounded timeout for each cycle.

Exit `0` means one new reportable event, never merely that a condition remains true.
For a persistent level, return an allowed retry code (default `75`) until a new edge is
observed. Exit `64` plus `avibe-watch: no-event` on stderr is a completed cycle with
nothing worth reporting. A forever waiter must keep a durable cursor, state transition,
or domain cooldown so it cannot emit the same level repeatedly.

Avibe admits only one queued/running follow-up per Watch. A forever Watch re-arms after
that Agent Run settles and a five-second safety delay. If the waiter still produces six
successful events within 60 seconds, Avibe pauses the Watch and sends the target Agent
one repair message containing the bounded latest waiter output. The Agent should inspect
and fix the waiter, and resume only after verifying an unambiguous, reversible fix.

## Bundled Waiter Example

This skill ships bundled GitHub waiters:

- `scripts/wait_pr.py`
  Waits for GitHub PR review activity, including reviews, inline review comments, PR conversation comments, PR status transitions such as `draft -> open`, `open -> merged`, or `open -> closed`, and the special Codex `+1` reaction on the PR body. It can also wait for newly opened PRs in a repository.
  When one or more `--workflow` values are provided for a specific PR, the same
  waiter also watches every matching Actions run at the PR's current head and
  optional branch. Add `--sha` only to pin a one-shot wait to one exact head.
- `scripts/wait_issue.py`
  Waits for GitHub issue activity, either newly opened issues in a repository or new comments on a single issue.
- `scripts/wait_action.py`
  Waits for selected GitHub Actions workflow runs on a specific commit SHA to finish
  when there is no PR activity stream to combine with them. Workflow failures are
  reported as an event so the follow-up turn can inspect and handle them.

Use bundled waiters as examples or as ready-to-run building blocks. The main skill is still `vibe watch`; the waiter is only the thing that blocks until the condition is met.
When running a bundled script through `uv`, prefer `uv run --no-project ...` so the script does not accidentally attach itself to an unrelated parent project.
Bundled GitHub waiters classify temporary network failures and GitHub
`408/429/5xx` responses as retryable. The one-shot PR and Actions waiters retry
those failures inside the same process so the managed watch stays alive. A
cycle-oriented waiter may use exit code `75` only when its supervisor explicitly
opts into retrying that code. Exit code `64` with the
`avibe-watch: no-event` marker means a cycle finished with nothing worth an Agent
turn.

Run bundled waiters relative to the directory containing this loaded `SKILL.md`.
The examples below use `BACKGROUND_WATCH_HOOK_DIR` for that directory:

```bash
BACKGROUND_WATCH_HOOK_DIR="<directory containing the loaded SKILL.md>"
```

## GitHub Example Waiter

For a PR delivery loop, prefer one durable combined `wait_pr.py` watch. It observes
PR review, comment, reaction, thread, lifecycle, and head-change events together
with selected Actions workflows for the PR's current head. Use `--forever` and one
state file for the whole loop: when a push changes the head, the next cycle fetches
Actions for the new exact SHA without replacing the Watch or rebuilding its PR
baseline. Use `wait_pr.py` without CI arguments for PR-only monitoring. Use
`wait_action.py` only for an Actions wait that is not attached to a PR.

### Preferred PR + CI watch

At least one repeatable `--workflow` enables combined CI monitoring. Omit `--sha`
for the normal delivery loop: each cycle resolves the PR's current head and queries
Actions for that exact SHA. `--branch`, `--max-pages`, and `--success-conclusion`
remain optional. Add `--sha` only when the caller intentionally wants a fixed-head
one-shot wait. The waiter stays quiet while a requested run is missing or still
running, then reports the complete exact-head result when every requested workflow
has terminal runs. Every distinct matching run ID is included, so an earlier failed
rerun remains visible to the follow-up turn. Set the forever Watch's `--timeout 0`:
the default 21600-second per-cycle timeout treats six quiet hours as a terminal
failure, which is not a meaningful end condition for a PR delivery loop.

```bash
STATE_FILE="$HOME/.avibe/state/watch-cursors/pr-151-review.json"
BRANCH="$(gh pr view 151 --repo avibe-bot/avibe --json headRefName --jq .headRefName)"

uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
  --repo avibe-bot/avibe --pr 151 \
  --branch "$BRANCH" \
  --workflow lint \
  --actionable-only \
  --state-file "$STATE_FILE" --seed-state

vibe watch add \
  --name "Watch PR 151 review and CI" \
  --forever \
  --timeout 0 \
  --message "PR #151 has new review activity, a head change, or current-head CI activity. Fetch the latest PR and Actions state, resolve actionable findings, and leave this durable combined Watch armed until close-out. Summarise the round here in one or two lines; do not post that summary as a PR comment." \
  -- \
  uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
    --repo avibe-bot/avibe --pr 151 \
    --branch "$BRANCH" \
    --workflow lint \
    --actionable-only --settle 20 --state-file "$STATE_FILE" --interval 60 \
    --timeout 0

# After the seeded Watch is confirmed live, push or post the review trigger.
# Keep this same state file and Watch for every later head and review round.
```

Do not combine `--new-prs` with CI arguments. A new PR has no stable exact-head
gate until the follow-up turn resolves its head and workflow set.

One-shot watch:

```bash
STATE_FILE="$HOME/.avibe/state/watch-cursors/pr-151-review.json"
uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
  --repo avibe-bot/avibe --pr 151 --state-file "$STATE_FILE" --seed-state

# Push or post the review trigger only after the baseline is durable.
vibe watch add \
  --name "Watch PR 151 reviews" \
  --message "PR #151 has new review activity. Fetch the latest review state and resolve the actionable findings on the PR. Then summarise the round here in one or two lines -- which findings you resolved and what changed -- and do not post that summary as a PR comment. Save a longer message for the review passing, the loop being blocked, or a decision that needs the user." \
  -- \
  uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
    --repo avibe-bot/avibe \
    --pr 151 \
    --actionable-only \
    --settle 20 \
    --state-file "$STATE_FILE" \
    --interval 60
```

Before the first watched push or review trigger in the delivery loop, seed an
owner-specific state file from the current complete PR snapshot. Arm the forever
Watch with that exact file and confirm it is live before taking the watched action.
Set `--timeout 0` on both sides of the `--` command separator: the first disables
the Watch supervisor's per-cycle deadline, while the second disables the bundled
waiter's own default six-hour deadline. GitHub request timeouts remain bounded inside
the waiter. A nonzero timeout at either layer is for a Watch whose lack of an event
by that deadline is itself reportable.
After the Watch starts, never reseed or replace its state between rounds: a later
event may already exist, and turning it into the new baseline silently drops it.
The forever Watch promotes each delivered batch and compares the next cycle against
that durable pre-event snapshot, including activity that landed while the Agent
follow-up was running.

Use `--catch-up` only when deliberately processing historical activity. It is
not a substitute for the pre-action baseline in a review loop.

Prefer `--actionable-only` for review loops. Without it the waiter wakes the Agent
for every comment on the PR, including the `@codex review` triggers the loop itself
posts and the bodyless `COMMENTED` review envelope GitHub wraps around inline
comments. With it the waiter still reports inline review comments, reviews carrying
a verdict or a body, the Codex pass reaction, and merged/closed transitions — which
is everything the review loop needs to make progress or close out.

Narrow it further with `--ignore-author <login>` and `--ignore-comment-pattern <regex>`
(both repeatable). Filtered items still advance the cursors, so they are examined
once and never re-reported. These filters suppress review/comment payloads, not
review-thread status. A thread becoming unresolved or resolved remains an
independent wake signal because thread state is a separate mutable resource and
may later be changed by a different actor.

`--settle <seconds>` is worth setting on any review loop. A bot review arrives as a
burst of inline comments plus an envelope, so the poll that happens to catch the
first fragment would otherwise report it alone and the rest would arrive as a second
event. With `--settle` the waiter re-polls until the set stops growing (at most three
extra polls) and reports the whole batch as one event, which is one Agent turn
instead of several. 20 seconds is a reasonable starting point. The window never runs
past `--timeout`: settling is skipped unless both the wait and a full re-poll fit in
what is left of the deadline, so a batch already worth a turn is reported rather than
lost to the timeout kill. Keep `--settle` well under `--timeout`.

Catch up on existing activity first:

```bash
STATE_FILE="$HOME/.avibe/state/watch-cursors/pr-151-catch-up.json"
vibe watch add \
  --name "Catch up PR 151 reviews" \
  --message "PR #151 already has review activity. Fetch the latest review state and resolve the actionable findings on the PR. Then summarise the round here in one or two lines -- which findings you resolved and what changed -- and do not post that summary as a PR comment. Save a longer message for the review passing, the loop being blocked, or a decision that needs the user." \
  -- \
  uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
    --repo avibe-bot/avibe \
    --pr 151 \
    --state-file "$STATE_FILE" \
    --catch-up
```

Stay armed for future activity:

```bash
STATE_FILE="$HOME/.avibe/state/watch-cursors/pr-151-forever.json"
uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
  --repo avibe-bot/avibe --pr 151 --state-file "$STATE_FILE" --seed-state

vibe watch add \
  --name "Monitor PR 151 reviews" \
  --forever \
  --timeout 0 \
  --lifetime-timeout 0 \
  --message "PR #151 has new review activity. Fetch the latest review state and resolve the actionable findings on the PR. Then summarise the round here in one or two lines -- which findings you resolved and what changed -- and do not post that summary as a PR comment. Save a longer message for the review passing, the loop being blocked, or a decision that needs the user." \
  -- \
  uv run --no-project "$BACKGROUND_WATCH_HOOK_DIR/scripts/wait_pr.py" \
    --repo avibe-bot/avibe \
    --pr 151 \
    --actionable-only \
    --settle 20 \
    --state-file "$STATE_FILE" \
    --interval 60 \
    --timeout 0
```

Always pass `--state-file` to a `--forever` watch. Each cycle is a fresh waiter
process, so without it the next cycle re-snapshots the PR as its baseline and
anything that arrived between the previous cycle's exit and that snapshot is lost.
The file carries the resolved GitHub login and the complete mutable PR baseline
forward. PR cycles reread complete review/comment/thread collections because
edits, deletions, and thread-resolution changes are wake-worthy state.
For an ordinary wait, that normalized complete snapshot is the single wake/no-wake
decision. Numeric cursors, fingerprints, and the thread map only describe a detected
change; they cannot wake independently. `--catch-up` and explicit `--since-*-id`
flags are the deliberate replay modes and therefore remain cursor-driven. A legacy
state file with cursors but no complete snapshot is rejected until it is deliberately
caught up or reseeded, rather than silently absorbing mutable changes.
The login is reused only while the token still fingerprints to the account it was
resolved for. Explicit cursor flags still request a replay from that cursor, while
the complete PR collections remain the source of truth for edits and removals. The
state also records the last observed PR head, review/comment fingerprints, and every
review-thread resolution state, so a pushed head, edited object, deletion, or thread
transition is activity even when no new numeric ID exists. Cursors that cover a reported event are not committed by the
cycle that reports it. A waiter cannot observe its own delivery — `vibe watch` reads
its stdout only after the process exits — so those cursors are staged under `pending`
while the committed ones stay before the event, together with the rendered report
and the value of
`AVIBE_WATCH_LAST_DELIVERY` the cycle started from. That variable is when this watch
last had a report durably queued, stamped in the same transaction as the follow-up, so
any later cycle that reads a *different* value knows the report was delivered and
promotes the staged cursors. An unchanged value means it may never have been queued,
so they are dropped and the event is reported again: at-least-once, costing one
repeated Agent turn instead of losing the activity for good. Comparing a durable stamp
rather than consuming a one-shot acknowledgement is what makes this correct across a
service restart, and for a `once` watch, whose one report is followed by no cycle at
all until the user resumes it. A manual run has no supervisor, and there printing *is*
