@muggleai/works 5.28.0 → 5.29.0-staging.256
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/dist/{chunk-N3G4AX6M.js → chunk-XEKNDWXD.js} +7 -4
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.codex-mcp.json +1 -1
- package/dist/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/.mcp.json +1 -1
- package/dist/plugin/hooks/README.md +3 -3
- package/dist/plugin/scripts/guardrail-dispatch.sh +6 -0
- package/dist/plugin/scripts/guardrail-e2e-gate.sh +24 -0
- package/dist/plugin/scripts/guardrail-lib.sh +1 -0
- package/dist/plugin/scripts/guardrails.mjs +119 -45
- package/dist/plugin/scripts/pr-watch-arm.sh +14 -10
- package/dist/plugin/scripts/pr-watch-events.sh +14 -5
- package/dist/plugin/scripts/pr-watch-fetch.sh +1 -1
- package/dist/plugin/scripts/pr-watch-loop.sh +6 -5
- package/dist/plugin/scripts/pr-watch-state.jq +15 -5
- package/dist/plugin/scripts/pr-watch-verdict.mjs +22 -7
- package/dist/plugin/skills/_shared/ci-check-to-command.md +1 -1
- package/dist/plugin/skills/do/e2e-acceptance.md +1 -1
- package/dist/plugin/skills/do/fix-ci.md +9 -1
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +2 -2
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +2 -0
- package/dist/plugin/skills/muggle-pr-followup/contract.md +2 -2
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +12 -12
- package/dist/release-manifest.json +3 -3
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-mcp.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/hooks/README.md +3 -3
- package/plugin/scripts/guardrail-dispatch.sh +6 -0
- package/plugin/scripts/guardrail-e2e-gate.sh +24 -0
- package/plugin/scripts/guardrail-lib.sh +1 -0
- package/plugin/scripts/guardrails.mjs +119 -45
- package/plugin/scripts/pr-watch-arm.sh +14 -10
- package/plugin/scripts/pr-watch-events.sh +14 -5
- package/plugin/scripts/pr-watch-fetch.sh +1 -1
- package/plugin/scripts/pr-watch-loop.sh +6 -5
- package/plugin/scripts/pr-watch-state.jq +15 -5
- package/plugin/scripts/pr-watch-verdict.mjs +22 -7
- package/plugin/skills/_shared/ci-check-to-command.md +1 -1
- package/plugin/skills/do/e2e-acceptance.md +1 -1
- package/plugin/skills/do/fix-ci.md +9 -1
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +2 -2
- package/plugin/skills/muggle-pr-followup/bootstrap.md +2 -0
- package/plugin/skills/muggle-pr-followup/contract.md +2 -2
- package/plugin/skills/muggle-pr-followup/state-schemas.md +12 -12
|
@@ -57,7 +57,7 @@ You receive everything from `state.md` already — pre-flight resolved it:
|
|
|
57
57
|
|
|
58
58
|
Read `state.md`.
|
|
59
59
|
|
|
60
|
-
**No `## Pre-flight answers` block at all** → the session was seeded poll-only (e.g. by auto-track, [`../muggle-pr-followup/auto-track.md`](../muggle-pr-followup/auto-track.md)). Treat `Validation` as `skip`: emit a `SKIPPED` report citing `
|
|
60
|
+
**No `## Pre-flight answers` block at all** → the session was seeded poll-only (e.g. by auto-track, [`../muggle-pr-followup/auto-track.md`](../muggle-pr-followup/auto-track.md)). Treat `Validation` as `skip`: emit a `SKIPPED` report citing the code that holds — `NO_WEB_SURFACE` when the repo serves nothing to drive, `NOT_APPLICABLE` when no browser run can reach the change — and exit cleanly. Never `NO_PR`: this cycle is on a PR, so the guardrail refutes it and the walkthrough stays unsettled. When a browser run *could* reach the change, no code holds: report `SKIPPED` without declaring one and leave the walkthrough to the owner's E2E decision. The watcher owns no E2E context by design; "no context" is a clean skip, not a failure.
|
|
61
61
|
|
|
62
62
|
Otherwise the block was seeded by pre-flight or bootstrap per [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md) — read it the same way regardless of seeder. The persisted `Validation` field (`local-e2e`, `staging-replay`, `unit-only`, `skip`) picks execution vs early-exit below. In a forward run, [`autoE2ETest`](../muggle-preferences/preference-gates/autoE2ETest.md) `ask` was resolved by pre-flight Q13; in a watcher cycle there is no per-tick pre-flight, so `Validation` **is** the standing decision — don't re-resolve `ask`.
|
|
63
63
|
|
|
@@ -31,6 +31,8 @@ Check out the PR branch in the session's working tree (per `state.md`). Capture
|
|
|
31
31
|
|
|
32
32
|
Then increment `last_seen.ci_fix_attempts[red_sha]` — before any fix, re-run or escalation, as a whole-file rewrite (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md), never the Edit tool. Every dispatched cycle is an attempt, including one that only re-runs a check or escalates without pushing. That count is the only record the watcher and its turn-end gate accept that this red head was acted on; an escalation written without it is ignored and fix-ci is dispatched again ([`../muggle-pr-followup/state-schemas.md`](../muggle-pr-followup/state-schemas.md#last_seenjson)).
|
|
33
33
|
|
|
34
|
+
The increment opens a cycle this turn carries through Step 5 or Step 6 — it never stands in for one. The single exception is a red `muggle-walkthrough` whose E2E run for **this** PR is already queued or running (its `state.md` shows `E2E dispatch: queued` or `running`): that run is the cycle, and its report settles the comment. Writing the count for a PR with no such run, or because another PR's run is in flight, clears the turn-end gate while the PR stays red; the watch never asks again until the head moves, and the PR can merge red.
|
|
35
|
+
|
|
34
36
|
### Step 2 — Map each failing check to a local command
|
|
35
37
|
|
|
36
38
|
Per [`../_shared/ci-check-to-command.md`](../_shared/ci-check-to-command.md). Fix the in-scope checks in the working tree; record out-of-scope checks for escalation (Step 6). A red `muggle-walkthrough` check takes Step 2.5; a red non-deterministic check takes Step 2.6.
|
|
@@ -45,7 +47,13 @@ How long a check takes to run — a 45-minute eval included — is never a reaso
|
|
|
45
47
|
4. **Order the settle after the push.** If Step 2 changed code, run Steps 3–4 below first, so the new head carries the fix. Then settle the walkthrough comment with the re-run's report via [`../muggle-pr-visual-walkthrough/SKILL.md`](../muggle-pr-visual-walkthrough/SKILL.md) Mode A; editing the comment re-runs the check against the head. With no code change (only retries and regenerations) there is nothing to push — the comment edit alone re-runs the check.
|
|
46
48
|
5. **Wait for the fresh verdict** before Step 5's respawn: poll the check run until one completes later than the `completed_at` recorded in item 1, for up to 5 minutes. A watcher respawned sooner reads the stale red verdict and spends a fix attempt on a run already repaired. On timeout, respawn anyway — the per-SHA budget still bounds it.
|
|
47
49
|
|
|
48
|
-
An empty-comment failure (no run ever posted) skips items 1–2
|
|
50
|
+
An empty-comment failure (no run ever posted) skips items 1–2. Pick the branch from `state.md`:
|
|
51
|
+
|
|
52
|
+
- **`local-e2e` or `staging-replay`** → run the suite and settle the comment with its report.
|
|
53
|
+
- **`unit-only`, `skip`, or no `## Pre-flight answers` block at all** (a poll-only session — auto-track, or bootstrap on a PR it found no testable surface for) → settle it with the skip code that holds per [`e2e-acceptance.md`](e2e-acceptance.md): `NO_WEB_SURFACE` when the repo serves nothing to drive, `NO_APP_LOGIC_CHANGE` (detail opening with the PR's merge-base commit) when every path the PR changed is docs, tests, lint or CI config, else `NOT_APPLICABLE` with the reason no browser run can reach the change. Run `echo "MUGGLE_E2E_SKIP: <CODE>: <detail>"` from the PR's working tree. The write runs detached, so confirm it landed: run the foreground command the walkthrough gate's block names until it prints `{"settled":true}`, never a hand-written PATCH. Never `NO_PR`: the guardrail refutes it whenever the session handled a PR.
|
|
54
|
+
- **A poll-only PR whose change a browser run *can* reach** → no code holds, so do not cite one. Escalate per Step 6, naming the missing E2E decision; the owner chooses run, project or waive.
|
|
55
|
+
|
|
56
|
+
`muggle-walkthrough` is never a Step 2.6 check: it reads the comment, so re-running it without editing the comment returns the same verdict.
|
|
49
57
|
|
|
50
58
|
### Step 2.6 — Red non-deterministic check: re-run once per head
|
|
51
59
|
|
|
@@ -13,7 +13,7 @@ The same argument that forbids authoring a per-slot `watch.sh` applies to arming
|
|
|
13
13
|
The `DRAIN` lines it prints, in the watch's output, are the outstanding work as of the arm. Review ids and threads the drain read are floored, so the loop will *not* tell you about those again. A stale branch or a red head is floored only when `last_seen.json` records a rebase or fix-ci cycle already attempted for that key or head ([`state-schemas.md`](state-schemas.md#watch-watermarkenv)); otherwise the drain line says `not floored: dispatch …` and the loop's first poll reports it as an event. A PR that spent its catch-up rebase budget is the exception: its drain line says the rebase is `held for the owner`, and nothing is dispatched — the tick would refuse it — until the owner raises [`maxCatchUpRebases`](../muggle-preferences/preference-gates/maxCatchUpRebases.md). Re-arming is never a way to set work aside — it neither handles nor silences anything. Steps 1-3 below are what the script does, and why.
|
|
14
14
|
|
|
15
15
|
1. **Drain.** Run one tick per [`contract.md`](contract.md). It acts on everything already outstanding — actionable threads (`gitlab`: discussions), body-only reviews past the watermark (GitHub-only — GitLab has no review envelope), a stale branch, red CI — and finalizes a terminal PR. If the tick dispatched a cycle, stop here: the cycle's exit path settles the watch when it finishes.
|
|
16
|
-
2. **Seed the watermark.** *(`pr-watch-arm.sh` does this; the reasoning is kept because the floors are subtle and wrong ones are quiet.)* Resolve the provider once per [`../_shared/vcs/detect-vcs.md`](../_shared/vcs/detect-vcs.md) — every fetch in this sequence uses that provider's recipes. Write the slot's watch watermark ([`state-schemas.md`](state-schemas.md#watch-watermarkenv)) to the ids the **drain itself read** — the max review-id and comment-id observed at the drain's own fetch (Step 1), snapshotted at that read. Never let the loop capture its own baseline — the arming session writes it; and **never** from a fresh fetch taken after the drain, which would include a comment that arrived after the drain read the wave and mark it seen unread. Seeded to the drain's floor, anything landing after that read stays above the watermark and the loop's first iteration surfaces it. Seed the CI floor (`CIRED`) from the same drain read: set it to the
|
|
16
|
+
2. **Seed the watermark.** *(`pr-watch-arm.sh` does this; the reasoning is kept because the floors are subtle and wrong ones are quiet.)* Resolve the provider once per [`../_shared/vcs/detect-vcs.md`](../_shared/vcs/detect-vcs.md) — every fetch in this sequence uses that provider's recipes. Write the slot's watch watermark ([`state-schemas.md`](state-schemas.md#watch-watermarkenv)) to the ids the **drain itself read** — the max review-id and comment-id observed at the drain's own fetch (Step 1), snapshotted at that read. Never let the loop capture its own baseline — the arming session writes it; and **never** from a fresh fetch taken after the drain, which would include a comment that arrived after the drain read the wave and mark it seen unread. Seeded to the drain's floor, anything landing after that read stays above the watermark and the loop's first iteration surfaces it. Seed the CI floor (`CIRED`) from the same drain read: set it to the red key (`<head_sha>@<red-check-runs>`) only when the checks have **already settled red** at that read (no check pending, one or more in the `fail` bucket per [`../_shared/vcs/common/ci-rollup.md`](../_shared/vcs/common/ci-rollup.md)) **and** fix-ci is finished with that head — it escalated, or spent the tick's per-head budget — and empty otherwise. A lone attempt does not floor: the count is written before the fix runs, so a cycle that recorded it and changed nothing would otherwise leave the red silent. Seed the rebase floor (`REBASED`) the same way, from the drain's branch-standing read per [`../_shared/vcs/common/branch-standing.md`](../_shared/vcs/common/branch-standing.md): the current `rebase_key` (`<head_sha>..<base_tip_sha>`) only when the drain found the branch behind or conflicting **and** a rebase attempt is recorded for that key, empty otherwise. An escalation entry alone never floors anything: one with no attempt behind it records a decision nobody acted on, and flooring it is how a re-arming session once kept a stale branch and a red head silent for a day ([`state-schemas.md`](state-schemas.md#watch-watermarkenv) names the fields). Seed the blocked-CI floor (`BLOCKED_CIDIGEST`) to the blocked fingerprint's `ci_digest` when arming while `last_seen.blocked` is already set, and empty otherwise — empty is the not-blocked state, in which the loop's blocked-resume probe stays dormant. Seed the rebase hold (`REBASE_HELD`) to `1` when `last_seen.json`'s `catch_up_rebases` has reached `maxCatchUpRebases` — the same check the tick's verdict blocks on — and empty otherwise: while it is set the loop raises no rebase wake on any base move. The script reads all four facts from `last_seen.json` through [`../../scripts/pr-watch-verdict.mjs`](../../scripts/pr-watch-verdict.mjs) `arm-floors`, and records what it found due in `watch-due.env` ([`state-schemas.md`](state-schemas.md#watch-dueenv)).
|
|
17
17
|
3. **Dedup, then watch.** First read `<slot>/watch.pid` ([`state-schemas.md`](state-schemas.md#watchpid)): if it names a live process (`kill -0 "$pid"` succeeds) **and `owner.json` records this session**, a watcher already owns this slot — **skip arming, do not start a second**. This is what stops orphaned watchers from accumulating: the in-session task dying does not stop the OS loop it launched (on Windows a detached Git Bash loop keeps running and polling `gh` forever after the session ends), so checking a live task list is not enough — the PID lease is.
|
|
18
18
|
|
|
19
19
|
**A live lease owned by a different session does not count as owned** — check it with `watcher_lease_is_foreign` from [`../../scripts/pr-watch-guards.sh`](../../scripts/pr-watch-guards.sh). A live PID proves the loop is running, never that anything is listening: an orphan left by a dead session keeps polling and keeps touching its heartbeat while its stdout is that session's closed pipe, so every liveness signal reads healthy and the events reach nobody. Skipping on the PID alone let such a corpse block this session from arming the PR for the whole lifetime cap — seven days, since the default moved off six hours. When the guard reports a foreign lease, kill that PID, delete `watch.pid`, and arm fresh; note it in the slot's `followup.log` so the reclaim is visible.
|
|
@@ -36,7 +36,7 @@ The `DRAIN` lines it prints, in the watch's output, are the outstanding work as
|
|
|
36
36
|
|
|
37
37
|
**Never author a per-slot `watch.sh`.** The loop ships as [`../../scripts/pr-watch-loop.sh`](../../scripts/pr-watch-loop.sh), with its wake conditions in [`../../scripts/pr-watch-events.sh`](../../scripts/pr-watch-events.sh) and its state projection in [`../../scripts/pr-watch-state.jq`](../../scripts/pr-watch-state.jq); arming runs it and passes arguments. Writing the loop from this prose was how it drifted — each arm produced an independent derivation, and a derivation that quietly dropped a wake still ran, still heartbeat, still logged, and simply never fired for the signal it lost. Two slots on one machine ended up without the behind-base wake, which left their PRs unmergeable under watchers that looked healthy. The prose below says *why* each wake exists; the shipped files are the only definition of *what* fires. A slot holding a legacy generated `watch.sh` keeps it until re-armed, at which point the supersede guard retires the old loop.
|
|
38
38
|
|
|
39
|
-
The task's description is `PR #<n> — <title>`. Description and command both matter: some task surfaces show one, some the other, and a slot-bearing command keeps the watch identifiable everywhere a raw script blob would not. One background task per PR at a time: it is the watch's visible handle, showing as a running task while the PR is polled, and replaced by a fresh one after each report. Its loop checks about every 60 seconds, re-reading the watermark and touching the slot's `watch-heartbeat` file each iteration — the liveness beacon that tells [`reconcile.md`](reconcile.md) a quiet watch is still alive; on a newer submitted review, a newer thread comment (`gitlab`: a newer discussion note), a thread newly unresolved (`gitlab`: discussion), **the head SHA's checks settling red** (no check pending and one or more in the `fail` bucket per [`../_shared/vcs/common/ci-rollup.md`](../_shared/vcs/common/ci-rollup.md)), **the branch falling behind or conflicting with its base** (`behind_by > 0` or the conflict signal per [`../_shared/vcs/common/branch-standing.md`](../_shared/vcs/common/branch-standing.md)), or — **only while the watch is blocked** (`BLOCKED_CIDIGEST` non-empty) — **the head's CI digest changing in any way** (not just to red) — it prints one line per finding and **exits**, recording what it reported in `watch-reported.env` ([`state-schemas.md`](state-schemas.md#watch-reportedenv)) so no later loop reports the same occurrence again. The review and thread floors are monotonic ids; the other three are not. The CI-red floor is the **head SHA
|
|
39
|
+
The task's description is `PR #<n> — <title>`. Description and command both matter: some task surfaces show one, some the other, and a slot-bearing command keeps the watch identifiable everywhere a raw script blob would not. One background task per PR at a time: it is the watch's visible handle, showing as a running task while the PR is polled, and replaced by a fresh one after each report. Its loop checks about every 60 seconds, re-reading the watermark and touching the slot's `watch-heartbeat` file each iteration — the liveness beacon that tells [`reconcile.md`](reconcile.md) a quiet watch is still alive; on a newer submitted review, a newer thread comment (`gitlab`: a newer discussion note), a thread newly unresolved (`gitlab`: discussion), **the head SHA's checks settling red** (no check pending and one or more in the `fail` bucket per [`../_shared/vcs/common/ci-rollup.md`](../_shared/vcs/common/ci-rollup.md)), **the branch falling behind or conflicting with its base** (`behind_by > 0` or the conflict signal per [`../_shared/vcs/common/branch-standing.md`](../_shared/vcs/common/branch-standing.md)), or — **only while the watch is blocked** (`BLOCKED_CIDIGEST` non-empty) — **the head's CI digest changing in any way** (not just to red) — it prints one line per finding and **exits**, recording what it reported in `watch-reported.env` ([`state-schemas.md`](state-schemas.md#watch-reportedenv)) so no later loop reports the same occurrence again. The review and thread floors are monotonic ids; the other three are not. The CI-red floor is the **red key** — the head SHA plus each failed check's run id — because the check rollup is non-monotonic: it flips green↔red and resets on every push, so the key fires once per red verdict and a later push re-arms it on the new SHA. The run ids matter because a settled walkthrough comment or a rerun publishes a new run on the same head; when that run is red too, it is the verdict fix-ci is waiting on, and a head-only floor swallowed it. The rebase floor (`REBASED`) is the **`rebase_key`** — `<head_sha>..<base_tip_sha>` — because staleness is a function of both sides: keying on the pair fires once per newly-due pair and re-arms when either the head or the base moves, where a head-only key would wedge permanently the first time the base advances (the head cannot change while nobody pushes). A head whose checks are still **pending** is never a red wake, and a branch with `behind_by == 0` and `mergeable == UNKNOWN` is never a rebase wake: pending checks may yet go green and conflict state is still computing, and the tick would idle on either (Steps 5–6) regardless. The blocked-CI signal is different in kind — a **resume** probe, live only while the watch is blocked: it wakes on any move of the head's CI digest (the same bucket-plus-sorted-name/conclusion signature the blocked fingerprint records — [`blocked-tick.md`](blocked-tick.md)) away from `BLOCKED_CIDIGEST`, so a block waiting on a green pass, a rerun, or an external deploy check resumes as promptly as one waiting on red. Quiet iterations print nothing and cost nothing — no model tokens are spent while the watch is quiet.
|
|
40
40
|
|
|
41
41
|
Each poll also rewrites the slot's `watch-due.env` whenever the due work changes — the rebase key found due and the head whose checks settled red, or an empty file when neither — so the session's turn-end gate can see work the watch reported without a provider call ([`state-schemas.md`](state-schemas.md#watch-dueenv)).
|
|
42
42
|
|
|
@@ -61,6 +61,8 @@ Line-comment threads need no seeding — the watcher derives them from live thre
|
|
|
61
61
|
|
|
62
62
|
The only step that may prompt the user, and only when the PR has a testable surface. Run silent detection from [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md) first. If it finds **no dev server / no testable surface** — a docs, config, skill, or library change, or a non-web repo — seed the watcher **poll-only**: no prompt, no `## Pre-flight answers` block, continue to Step 7 (same as [`auto-track`](auto-track.md)).
|
|
63
63
|
|
|
64
|
+
Detection just established why E2E does not apply, so record it on the PR before arming. If the PR's walkthrough comment is still the placeholder, run `echo "MUGGLE_E2E_SKIP: <CODE>: <detail>"` from Step 3's working tree: `NO_WEB_SURFACE` when the repo serves nothing to drive, `NO_APP_LOGIC_CHANGE` (detail opening with the PR's merge-base commit) when every changed path is docs, tests, lint or CI config, else `NOT_APPLICABLE` naming why no browser run reaches the change. Never `NO_PR` — the guardrail refutes it on a PR. Then run the foreground command the walkthrough gate's block names until it prints `{"settled":true}`. Left unsettled, the walkthrough check sits red with nothing in this watch able to clear it, and the PR can merge that way.
|
|
65
|
+
|
|
64
66
|
Otherwise resolve the context per that file: reuse an existing one (gated by `autoReuseValidationContext`), else one `AskUserQuestion` (strategy, local URL, backend, project, credentials, re-auth) with detected values as defaults; record Step 3's verified working tree as `Working tree`. The user may pick `skip`/`unit-only` or decline the prompt outright — either way, seed poll-only and continue. Never abort the watcher over validation.
|
|
65
67
|
|
|
66
68
|
Capture any resolved fields for Step 7. Do **not** run E2E now — the first watcher tick that dispatches `/muggle-do` does that.
|
|
@@ -135,7 +135,7 @@ It prints one line, `VERDICT=<verdict>`. A non-zero exit is a broken install: re
|
|
|
135
135
|
|
|
136
136
|
1. A rebase is due and `catch_up_rebases >= maxCatchUpRebases` (the cap from the user's preferences; the `rebase_budget_exhausted` flag is not consulted) → `block-rebase-budget-exhausted`.
|
|
137
137
|
2. A rebase is due, `conflict_resolve_attempts[rebase_key] < 2`, and `rebase_key` ∉ `conflict_escalated_keys` → `dispatch-rebase`. With the per-key budget spent — `conflict_resolve_attempts[rebase_key] >= 2` or `rebase_key` ∈ `conflict_escalated_keys` — it falls through to CI.
|
|
138
|
-
3. CI is `red`, `ci_fix_attempts[head_sha] < 3`, and `head_sha` ∉ `ci_escalated_shas` → `dispatch-fix-ci`. Red with `ci_fix_attempts[head_sha] >= 3` or `head_sha` ∈ `ci_escalated_shas` dispatches nothing — the fix budget is spent and the watcher does not re-dispatch. A check still pending (`bucket == "pending"`) never dispatches: the tick idles until CI settles, and a green head idles too.
|
|
138
|
+
3. CI is `red`, `ci_fix_attempts[head_sha] < 3`, and `head_sha` ∉ `ci_escalated_shas` → `dispatch-fix-ci`. Red with `ci_fix_attempts[head_sha] >= 3` or `head_sha` ∈ `ci_escalated_shas` dispatches nothing — the fix budget is spent and the watcher does not re-dispatch; it holds as `block-ci-escalated` (Step 4) even when fix-ci never wrote the escalation, because idling there leaves a red PR with nobody told. A check still pending (`bucket == "pending"`) never dispatches: the tick idles until CI settles, and a green head idles too.
|
|
139
139
|
4. Otherwise `block-conflict-escalated`, `block-ci-escalated`, or `block-reviews-escalated` when the matching escalation stands, else `idle`.
|
|
140
140
|
|
|
141
141
|
**An escalation counts only with an attempt behind it.** An entry in `conflict_escalated_keys` suppresses a rebase only when `conflict_resolve_attempts[rebase_key] >= 1`, and an entry in `ci_escalated_shas` suppresses fix-ci only when `ci_fix_attempts[head_sha] >= 1`: a cycle ran and gave up. An entry with no attempt is ignored, and the work dispatches. Those fields are the executor's record of what its cycles did — never write an escalation entry or an attempt counter by hand.
|
|
@@ -172,7 +172,7 @@ Act on the verdict:
|
|
|
172
172
|
Any tick that did not dispatch. The Step 6 verdict says **why** it idled. It is **blocked pending a human** on a `block-*` verdict, a durable block only the user can clear:
|
|
173
173
|
|
|
174
174
|
- `block-conflict-escalated` — a rebase `/muggle-do` attempted and gave up on (a semantic conflict, or `autoResolveConflicts=never`), reason `conflict_escalated`. This block clears on its own when the base moves: the new `base_tip_sha` yields a key the set does not contain, and the branch re-arms for a fresh rebase attempt;
|
|
175
|
-
- `block-ci-escalated` — CI the fix-ci stage
|
|
175
|
+
- `block-ci-escalated` — CI the fix-ci stage gave up on, or a red head that spent all 3 fix-ci attempts, reason `ci_escalated`;
|
|
176
176
|
- `block-reviews-escalated` — an ambiguous review in `last_seen.escalated_review_ids` awaiting the user's direction, with the actionable set empty, reason `reviews_escalated`;
|
|
177
177
|
- `block-rebase-budget-exhausted` — reason `rebase_budget_exhausted` (Step 6).
|
|
178
178
|
|
|
@@ -217,7 +217,7 @@ The watch loop's comparison floor — plain `KEY=VALUE` lines, one file per slot
|
|
|
217
217
|
- `REV` — highest submitted-review id already handled
|
|
218
218
|
- `COM` — highest thread-comment id already handled
|
|
219
219
|
- `THREADS` — semicolon-joined ids of threads already known unresolved
|
|
220
|
-
- `CIRED` —
|
|
220
|
+
- `CIRED` — the red key `<head_sha>@<red-check-runs>` (each failed check's `name#run-id`, sorted — the last field of [`../../scripts/pr-watch-state.jq`](../../scripts/pr-watch-state.jq)) of a red verdict fix-ci is finished with: `head_sha` ∈ `ci_escalated_shas` with an attempt behind it, or `ci_fix_attempts[sha] >= 3`. Empty when the checks are green, still pending, unseen, or red with fix-ci not finished — the last so the loop reports it. A lone attempt never floors: the count is written before the fix runs, so it proves a cycle started, not that anything on the PR changed. The floor is a red key rather than a monotonic id because the rollup flips green↔red and resets on each push, and it carries run ids because a settled walkthrough comment or a rerun publishes a new run on the **same** head — a red verdict from that run is what fix-ci waits on, so it wakes the loop again ([`arm-watcher.md`](arm-watcher.md)).
|
|
221
221
|
- `REBASED` — the `rebase_key` (`<head_sha>..<base_tip_sha>`) whose behind/conflicting state a rebase cycle already attempted (`conflict_resolve_attempts[key] >= 1`); empty when the branch is current with its base, unseen, or stale with no attempt. Keyed on the pair, not a monotonic id, because staleness depends on both sides — the loop fires once per newly-due pair and re-arms when the head or the base moves. Same key [`contract.md`](contract.md) Steps 5–6 dedup rebase dispatch on; the loop only signals a due rebase, the tick's verdict applies the budgets.
|
|
222
222
|
- `BLOCKED_CIDIGEST` — the head's `ci_digest` at the moment a block was flagged (the same signature `blocked.fingerprint.ci_digest` carries); empty whenever the watch is not blocked. Non-empty arms the loop's blocked-resume probe: while set, the loop wakes on any move of the live digest away from this value, so a block resumes on a green pass, a rerun, or an external deploy check — not only on red. Written by [`blocked-tick.md`](blocked-tick.md) when a block is flagged and cleared to empty when it resumes.
|
|
223
223
|
- `REBASE_HELD` — `1` when the PR has spent its catch-up rebase budget (`catch_up_rebases` has reached [`maxCatchUpRebases`](../muggle-preferences/preference-gates/maxCatchUpRebases.md)), empty otherwise. While set, the loop raises no rebase wake and records no rebase as due however far the base moves: the tick's verdict refuses every rebase until the owner raises the cap, so a wake would only ask for work it turns down. Every arm recomputes it, so a raised cap lifts the hold at the next re-arm.
|
|
@@ -236,17 +236,17 @@ The work the watch last saw due on its PR, for the owning session's turn-end gat
|
|
|
236
236
|
|
|
237
237
|
The file is **empty** when nothing is due. Written by [`../../scripts/pr-watch-arm.sh`](../../scripts/pr-watch-arm.sh) from the drain's read and rewritten by the loop whenever what is due changes; the session never writes it. The follow-through Stop gate ([`../../hooks/README.md`](../../hooks/README.md)) blocks the turn end while a non-empty record in a slot this session owns names work with no matching attempt in `last_seen.json`: a `REBASE_DUE` key with no `conflict_resolve_attempts` entry (under `autoRebase=always`, budget left), or a `CI_RED_HEAD` with no `ci_fix_attempts` entry. A later tick's `prs.json` naming another head supersedes the record.
|
|
238
238
|
|
|
239
|
-
## `watch-reported.env`
|
|
240
|
-
|
|
241
|
-
What the slot's loops already reported to the session — `KEY="value"` lines like the watermark's, written only by the loop, and only on the poll that ends it with a report:
|
|
242
|
-
|
|
243
|
-
- `REV` / `COM` — the newest review and comment ids that poll saw; each one at or below them was reported or already floored.
|
|
244
|
-
- `THREADS` — the threads unresolved at that poll.
|
|
245
|
-
- `CIRED` / `REBASED` — the red
|
|
246
|
-
- `BLOCKED_CIDIGEST` / `BLOCKED_FOR` — the CI digest a blocked-resume wake reported, and the watermark's `BLOCKED_CIDIGEST` it was reported under. It quiets only that block, so the same digest can resume a later one.
|
|
247
|
-
|
|
248
|
-
A loop fires only on what sits above both this file and [`watch-watermark.env`](#watch-watermarkenv). Every report ends a loop, so no floor can live in its memory: without this file, anything the session saw and chose not to act on — a comment needing no reply, a red check under `HOLD WATCH` — would end every fresh loop on its first poll. Read it as the watermark is read, never by `source`.
|
|
249
|
-
|
|
239
|
+
## `watch-reported.env`
|
|
240
|
+
|
|
241
|
+
What the slot's loops already reported to the session — `KEY="value"` lines like the watermark's, written only by the loop, and only on the poll that ends it with a report:
|
|
242
|
+
|
|
243
|
+
- `REV` / `COM` — the newest review and comment ids that poll saw; each one at or below them was reported or already floored.
|
|
244
|
+
- `THREADS` — the threads unresolved at that poll.
|
|
245
|
+
- `CIRED` / `REBASED` — the red key and the rebase key a loop reported.
|
|
246
|
+
- `BLOCKED_CIDIGEST` / `BLOCKED_FOR` — the CI digest a blocked-resume wake reported, and the watermark's `BLOCKED_CIDIGEST` it was reported under. It quiets only that block, so the same digest can resume a later one.
|
|
247
|
+
|
|
248
|
+
A loop fires only on what sits above both this file and [`watch-watermark.env`](#watch-watermarkenv). Every report ends a loop, so no floor can live in its memory: without this file, anything the session saw and chose not to act on — a comment needing no reply, a red check under `HOLD WATCH` — would end every fresh loop on its first poll. Read it as the watermark is read, never by `source`.
|
|
249
|
+
|
|
250
250
|
## `watch.pid`
|
|
251
251
|
|
|
252
252
|
A single line: the process id of the watch loop that currently owns this slot. Written once by the loop itself on start (`echo "$$" > watch.pid`); read by two parties, never rewritten in place:
|