Skill Tracer
Skills › Compound Engineering › ce-babysit-pr › tick.md

tick.md

102 lines · skills/ce-babysit-pr/references/tick.md at 3449230

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# The tick: snapshot, watch, marks, ordering
2
3Read this before the first snapshot.
4

The tick: snapshot, watch, marks, ordering

Read this before the first snapshot.

Raw lines 1 to 4
5## Step 2: Run one tick
6
7A tick is fully resumable from disk, so any re-invocation drives it: a scheduler, `/loop`, or the user re-running the skill an hour later. Set `SKILL_DIR` to the directory containing this SKILL.md, then snapshot both streams in one batch:
8
9```bash
10SKILL_DIR="<absolute path of the directory containing the SKILL.md you just read>";
11SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; };
12STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>";
13(umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1;
14PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
15"$PY" "$SKILL_DIR/scripts/pr-snapshot" snapshot --pr <N> --repo <[host/]owner/repo> --state-dir "$STATE_DIR" --start-invocation --invocation-budget-seconds <seconds>
16```
18This is the only command that may start a budget. Pass the user's requested duration, or the **8-hour default** (`28800`). Record the output's `invocation_id`, `invocation_started_at`, and `invocation_budget_seconds` as `RUN_INVOCATION_ID`, `RUN_STARTED_AT`, and `RUN_BUDGET_SECONDS`. Require `invocation_elapsed_seconds <= 60`; otherwise fail before arming a watcher. Every later snapshot, watch arm, and mark must present all three recorded values; the helper rejects a missing or mismatched token, anchor, or budget. Never use `--start-invocation` after this first snapshot: re-arms, mutations, retries, review/CI rounds, and managed-stack layer transitions (which add `--continue-invocation`) share one non-rolling budget. Durable PR dispositions, dedup, and trajectory carry over to a new invocation; its budget clock does not.
20The budget is spent in **active watch time**, not raw wall-clock time. While the in-session watch runs, a span where the whole process was suspended (a closed laptop) is charged to dead time and excluded from `invocation_elapsed_seconds`. Detection is coarse: only an activity gap well above the poll interval counts as dead time, while ordinary polls, agent ticks, and human-blocked waits keep counting. A re-arm preserves accumulated dead time rather than resetting it. A separate **3-calendar-day wall-clock backstop** caps every invocation regardless of dead time; it is the ceiling for a stale PR or a zombie watch. Checkpoint mode and the durable/cron path have no continuous poll, so they count wall-clock time.
22Treat every fresh `snapshot` as the canonical source of truth for review-thread state; its bundled fetch paginates the full thread connection. Never replace it with a one-shot `reviewThreads(first:N)` result. If a direct diagnostic query is genuinely necessary, follow `pageInfo` until `hasNextPage == false` before drawing a count or unresolved-state conclusion.
24**In the self-sustaining watch, back the tick with the background change-detector.** `pr-snapshot watch` runs that same fetch→diff on an interval with **no agent tokens** and prints a single `BABYSIT_WAKE {reason,url,...}` line *only* when there is work to inspect or a stop/residual condition, then exits. The wake reasons, which reference owns each, and how to handle a stale or superseded wake are in `references/watch-loop.md`. Keep the session active while waiting for that line with the harness's tools (Step 1); arm it with:
26```bash
27SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1; RUN_INVOCATION_ID="<invocation_id>"; RUN_STARTED_AT="<invocation_started_at>"; RUN_BUDGET_SECONDS="<invocation_budget_seconds>";
28PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
29"$PY" "$SKILL_DIR/scripts/pr-snapshot" watch --pr <N> --repo <[host/]owner/repo> --state-dir "$STATE_DIR" --interval 150 --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS"
30```
32In a confirmed managed stack under `stack-ready`/`stack-land`, append `--downstack-pr <N>` for every open lower layer to that arm so the watcher probes below (`references/stack.md`).
34Do **not** pass `--settle-seconds` or `--blocked-external-drain-seconds` on the ordinary arm. The script's 300s default is the initial merge-ready settle window. Step 3 alone sets `--settle-seconds` after a rejected `merge-ready` wake, and `--blocked-external-drain-seconds` after a `blocked-external` (approval-gate) wake begins the bounded review drain.
36**Shell state does not persist between separate tool calls.** `SKILL_DIR` and `STATE_DIR` are set only for the command they appear in. The later `mark` calls (Steps 3 and 5) run as their own invocations, so re-set both inline in each of those commands, or pass the absolute paths directly. A bare `$SKILL_DIR` in a fresh call is empty and resolves to the wrong path.
38**`<host>` in `STATE_DIR` is required for GitHub Enterprise.** Derive it from the PR URL's host (or `gh repo view --json url`); on plain github.com it is `github.com`. Use the same value in every `mark`, so PRs that share `owner/repo#N` on different hosts never share one `state.json`. **Pass the same host in `--repo <host>/<owner>/<repo>`** (the documented `[HOST/]OWNER/REPO` selector): `pr-snapshot`'s first `gh pr view` runs before it parses the URL host, and without the selector it queries the checkout's default `github.com`.
40The snapshot emits the **attention set**: the items that still need action. It contains unresolved threads you have not yet acted on, **non-thread feedback candidates** (top-level PR comments + review-submission bodies) you have not yet classified, failing checks on the current head you have not yet dispatched, and the exact current `branch_currency` item with its `attention` route. `counts` totals the threads, checks, and comments; the currency item appears only in `branch_currency`, so zero counts do not mean the attention set is empty. The rest of the snapshot reports merge, check, currency, timing, decision, `pr_chain`, and `trajectory` facts; the references that act on a field state what it means.
42Whether `mergeable` was computed against the current base is decided by `base.identity` and `mergeability_certain`; a `race`, `mergeability-pending`, or `probe-error` identity disables `mergeability_certain` and re-polls (`references/watch-loop.md`, **Current-base merge identity**). Invocation time and persisted-state age are separate; never report one as the other. `pr_chain` carries two independent axes, `manager_status` (`confirmed|absent|probe-error`) and `relationship_status` (`dependent|independent|probe-error`), plus the manager source, target/upstack freshness, ordered entries, and ordinary parent/dependent PRs when available.
44The members of `actionable.comments` are candidates awaiting semantic classification, not detector-proven action items. The deterministic fetch excludes only empty bodies. A nonempty comment from the PR author stays in, since an author asking for a change on their agent-opened PR is ordinary feedback; the `dispatched` mark, not author identity, prevents loops. The fetch does **not** decide from content, bot identity, or comment-vs-review surface whether an external message is valid feedback; `ce-resolve-pr-feedback` makes that judgment. An item stays in the attention set until you confirm you acted or classified it (`mark`) or remote truth removes it (a resolved thread drops out of the fetch), so a crashed, failed, or superseded resolve pass leaves its items for the next tick. Every `mark` presents the same `RUN_INVOCATION_ID`, `RUN_STARTED_AT`, and `RUN_BUDGET_SECONDS`, so a stale resolver tick fails before it can silence work in a replacement invocation. The state schema and the claim→act→confirm protocol are in `references/watch-loop.md`; read it before acting if this session has not already loaded it.
46**The `trajectory` is facts, not a verdict. You hand it to the delegated skills (the leaves: `ce-debug` and `ce-resolve-pr-feedback`), and they judge convergence.** The triggers are: `check_recur_max >= 2`; `stream_alternations >= 3`; a rising `unresolved_trend` with `new_threads_this_tick > 0` across passes; `heads_since_progress >= 2`; or any `invariant_rounds[].rounds >= 2` (a key at 2 recorded rounds means the next fix would be its third). When the trajectory crosses one, pass it to that tick's `ce-debug`/`ce-resolve-pr-feedback` invocation as **mandatory input** **before** that leaf mutates, and let the leaf decide whether this is ordinary progress or genuine non-convergence. A leaf may then return a `needs-human` residual that parks the *whole stream*, for example an emergent CI trade-off, a wrong-approach nitpick cluster, or a third invariant round. Never declare non-convergence yourself. The trigger→route→park→re-open protocol is in `references/watch-loop.md` (**Non-convergence**); read it before acting on this if this session has not already loaded it.

