| name | mr-watcher |
| description | Activate when the user says "watch MR X until it lands", "babysit MR Y", "keep MR Z healthy", "keep an eye on this MR until it merges", or asks to run a long-running shepherd that survives across sessions. Complements `mr-shepherd` (single-pass) and `mr-submit` (end-to-end one-shot): mr-watcher is the persistent tail state that polls until GitLab marks the MR `merged` or `closed`, dispatching per-cycle shepherd work as new threads/CI events land. |
MR Watcher Workflow
Persistent, checkpoint-resumable coverage of a single GitLab MR. Runs in-process with a configurable sleep between polls. Exits only when GitLab itself marks the MR merged or closed — no inactivity timeout, no auto-exit on red CI.
When to Use
Use mr-watcher when:
- The user wants an MR baby-sat from submission through merge.
- CI is flaky and needs persistent retry-on-infra-failure coverage.
- The MR is expected to collect new reviewer comments over time, and the operator wants them addressed without manual re-invocation of shepherd.
- A long-running poll survives across shell sessions — the checkpoint makes resume deterministic.
Do NOT use mr-watcher when:
- You want a single-pass review + fix. Use
mr-shepherd for that.
- You want end-to-end one-shot (review → fix → push → resolve threads in one call). Use
mr-submit / mr-shepherd-loop instead.
- You want multi-MR coverage. Use
fan-out-mr-shepherd.
- The MR is not yours to modify. The watcher pushes commits and resolves threads — only dispatch when the operator has write rights.
Distinction from siblings
| Skill | Scope | Duration | Terminal | Composes |
| --------------------- | ---------------- | --------------- | ------------------ | ------------------ | ------------------ |
| mr-shepherd | One MR, one pass | Seconds–minutes | Operator exit | mr-shepherd-loop |
| mr-submit | One MR, one shot | Minutes | Push + resolve | mr-shepherd-loop |
| mr-shepherd-loop | One MR, bounded | Up to N minutes | max_poll_minutes | Primitive services |
| mr-watcher | One MR, full | Unbounded | merged | closed | mr-shepherd-loop |
| fan-out-mr-shepherd | N MRs, parallel | Bounded per-MR | Per-MR verdict | mr-shepherd-loop |
Workflow
1. Inputs
| Input | Type | Default | Purpose |
|---|
mr_id | int | required | GitLab MR IID |
poll_interval | int | 60 | Seconds between polls. Floor 60 (rate-limit guard). |
max_runtime | int? | null | Seconds. null = unlimited; stop only on merged/closed. |
resume_from_checkpoint | bool | true | On start, read prior checkpoint if present. |
terminal_states | list | ["merged","closed"] | Override the terminal-state set (rare; keep default). |
2. Checkpoint bootstrap
Checkpoint path: .claude/mr-watcher/<mr_id>/state.json
CKPT_DIR=".claude/mr-watcher/${MR_ID}"
CKPT_FILE="${CKPT_DIR}/state.json"
mkdir -p "${CKPT_DIR}"
if [ "${RESUME_FROM_CHECKPOINT}" = "true" ] && [ -f "${CKPT_FILE}" ]; then
prior_polls=$(jq -r .polls_total "${CKPT_FILE}")
prior_fixes=$(jq -r .fixes_applied "${CKPT_FILE}")
prior_retries=$(jq -r .infra_retries "${CKPT_FILE}")
else
prior_polls=0
prior_fixes=0
prior_retries=0
fi
3. Poll loop
Each cycle:
- Fetch terminal state. Call
fetch-mr (not fetch-mr-status — only fetch-mr.mr.state is the authoritative opened|merged|closed|locked source).
- If
mr.state ∈ terminal_states → write final checkpoint, emit summary, exit.
- Fetch CI + thread counts. Call
fetch-mr-status for pipeline.state and threads.unresolved.
- React by precedence (one action per cycle, stop after first match):
- New unresolved threads (
threads.unresolved > last_known_unresolved_count) → dispatch mr-shepherd-loop with max_review_iterations: 1, max_poll_minutes: 0 (single pass, no nested poll). Increment fixes_applied by its fixed_count.
- Pipeline failed with infra pattern (match against the harness CI-triage ruleset — docker-pull timeouts, network 5xx, known-flaky stages) → retry the failing job via
glab ci retry <pipeline_id>. Increment infra_retries.
- Pipeline failed with code pattern → dispatch
mr-shepherd-loop with stop_on_pipeline_fail: false so it diagnoses, fixes, and pushes. Increment fixes_applied.
- Branch behind
origin/main (detected by comparing mr.base_sha against origin/main tip) → dispatch sync-and-push (MR !27). Rebase in place, resolve conflicts, push with --force-with-lease.
- No-op → nothing needed this cycle.
- Write checkpoint (see schema below). Sleep
poll_interval. Repeat.
4. Terminal exit
On mr.state == "merged" or mr.state == "closed":
-
Write final checkpoint with final_state set.
-
Emit summary:
[MR_WATCHER_TERMINAL]
mr_id: <int>
final_state: merged | closed
polls_total: <int>
fixes_applied: <int>
infra_retries: <int>
elapsed_seconds: <int>
checkpoint_path: .claude/mr-watcher/<mr_id>/state.json
-
Return. Do NOT continue polling.
Checkpoint schema
.claude/mr-watcher/<mr_id>/state.json:
{
"mr_id": 42,
"schema_version": 1,
"started_at": "2026-05-12T14:03:11Z",
"last_poll_at": "2026-05-12T14:18:11Z",
"polls_total": 15,
"fixes_applied": 3,
"infra_retries": 1,
"elapsed_seconds": 900,
"last_known_pipeline": {
"state": "success",
"sha": "abc1234",
"web_url": "https://gitlab.../pipelines/12345"
},
"last_known_unresolved_count": 0,
"next_action_planned": "idle",
"final_state": null,
"poll_interval": 60,
"recent_events": [
{
"at": "2026-05-12T14:17:11Z",
"kind": "threads_resolved",
"count": 2,
"via": "mr-shepherd-loop"
}
]
}
Field contracts
| Field | Type | Notes |
| ----------------------------- | ------------ | ------------------------------------------------------------------------- | ------- | -------- | ------ | -------- |
| mr_id | int | Constant across restarts; mismatch → refuse to resume. |
| schema_version | int | Start at 1. Bump on breaking shape changes. |
| started_at | ISO 8601 UTC | First started_at; preserved across resumes. |
| last_poll_at | ISO 8601 UTC | Updated every cycle, including no-op cycles. |
| polls_total | int | Cumulative across restarts. |
| fixes_applied | int | Cumulative count of successful mr-shepherd-loop dispatches that pushed. |
| infra_retries | int | Cumulative count of CI retries triggered on infra patterns. |
| elapsed_seconds | int | now - started_at at last write. |
| last_known_pipeline | object | Last non-null pipeline object; null until first poll. |
| last_known_unresolved_count | int | Used to detect new threads between cycles. |
| next_action_planned | string | One of idle | threads | ci_retry | ci_fix | rebase. |
| final_state | string? | null while watching; set on terminal exit. |
| poll_interval | int | Current effective interval (floor-adjusted). |
| recent_events | array | Rolling buffer, max 20 entries. Oldest dropped on overflow. |
Resume contract
On resume:
started_at, polls_total, fixes_applied, infra_retries carry forward.
last_poll_at is overwritten on the first new cycle.
- If
mr_id in checkpoint doesn't match the invocation input, refuse to resume (mismatched MR).
- If
final_state is non-null (prior run already terminated), refuse to resume — terminal is terminal.
Composition with siblings
mr-watcher is a composer, not a reimplementer:
- Per-cycle fix work →
mr-shepherd-loop. Dispatch with max_review_iterations: 1 + max_poll_minutes: 0 so it does one review-fix-push pass and returns. mr-watcher's outer loop handles the "keep going" part.
- Rebase-when-behind →
sync-and-push (MR !27). Do not inline rebase logic here.
- Thread resolution → inherited via
mr-shepherd-loop. Its Phase 5b match-threads-to-resolved is the contract for resolving threads post-fix.
- Terminal detection →
fetch-mr. Authoritative source of mr.state.
- CI snapshot →
fetch-mr-status. Pipeline + unresolved-thread count in one call.
- Polling cadence → self.
wait-pipeline is for single-shot pipeline waits; mr-watcher owns its own poll cadence so it can interleave thread + CI + rebase checks.
Idempotence + safety
- Every dispatched service is itself idempotent (
push-changes handles no-op trees, resolve-thread on already-resolved is non-fatal, fetch-* are read-only).
- A watcher killed mid-cycle loses at most the pending action for that cycle. Resume picks up on the next poll interval.
- Two watchers on the same
mr_id race on the checkpoint file. Operator discipline: one watcher per MR. The skill does not enforce a lock — add that at the harness layer if needed.
Related
- Skill
mr-shepherd — single-pass shepherd; mr-watcher dispatches it per cycle.
- Skill
mr-status — operator dashboard for active watchers (reads the checkpoint directory).
- Service
mr-shepherd-loop — per-cycle fix primitive.
- Service
sync-and-push — rebase-when-behind primitive.
- Service
mr-watch — the ProseScript orchestrator that implements this skill's contract.