Step 2: Run one tick

A tick is fully resumable from disk, so any re-invocation drives it: a scheduler, /loop, or the user re-running the skill an hour later. Set SKILL_DIR to the directory containing this SKILL.md, then snapshot both streams in one batch:

SKILL_DIR="<absolute path of the directory containing the SKILL.md you just read>";
SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; };
STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>";
(umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1;
PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
"$PY" "$SKILL_DIR/scripts/pr-snapshot" snapshot --pr <N> --repo <[host/]owner/repo> --state-dir "$STATE_DIR" --start-invocation --invocation-budget-seconds <seconds>

This is the only command that may start a budget. Pass the user's requested duration, or the 8-hour default (28800). Record the output's invocation_id, invocation_started_at, and invocation_budget_seconds as RUN_INVOCATION_ID, RUN_STARTED_AT, and RUN_BUDGET_SECONDS. Require invocation_elapsed_seconds <= 60; otherwise fail before arming a watcher. Every later snapshot, watch arm, and mark must present all three recorded values; the helper rejects a missing or mismatched token, anchor, or budget. Never use --start-invocation after this first snapshot: re-arms, mutations, retries, review/CI rounds, and managed-stack layer transitions (which add --continue-invocation) share one non-rolling budget. Durable PR dispositions, dedup, and trajectory carry over to a new invocation; its budget clock does not.

The budget is spent in active watch time, not raw wall-clock time. While the in-session watch runs, a span where the whole process was suspended (a closed laptop) is charged to dead time and excluded from invocation_elapsed_seconds. Detection is coarse: only an activity gap well above the poll interval counts as dead time, while ordinary polls, agent ticks, and human-blocked waits keep counting. A re-arm preserves accumulated dead time rather than resetting it. A separate 3-calendar-day wall-clock backstop caps every invocation regardless of dead time; it is the ceiling for a stale PR or a zombie watch. Checkpoint mode and the durable/cron path have no continuous poll, so they count wall-clock time.

Treat every fresh snapshot as the canonical source of truth for review-thread state; its bundled fetch paginates the full thread connection. Never replace it with a one-shot reviewThreads(first:N) result. If a direct diagnostic query is genuinely necessary, follow pageInfo until hasNextPage == false before drawing a count or unresolved-state conclusion.

In the self-sustaining watch, back the tick with the background change-detector. pr-snapshot watch runs that same fetch→diff on an interval with no agent tokens and prints a single BABYSIT_WAKE {reason,url,...} line only when there is work to inspect or a stop/residual condition, then exits. The wake reasons, which reference owns each, and how to handle a stale or superseded wake are in references/watch-loop.md. Keep the session active while waiting for that line with the harness's tools (Step 1); arm it with:

SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1; RUN_INVOCATION_ID="<invocation_id>"; RUN_STARTED_AT="<invocation_started_at>"; RUN_BUDGET_SECONDS="<invocation_budget_seconds>";
PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
"$PY" "$SKILL_DIR/scripts/pr-snapshot" watch --pr <N> --repo <[host/]owner/repo> --state-dir "$STATE_DIR" --interval 150 --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS"

In a confirmed managed stack under stack-ready/stack-land, append --downstack-pr <N> for every open lower layer to that arm so the watcher probes below (references/stack.md).

Do not pass --settle-seconds or --blocked-external-drain-seconds on the ordinary arm. The script's 300s default is the initial merge-ready settle window. Step 3 alone sets --settle-seconds after a rejected merge-ready wake, and --blocked-external-drain-seconds after a blocked-external (approval-gate) wake begins the bounded review drain.

Shell state does not persist between separate tool calls. SKILL_DIR and STATE_DIR are set only for the command they appear in. The later mark calls (Steps 3 and 5) run as their own invocations, so re-set both inline in each of those commands, or pass the absolute paths directly. A bare $SKILL_DIR in a fresh call is empty and resolves to the wrong path.

**<host> in STATE_DIR is required for GitHub Enterprise.** Derive it from the PR URL's host (or gh repo view --json url); on plain github.com it is github.com. Use the same value in every mark, so PRs that share owner/repo#N on different hosts never share one state.json. **Pass the same host in --repo <host>/<owner>/<repo>** (the documented [HOST/]OWNER/REPO selector): pr-snapshot's first gh pr view runs before it parses the URL host, and without the selector it queries the checkout's default github.com.

The snapshot emits the attention set: the items that still need action. It contains unresolved threads you have not yet acted on, non-thread feedback candidates (top-level PR comments + review-submission bodies) you have not yet classified, failing checks on the current head you have not yet dispatched, and the exact current branch_currency item with its attention route. counts totals the threads, checks, and comments; the currency item appears only in branch_currency, so zero counts do not mean the attention set is empty. The rest of the snapshot reports merge, check, currency, timing, decision, pr_chain, and trajectory facts; the references that act on a field state what it means.

Whether mergeable was computed against the current base is decided by base.identity and mergeability_certain; a race, mergeability-pending, or probe-error identity disables mergeability_certain and re-polls (references/watch-loop.md, Current-base merge identity). Invocation time and persisted-state age are separate; never report one as the other. pr_chain carries two independent axes, manager_status (confirmed|absent|probe-error) and relationship_status (dependent|independent|probe-error), plus the manager source, target/upstack freshness, ordered entries, and ordinary parent/dependent PRs when available.

The members of actionable.comments are candidates awaiting semantic classification, not detector-proven action items. The deterministic fetch excludes only empty bodies. A nonempty comment from the PR author stays in, since an author asking for a change on their agent-opened PR is ordinary feedback; the dispatched mark, not author identity, prevents loops. The fetch does not decide from content, bot identity, or comment-vs-review surface whether an external message is valid feedback; ce-resolve-pr-feedback makes that judgment. An item stays in the attention set until you confirm you acted or classified it (mark) or remote truth removes it (a resolved thread drops out of the fetch), so a crashed, failed, or superseded resolve pass leaves its items for the next tick. Every mark presents the same RUN_INVOCATION_ID, RUN_STARTED_AT, and RUN_BUDGET_SECONDS, so a stale resolver tick fails before it can silence work in a replacement invocation. The state schema and the claim→act→confirm protocol are in references/watch-loop.md; read it before acting if this session has not already loaded it.

**The trajectory is facts, not a verdict. You hand it to the delegated skills (the leaves: ce-debug and ce-resolve-pr-feedback), and they judge convergence.** The triggers are: check_recur_max >= 2; stream_alternations >= 3; a rising unresolved_trend with new_threads_this_tick > 0 across passes; heads_since_progress >= 2; or any invariant_rounds[].rounds >= 2 (a key at 2 recorded rounds means the next fix would be its third). When the trajectory crosses one, pass it to that tick's ce-debug/ce-resolve-pr-feedback invocation as mandatory input before that leaf mutates, and let the leaf decide whether this is ordinary progress or genuine non-convergence. A leaf may then return a needs-human residual that parks the whole stream, for example an emergent CI trade-off, a wrong-approach nitpick cluster, or a third invariant round. Never declare non-convergence yourself. The trigger→route→park→re-open protocol is in references/watch-loop.md (Non-convergence); read it before acting on this if this session has not already loaded it.

Raw lines 5 to 47
Runs code scripts/pr-snapshot :15
Starts an agent dispatched :40
Names a skill ce-resolve-pr-feedback :44
Names a skill ce-debug :46
48## Ordering invariant (full text)
50**The ordering invariant (this is the whole point):**
521. **Terminal check first.** If `pr_state` is `MERGED` or `CLOSED`, stop and report; the loop is done. The one **exception**: when this run just completed an authorized `stack-land` merge on that PR, treat that MERGED outcome as a managed-stack layer transition (see Step 3's stack-land land step), not a run-level Terminal stop.
532. **Capture the head SHA now** (`git rev-parse HEAD` or the snapshot's `head_sha`) so you can tell later whether the comment pass pushed.
55**Managed-stack pre-push baseline.** Before invoking a delegate that may push the active target in a confirmed managed stack, record a recoverable baseline from a fresh `gh stack view --json`: the manager-ordered open branches at or above the target (the target plus its open dependents) and each branch's current remote-tracking OID on the tracking remote. Require a clean worktree and still-confirmed manager membership for the target/current branch. If either precondition fails, stop the active invocation in every mode: do not invoke a delegate, run another tick, or arm/re-arm a watcher; state the residual and give the host-rendered resume invocation. Do not stop for missing atomic multi-ref push proof; current `gh stack push` may update branches non-atomically (`github/gh-stack#216`). Prefer all-or-none when an installed manager later proves atomic push, but always re-probe after push rather than assuming it.
573. **Feedback before CI.** If the attention set has **either** unresolved threads **or** non-thread feedback candidates (`counts.threads > 0` or `counts.comments > 0`), invoke `ce-resolve-pr-feedback` **once**, in full mode **with `mode:pipeline`**. One resolve pass per tick; never fan out multiple. Pass the resolved PR ref: the base `[HOST/]OWNER/REPO#N` or the full PR URL from the snapshot's `url`. A fork→upstream PR then resolves against the **upstream base**, not the fork checkout's `origin`, which would query the wrong PR namespace. `mode:pipeline` makes the resolver return any `needs-human` as a structured residual instead of pausing on a blocking user question that would stall the watch. The resolver re-fetches and judges *all* feedback (inline threads, review bodies, and top-level comments) and is idempotent on empty. A Changes-Requested review body or a bare top-level "please rename X" with **no inline thread** must still trigger a pass. **When the review trigger above is crossed, pass the `trajectory`** so the resolver can judge a treadmill or wrong-approach nitpick cluster and return one approach-level `needs-human` instead of fixing forever; an `invariant_rounds[].rounds >= 2` trigger routes it **before** this pass may fix, commit, or push. **When the recurring items are *valid* and share one root and fix, request a bounded-class assessment**, so it consolidates the equivalent sites this PR touched into a single fix rather than fixing one per head (`references/watch-loop.md`, Non-convergence).
59For any delegate result, process its typed `needs-human` residuals through one shared step. Immediately render every complete payload under `## Needs your decision`, preserve it unchanged for caller return, and write it to an OS-temp JSON file. Persist each residual once: the snapshot validates the full schema, freezes every source's current observation, and publishes one decision under the locked state write. It emits the unchanged payload in canonical `needs_human_residuals` and its answer-routing ID in `human_decisions`; source dispositions remain ordinary open/dispatched facts. Any covered observation changing or disappearing invalidates the whole decision and reactivates its surviving sources, but remote activity is never an answer. Continue independent work without resolving covered threads.
61After the resolver, reconcile every **comment you passed that is not covered by a returned typed residual**. A top-level comment or review body never drops out of the fetch on its own, and `ce-resolve-pr-feedback` may silently drop boilerplate, status noise, or other non-actionable feedback after applying agent judgment. Mark every comment you passed as `dispatched` unless the shared residual mark already parked it; never add a route-specific `needs-human` mark. Leaving an uncovered comment unmarked keeps it in the attention set, so the loop can never settle:
63```bash
64SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1;
65PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
66"$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --pr <N> --repo <[host/]owner/repo> --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --disposition needs-human --residual-file <path-containing-one-exact-typed-residual>
67"$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --comment <ID> --disposition dispatched --invariant-key <key-returned-by-the-leaf-on-a-fix-outcome>
68```
70Pass `--pr`/`--repo` on the shared residual mark. `mark` then re-reads every covered thread's current last comment (including your just-posted replies) before the atomic write, so later reviewer activity re-opens the whole group instead of being swallowed. The decision is published only when every covered thread was re-read; a missing observation leaves the complete source set actionable. Covered comments and review bodies keep their snapshot edit identities for the same reason. A **dispatched comment** mark needs no baseline: it stays silenced until an explicit `mark --disposition open` and is never reactivated by a body edit, because status bots rewrite their bodies on every push. A genuinely new request arrives as a review thread or a new comment, both of which are still shown. On a **fix** outcome, persist the leaf-returned `invariant_key` on that same dispatched mark (`--thread` or `--comment` plus `--invariant-key`); omit the flag when the leaf returned none. The leaf does not run `pr-snapshot`.
72When the user answers, map their response to the displayed `decision_id`, preserve the exact response in a file, and record the shared answer transition before acting on it. The answer is consumed only when this mark succeeds. Read-only prohibits executing the transition, not rendering it. In a read-only run (one whose boundaries forbid writes), return the literal command below as the sole pending transition; substitute exact known values and retain explicit placeholders for unavailable invocation metadata or the answer-file path. That rendered command must include the literal `--answer-decision` and `--answer-file` flags. A prose paraphrase or an in-memory state move is incomplete.
74```bash
75SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1; RUN_INVOCATION_ID="<invocation_id>"; RUN_STARTED_AT="<invocation_started_at>"; RUN_BUDGET_SECONDS="<invocation_budget_seconds>";
76PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
77"$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --answer-decision <decision_id> --answer-file <path-containing-the-exact-human-response>
78```
80The next snapshot returns that record in `answered_human_decisions` and makes every still-matching source ordinary actionable work. Apply the answer, then use the existing dispatched/confirmed marks; the answered record retires when its sources are handled or their observations move. Never convert a thread reply, comment edit, check rerun, head move, or currency change into an answer implicitly.
82Show those decisions in Step 4 (the report) and continue independent work. Once none remains, `mode:pipeline` returns the canonical set as its decision handoff; interactive continuous mode keeps watching around the parked sources. Also keep the resolver's **non-routine verdicts** for the Step 4 summary: a fix done differently than the reviewer suggested (`fixed-differently`), feedback it declined (`declined`), or feedback it rebutted as wrong (`not-addressing`). A plain `fixed` is routine and not worth carrying.
834. **Stale-SHA cancellation.** Compare the current head SHA to the one captured in step 2. If it **changed**, the comment pass (or someone else) pushed, so the CI failures in this snapshot are against a dead SHA: **do not act on them**. The new run will appear next tick. If it did **not** change, continue to CI.
845. **CI on the current head.** Aggregate *all* actionable failing checks into one remediation pass; do not dispatch per check. Classify from metadata:
85 - **Flaky/infra** (known-flaky job, infrastructure/timeout signal) → extract the run ID **and the full base repo including host** from the failing check's `details_url` (`https://<host>/<owner>/<repo>/actions/runs/<run-id>/…`) and run `gh run rerun <run-id> --failed -R <host>/<owner>/<repo>`. Without the run ID, `gh run rerun` opens an interactive run-picker that blocks an unattended run. The host-qualified `-R` is required for fork→upstream and GitHub Enterprise PRs: the run lives in the **base** repo on its own host, so a bare `-R <owner/repo>` (or no `-R`) targets the fork or the default `github.com` and 404s.
86 - **Blocked by a repository setting or permission** (the log shows GitHub refusing an action because of how the repository or token is configured, not because of the code — for example an auto-merge job failing with "Auto merge is not allowed for this repository") → do not invoke `ce-debug`; no code change can fix it, and changing the setting is outside this skill's authority. Once the setting is fixed, re-run only that job, not every failed job in its run: take `<run-id>` and the repo as the flaky/infra bullet does, look up the job's `databaseId` with `gh run view <run-id> --json jobs --jq '.jobs[] | {name, databaseId}' -R <host>/<owner>/<repo>` (the job number in the check's browser URL is a different ID and 404s), then run `gh run rerun --job <databaseId> -R <host>/<owner>/<repo>`. If it already reads fixed now (for auto-merge, `allow_auto_merge` is `true`), re-run it this tick. Otherwise write a typed `needs-human` residual whose `sources` is that check and whose `decision_context` names the setting or permission and where it is changed, persist it through the shared residual step above, and re-run when the user answers that it is fixed. Never push an empty commit or any other new head to retrigger it.
87 - **Real test/build failure** → invoke `ce-debug mode:pipeline` once, seeded with the failing jobs and their log tails. **When the CI trigger above is crossed, also pass the `trajectory` (`recurring_checks`, `check_recur_max`, `heads_since_progress`) so it can judge oscillation vs ordinary progress.** Its structured return `status` is exactly one of `fixed-and-pushed`, `fixed-not-pushed`, `flaky-infra`, `diagnosed-no-fix`, or `needs-human`; do not invent `infra-retry` or `stale`. Handle convergent fixes and retries as before. A `needs-human` result returns complete typed residuals whose `check` sources enter the canonical set through the shared residual step above; park those checks with the same residual file. `diagnosed-no-fix` and failed pushes remain ordinary red residuals. Never weaken a test, rebase, reset, or force-push to clear them.
88 The shared residual step above already parks every check covered by a `needs-human` residual. Record each other check you acted on so it is not re-dispatched at this head (re-set the vars inline):
90 ```bash
91 SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1;
92 PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
93 "$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --check "<key>"
94 ```
96 (A new head SHA clears these automatically.)
986. **Branch currency & conflicts (the third stream — after comments and CI).** Consume the exact current `branch_currency` item; no item means no base-into-head mutation. The full route, the claim lifecycle, and the `BEHIND`/`DIRTY` mechanisms are in `references/branch-currency.md`.
997. **After an authorized target-head push in a confirmed managed stack, preserve the upstack before resuming the watch** (`gh stack rebase "<first-dependent-branch>" --upstack --no-trunk` + `gh stack push`; abort and park on conflict): `references/stack.md`.
1008. **After any mutation, re-snapshot** at the start of the next tick, passing the same `--invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS"`. The head SHA and the CI universe have changed, but the invocation-wide budget has not. Do not run a second `snapshot` mid-tick to re-derive CI; that is what caused stale-SHA confusion.
102During accepted managed-stack continuation there is one watcher (probing lower layers via `--downstack-pr`) and one mutated target; the transition condition, the return rule, and the landing gate are defined in `references/stack.md`.

Ordering invariant (full text)

The ordering invariant (this is the whole point):

  1. Terminal check first. If pr_state is MERGED or CLOSED, stop and report; the loop is done. The one exception: when this run just completed an authorized stack-land merge on that PR, treat that MERGED outcome as a managed-stack layer transition (see Step 3's stack-land land step), not a run-level Terminal stop.
  2. Capture the head SHA now (git rev-parse HEAD or the snapshot's head_sha) so you can tell later whether the comment pass pushed.

Managed-stack pre-push baseline. Before invoking a delegate that may push the active target in a confirmed managed stack, record a recoverable baseline from a fresh gh stack view --json: the manager-ordered open branches at or above the target (the target plus its open dependents) and each branch's current remote-tracking OID on the tracking remote. Require a clean worktree and still-confirmed manager membership for the target/current branch. If either precondition fails, stop the active invocation in every mode: do not invoke a delegate, run another tick, or arm/re-arm a watcher; state the residual and give the host-rendered resume invocation. Do not stop for missing atomic multi-ref push proof; current gh stack push may update branches non-atomically (github/gh-stack#216). Prefer all-or-none when an installed manager later proves atomic push, but always re-probe after push rather than assuming it.

  1. Feedback before CI. If the attention set has either unresolved threads or non-thread feedback candidates (counts.threads > 0 or counts.comments > 0), invoke ce-resolve-pr-feedback once, in full mode **with mode:pipeline**. One resolve pass per tick; never fan out multiple. Pass the resolved PR ref: the base [HOST/]OWNER/REPO#N or the full PR URL from the snapshot's url. A fork→upstream PR then resolves against the upstream base, not the fork checkout's origin, which would query the wrong PR namespace. mode:pipeline makes the resolver return any needs-human as a structured residual instead of pausing on a blocking user question that would stall the watch. The resolver re-fetches and judges all feedback (inline threads, review bodies, and top-level comments) and is idempotent on empty. A Changes-Requested review body or a bare top-level "please rename X" with no inline thread must still trigger a pass. **When the review trigger above is crossed, pass the trajectory** so the resolver can judge a treadmill or wrong-approach nitpick cluster and return one approach-level needs-human instead of fixing forever; an invariant_rounds[].rounds >= 2 trigger routes it before this pass may fix, commit, or push. **When the recurring items are valid and share one root and fix, request a bounded-class assessment**, so it consolidates the equivalent sites this PR touched into a single fix rather than fixing one per head (references/watch-loop.md, Non-convergence).

For any delegate result, process its typed needs-human residuals through one shared step. Immediately render every complete payload under ## Needs your decision, preserve it unchanged for caller return, and write it to an OS-temp JSON file. Persist each residual once: the snapshot validates the full schema, freezes every source's current observation, and publishes one decision under the locked state write. It emits the unchanged payload in canonical needs_human_residuals and its answer-routing ID in human_decisions; source dispositions remain ordinary open/dispatched facts. Any covered observation changing or disappearing invalidates the whole decision and reactivates its surviving sources, but remote activity is never an answer. Continue independent work without resolving covered threads.

After the resolver, reconcile every comment you passed that is not covered by a returned typed residual. A top-level comment or review body never drops out of the fetch on its own, and ce-resolve-pr-feedback may silently drop boilerplate, status noise, or other non-actionable feedback after applying agent judgment. Mark every comment you passed as dispatched unless the shared residual mark already parked it; never add a route-specific needs-human mark. Leaving an uncovered comment unmarked keeps it in the attention set, so the loop can never settle:

SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1;
PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
"$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --pr <N> --repo <[host/]owner/repo> --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --disposition needs-human --residual-file <path-containing-one-exact-typed-residual>
"$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --comment <ID> --disposition dispatched --invariant-key <key-returned-by-the-leaf-on-a-fix-outcome>

Pass --pr/--repo on the shared residual mark. mark then re-reads every covered thread's current last comment (including your just-posted replies) before the atomic write, so later reviewer activity re-opens the whole group instead of being swallowed. The decision is published only when every covered thread was re-read; a missing observation leaves the complete source set actionable. Covered comments and review bodies keep their snapshot edit identities for the same reason. A dispatched comment mark needs no baseline: it stays silenced until an explicit mark --disposition open and is never reactivated by a body edit, because status bots rewrite their bodies on every push. A genuinely new request arrives as a review thread or a new comment, both of which are still shown. On a fix outcome, persist the leaf-returned invariant_key on that same dispatched mark (--thread or --comment plus --invariant-key); omit the flag when the leaf returned none. The leaf does not run pr-snapshot.

When the user answers, map their response to the displayed decision_id, preserve the exact response in a file, and record the shared answer transition before acting on it. The answer is consumed only when this mark succeeds. Read-only prohibits executing the transition, not rendering it. In a read-only run (one whose boundaries forbid writes), return the literal command below as the sole pending transition; substitute exact known values and retain explicit placeholders for unavailable invocation metadata or the answer-file path. That rendered command must include the literal --answer-decision and --answer-file flags. A prose paraphrase or an in-memory state move is incomplete.

SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1; RUN_INVOCATION_ID="<invocation_id>"; RUN_STARTED_AT="<invocation_started_at>"; RUN_BUDGET_SECONDS="<invocation_budget_seconds>";
PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; };
"$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --answer-decision <decision_id> --answer-file <path-containing-the-exact-human-response>

The next snapshot returns that record in answered_human_decisions and makes every still-matching source ordinary actionable work. Apply the answer, then use the existing dispatched/confirmed marks; the answered record retires when its sources are handled or their observations move. Never convert a thread reply, comment edit, check rerun, head move, or currency change into an answer implicitly.

Show those decisions in Step 4 (the report) and continue independent work. Once none remains, mode:pipeline returns the canonical set as its decision handoff; interactive continuous mode keeps watching around the parked sources. Also keep the resolver's non-routine verdicts for the Step 4 summary: a fix done differently than the reviewer suggested (fixed-differently), feedback it declined (declined), or feedback it rebutted as wrong (not-addressing). A plain fixed is routine and not worth carrying.

  1. Stale-SHA cancellation. Compare the current head SHA to the one captured in step 2. If it changed, the comment pass (or someone else) pushed, so the CI failures in this snapshot are against a dead SHA: do not act on them. The new run will appear next tick. If it did not change, continue to CI.
  2. CI on the current head. Aggregate all actionable failing checks into one remediation pass; do not dispatch per check. Classify from metadata:
  3. Flaky/infra (known-flaky job, infrastructure/timeout signal) → extract the run ID and the full base repo including host from the failing check's details_url (https://<host>/<owner>/<repo>/actions/runs/<run-id>/…) and run gh run rerun <run-id> --failed -R <host>/<owner>/<repo>. Without the run ID, gh run rerun opens an interactive run-picker that blocks an unattended run. The host-qualified -R is required for fork→upstream and GitHub Enterprise PRs: the run lives in the base repo on its own host, so a bare -R <owner/repo> (or no -R) targets the fork or the default github.com and 404s.
  4. Blocked by a repository setting or permission (the log shows GitHub refusing an action because of how the repository or token is configured, not because of the code — for example an auto-merge job failing with "Auto merge is not allowed for this repository") → do not invoke ce-debug; no code change can fix it, and changing the setting is outside this skill's authority. Once the setting is fixed, re-run only that job, not every failed job in its run: take <run-id> and the repo as the flaky/infra bullet does, look up the job's databaseId with gh run view <run-id> --json jobs --jq '.jobs[] | {name, databaseId}' -R <host>/<owner>/<repo> (the job number in the check's browser URL is a different ID and 404s), then run gh run rerun --job <databaseId> -R <host>/<owner>/<repo>. If it already reads fixed now (for auto-merge, allow_auto_merge is true), re-run it this tick. Otherwise write a typed needs-human residual whose sources is that check and whose decision_context names the setting or permission and where it is changed, persist it through the shared residual step above, and re-run when the user answers that it is fixed. Never push an empty commit or any other new head to retrigger it.
  5. Real test/build failure → invoke ce-debug mode:pipeline once, seeded with the failing jobs and their log tails. **When the CI trigger above is crossed, also pass the trajectory (recurring_checks, check_recur_max, heads_since_progress) so it can judge oscillation vs ordinary progress.** Its structured return status is exactly one of fixed-and-pushed, fixed-not-pushed, flaky-infra, diagnosed-no-fix, or needs-human; do not invent infra-retry or stale. Handle convergent fixes and retries as before. A needs-human result returns complete typed residuals whose check sources enter the canonical set through the shared residual step above; park those checks with the same residual file. diagnosed-no-fix and failed pushes remain ordinary red residuals. Never weaken a test, rebase, reset, or force-push to clear them. The shared residual step above already parks every check covered by a needs-human residual. Record each other check you acted on so it is not re-dispatched at this head (re-set the vars inline):

bash SKILL_DIR="<absolute path of this skill's directory>"; SCRATCH_ROOT="/tmp/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") 2>/dev/null && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && [ -w "$SCRATCH_ROOT" ] || SCRATCH_ROOT="${TMPDIR:-/tmp}/compound-engineering-$(id -u)"; [ ! -L "$SCRATCH_ROOT" ] && (umask 077; mkdir -p "$SCRATCH_ROOT") && [ ! -L "$SCRATCH_ROOT" ] && [ -O "$SCRATCH_ROOT" ] && chmod 700 "$SCRATCH_ROOT" || { echo "unsafe scratch root: $SCRATCH_ROOT" >&2; exit 1; }; STATE_DIR="$SCRATCH_ROOT/ce-babysit-pr/<host>-<owner>-<repo>-<N>"; (umask 077; mkdir -p "$STATE_DIR") || exit 1; chmod 700 "$STATE_DIR" || exit 1; PY="$(for c in python3 python py; do command -v "$c" >/dev/null 2>&1 && "$c" -c '' >/dev/null 2>&1 && { echo "$c"; break; }; done)"; [ -n "$PY" ] || { echo "no working Python 3 interpreter on PATH" >&2; exit 1; }; "$PY" "$SKILL_DIR/scripts/pr-snapshot" mark --state-dir "$STATE_DIR" --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS" --check "<key>"

(A new head SHA clears these automatically.)

  1. Branch currency & conflicts (the third stream — after comments and CI). Consume the exact current branch_currency item; no item means no base-into-head mutation. The full route, the claim lifecycle, and the BEHIND/DIRTY mechanisms are in references/branch-currency.md.
  2. After an authorized target-head push in a confirmed managed stack, preserve the upstack before resuming the watch (gh stack rebase "<first-dependent-branch>" --upstack --no-trunk + gh stack push; abort and park on conflict): references/stack.md.
  3. After any mutation, re-snapshot at the start of the next tick, passing the same --invocation-id "$RUN_INVOCATION_ID" --session-started-at "$RUN_STARTED_AT" --invocation-budget-seconds "$RUN_BUDGET_SECONDS". The head SHA and the CI universe have changed, but the invocation-wide budget has not. Do not run a second snapshot mid-tick to re-derive CI; that is what caused stale-SHA confusion.

During accepted managed-stack continuation there is one watcher (probing lower layers via --downstack-pr) and one mutated target; the transition condition, the return rule, and the landing gate are defined in references/stack.md.

Raw lines 48 to 102
Names a skill ce-resolve-pr-feedback :57
Starts an agent dispatched :59
Runs code scripts/pr-snapshot :66
Starts an agent dispatch :84
Names a skill ce-debug :86