@muggleai/works 5.4.2 → 5.5.1
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/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/README.md +6 -0
- package/dist/plugin/hooks/hooks.json +20 -7
- package/dist/plugin/scripts/guardrail-build-router.sh +17 -3
- package/dist/plugin/scripts/guardrail-e2e-gate.sh +33 -3
- package/dist/plugin/scripts/guardrail-pr-opened.sh +15 -4
- package/dist/plugin/scripts/guardrail-record-tests.sh +19 -6
- package/dist/plugin/scripts/guardrail-report-format.sh +17 -3
- package/dist/plugin/scripts/reconcile-stale-watchers.sh +59 -0
- package/dist/plugin/skills/CLAUDE.md +6 -0
- package/dist/plugin/skills/_shared/dev-loop/run.md +1 -1
- package/dist/plugin/skills/_shared/post-merge-cleanup.md +2 -3
- package/dist/plugin/skills/_shared/rebase-before-e2e.md +1 -1
- package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -1
- package/dist/plugin/skills/_shared/resolve-rebase-conflicts.md +9 -33
- package/dist/plugin/skills/_shared/session-state-writes.md +2 -2
- package/dist/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
- package/dist/plugin/skills/_shared/telemetry-events/pr-followup-tick.md +2 -0
- package/dist/plugin/skills/_shared/use-worktrees.md +0 -1
- package/dist/plugin/skills/_shared/vcs/github/verify-working-tree.md +1 -1
- package/dist/plugin/skills/_shared/verify-or-rollback-gate.md +39 -0
- package/dist/plugin/skills/do/address-reviews.md +2 -11
- package/dist/plugin/skills/do/fix-ci.md +3 -2
- package/dist/plugin/skills/do/resolve-conflicts.md +7 -13
- package/dist/plugin/skills/do/respawn-watcher.md +27 -0
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +7 -1
- package/dist/plugin/skills/muggle-pr-followup/auto-track.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/blocked-tick.md +38 -0
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
- package/dist/plugin/skills/muggle-pr-followup/cancel-cron.md +6 -5
- package/dist/plugin/skills/muggle-pr-followup/contract.md +29 -7
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +89 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/blocked-reminder.md +30 -0
- package/dist/plugin/skills/muggle-pr-followup/output-templates/watcher-log.md +11 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates.md +2 -1
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +38 -5
- package/dist/plugin/skills/muggle-pr-followup/record-cron-id.md +12 -0
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +38 -4
- package/dist/plugin/skills/skill-deps.config.json +8 -0
- package/dist/release-manifest.json +4 -4
- package/package.json +7 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/hooks/README.md +6 -0
- package/plugin/hooks/hooks.json +20 -7
- package/plugin/scripts/guardrail-build-router.sh +17 -3
- package/plugin/scripts/guardrail-e2e-gate.sh +33 -3
- package/plugin/scripts/guardrail-pr-opened.sh +15 -4
- package/plugin/scripts/guardrail-record-tests.sh +19 -6
- package/plugin/scripts/guardrail-report-format.sh +17 -3
- package/plugin/scripts/reconcile-stale-watchers.sh +59 -0
- package/plugin/skills/CLAUDE.md +6 -0
- package/plugin/skills/_shared/dev-loop/run.md +1 -1
- package/plugin/skills/_shared/post-merge-cleanup.md +2 -3
- package/plugin/skills/_shared/rebase-before-e2e.md +1 -1
- package/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -1
- package/plugin/skills/_shared/resolve-rebase-conflicts.md +9 -33
- package/plugin/skills/_shared/session-state-writes.md +2 -2
- package/plugin/skills/_shared/telemetry-events/muggle-do-cycle.md +1 -1
- package/plugin/skills/_shared/telemetry-events/pr-followup-tick.md +2 -0
- package/plugin/skills/_shared/use-worktrees.md +0 -1
- package/plugin/skills/_shared/vcs/github/verify-working-tree.md +1 -1
- package/plugin/skills/_shared/verify-or-rollback-gate.md +39 -0
- package/plugin/skills/do/address-reviews.md +2 -11
- package/plugin/skills/do/fix-ci.md +3 -2
- package/plugin/skills/do/resolve-conflicts.md +7 -13
- package/plugin/skills/do/respawn-watcher.md +27 -0
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +5 -3
- package/plugin/skills/muggle-pr-followup/SKILL.md +7 -1
- package/plugin/skills/muggle-pr-followup/auto-track.md +1 -1
- package/plugin/skills/muggle-pr-followup/blocked-tick.md +38 -0
- package/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
- package/plugin/skills/muggle-pr-followup/cancel-cron.md +6 -5
- package/plugin/skills/muggle-pr-followup/contract.md +29 -7
- package/plugin/skills/muggle-pr-followup/evals/evals.json +89 -0
- package/plugin/skills/muggle-pr-followup/output-templates/blocked-reminder.md +30 -0
- package/plugin/skills/muggle-pr-followup/output-templates/watcher-log.md +11 -1
- package/plugin/skills/muggle-pr-followup/output-templates.md +2 -1
- package/plugin/skills/muggle-pr-followup/reconcile.md +38 -5
- package/plugin/skills/muggle-pr-followup/record-cron-id.md +12 -0
- package/plugin/skills/muggle-pr-followup/state-schemas.md +38 -4
- package/plugin/skills/skill-deps.config.json +8 -0
|
@@ -14,4 +14,4 @@ Accept any remote URL form for `<owner>/<repo>` (with or without trailing `.git`
|
|
|
14
14
|
- `git@github.com:<owner>/<repo>`
|
|
15
15
|
- `ssh://git@github.com/<owner>/<repo>`
|
|
16
16
|
|
|
17
|
-
Any mismatch →
|
|
17
|
+
Any mismatch → abort; the calling skill reports the wrong checkout to the user with its own message.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Verify-or-Rollback Gate
|
|
2
|
+
|
|
3
|
+
The mandatory gate a caller runs after an operation has mutated the working tree and that tree must be proven safe before anything ships — e.g. a resolved rebase onto `origin/{default}`. It verifies the changed surface; on any failure it restores the branch to its pre-mutation state and escalates. The caller hands off the inputs below; this file names no caller — the dependency runs one way.
|
|
4
|
+
|
|
5
|
+
Contract: never let a mutated tree ship without passing verification, and always keep the branch restorable to its pre-mutation state.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
- `pre_rebase_sha` — branch HEAD the caller captured **before** the mutating operation; the rollback point.
|
|
10
|
+
- Session context: slug, PR url/number, and the persisted validation strategy (for the E2E step).
|
|
11
|
+
|
|
12
|
+
## Procedure
|
|
13
|
+
|
|
14
|
+
### Step 1 — Verify (mandatory)
|
|
15
|
+
|
|
16
|
+
Each must pass, in order:
|
|
17
|
+
|
|
18
|
+
1. **Build** — typecheck + lint on the changed surface, run via the caller's build step.
|
|
19
|
+
2. **Unit suite** — the caller's unit run; record PASS.
|
|
20
|
+
3. **E2E** — the caller's E2E step under the persisted [`autoE2ETest`](../muggle-preferences/preference-gates/autoE2ETest.md) strategy. A poll-only session with no validation context reports `SKIPPED`, same as the normal cycle.
|
|
21
|
+
|
|
22
|
+
### Step 2 — Pass → proceed
|
|
23
|
+
|
|
24
|
+
Return success. The caller resumes the normal flow; the push happens downstream, so a mutated tree ships only after it has verified.
|
|
25
|
+
|
|
26
|
+
### Step 3 — Fail → restore + escalate
|
|
27
|
+
|
|
28
|
+
On any verify failure — or when the caller reports the preceding operation could not complete:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git reset --hard <pre_rebase_sha>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The branch is now byte-for-byte its pre-mutation state. Emit one terminal escalation — the caller's escalation message — naming the failing step, plus the `muggle-do:escalation` event with `kind: "rebase-conflict"` ([`telemetry-events/muggle-do-escalation.md`](telemetry-events/muggle-do-escalation.md)). Do not push.
|
|
35
|
+
|
|
36
|
+
## Invariants
|
|
37
|
+
|
|
38
|
+
- A push never follows a verify failure.
|
|
39
|
+
- The branch is always restorable to `pre_rebase_sha`.
|
|
@@ -29,7 +29,7 @@ Read from `~/.muggle-ai/muggle-do/sessions/<slug>/`:
|
|
|
29
29
|
|
|
30
30
|
### Step 0 — Track the default branch
|
|
31
31
|
|
|
32
|
-
Before assembling work, rebase onto the latest default branch so the cycle addresses reviews against current master, not a stale base. Run [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before-e2e.md) — gated by [`autoRebase`](../muggle-preferences/preference-gates/autoRebase.md), fires only when `behind > 0`. Conflict handling follows [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md): the default `never` aborts and escalates (`kind: "rebase-conflict"`); `always` resolves behind the verify-or-rollback gate. If the rebase escalates, stop the cycle — do not push.
|
|
32
|
+
Before assembling work, rebase onto the latest default branch so the cycle addresses reviews against current master, not a stale base. Run [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before-e2e.md) — gated by [`autoRebase`](../muggle-preferences/preference-gates/autoRebase.md), fires only when `behind > 0`. Conflict handling follows [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md): the default `never` aborts and escalates (`kind: "rebase-conflict"`); `always` resolves behind the verify-or-rollback gate. If the rebase escalates, stop the cycle — do not push, do not address reviews — but **skip to Step 6 to respawn the watcher**. Escalating the rebase does not end the PR; the poller must keep running (it will remind on the `conflict_escalated` block), so skipping respawn here is exactly the silent-stop bug [`respawn-watcher.md`](respawn-watcher.md) exists to prevent.
|
|
33
33
|
|
|
34
34
|
### Step 1 — Assemble the work set
|
|
35
35
|
|
|
@@ -115,16 +115,7 @@ Invoke [`resolve-reminder.md`](resolve-reminder.md) once, regardless of whether
|
|
|
115
115
|
|
|
116
116
|
### Step 6 — Respawn the watcher
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
1. Write `result.md` per [`../muggle-pr-followup/state-schemas.md`](../muggle-pr-followup/state-schemas.md#resultmd).
|
|
121
|
-
2. Do **not** respawn the watcher.
|
|
122
|
-
|
|
123
|
-
Otherwise, dispatch the next watcher as the last action of this turn. The watcher cancelled its own cron when it dispatched this cycle ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md) Step 4), so this restart is the single live watcher — never a duplicate:
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
/loop 1m /muggle:muggle-pr-followup <slug> <n>
|
|
127
|
-
```
|
|
118
|
+
Respawn per [`respawn-watcher.md`](respawn-watcher.md): refresh PR state, finalize (write `result.md`, no respawn) if the PR is now merged or closed, otherwise restart the single live watcher as the turn's last action. **Every** exit path in this procedure lands here — the actionable-and-pushed happy path, the ambiguous-only branch (Step 3), the design-adjustment escalation, and the Step 0 rebase-escalation — so an open PR is never left un-watched.
|
|
128
119
|
|
|
129
120
|
### Step 7 — Telemetry
|
|
130
121
|
|
|
@@ -40,7 +40,7 @@ Commit per the `fix(ci): <check> — <what>` convention ([`../_shared/pr-followu
|
|
|
40
40
|
### Step 5 — Update state + respawn
|
|
41
41
|
|
|
42
42
|
- Increment `last_seen.ci_fix_attempts[red_sha]` — a whole-file rewrite (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md), never the Edit tool.
|
|
43
|
-
- Respawn the watcher
|
|
43
|
+
- Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md). CI on the new SHA is the verify loop — a still-red SHA returns as a fresh dispatch, bounded by the per-SHA fix budget (Step 6).
|
|
44
44
|
|
|
45
45
|
### Step 6 — Escalate (budget spent or out of scope)
|
|
46
46
|
|
|
@@ -48,7 +48,8 @@ When the failing checks are all out of scope, or `ci_fix_attempts[red_sha]` has
|
|
|
48
48
|
|
|
49
49
|
1. Add `red_sha` to `last_seen.ci_escalated_shas` so the SHA is not re-fixed.
|
|
50
50
|
2. Emit one terminal message naming the unresolved checks.
|
|
51
|
-
3. Emit the cycle event with `outcome: "ci-escalated"` (Step 7).
|
|
51
|
+
3. Emit the cycle event with `outcome: "ci-escalated"` (Step 7).
|
|
52
|
+
4. **Respawn the watcher** per [`respawn-watcher.md`](respawn-watcher.md). Escalation stops fix-ci from re-dispatching this SHA — but the poller must keep running: it reminds on the `ci_escalated` block and catches a new push, a new review, or the user's own fix. Skipping respawn here is the silent-stop bug the helper exists to prevent.
|
|
52
53
|
|
|
53
54
|
### Step 7 — Telemetry
|
|
54
55
|
|
|
@@ -14,13 +14,13 @@ Rebase a PR's branch onto its base — whether it's merely **behind** (out of da
|
|
|
14
14
|
|
|
15
15
|
## Inputs from disk
|
|
16
16
|
|
|
17
|
-
From `~/.muggle-ai/muggle-do/sessions/<slug>/`: `prs.json` (PR + branch + `head_sha`), `last_seen.json` (`conflict_resolve_attempts`, `
|
|
17
|
+
From `~/.muggle-ai/muggle-do/sessions/<slug>/`: `prs.json` (PR + branch + `head_sha`), `last_seen.json` (`conflict_resolve_attempts`, `conflict_escalated_keys`, `pushed_shas`), `state.md` (worktree path, validation strategy, base branch).
|
|
18
18
|
|
|
19
19
|
## Procedure
|
|
20
20
|
|
|
21
21
|
### Step 1 — Re-attach
|
|
22
22
|
|
|
23
|
-
Materialize the PR branch in its worktree per [`../_shared/pr-branch-worktree.md`](../_shared/pr-branch-worktree.md) (or use `state.md`'s `worktreePath`). Capture `rebase_sha = prs.json[0].head_sha` and the base branch (`baseRefName` from [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md)).
|
|
23
|
+
Materialize the PR branch in its worktree per [`../_shared/pr-branch-worktree.md`](../_shared/pr-branch-worktree.md) (or use `state.md`'s `worktreePath`). Capture `rebase_sha = prs.json[0].head_sha` and the base branch (`baseRefName` from [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md)). Capture the base tip too — `.base_commit.sha` from that recipe's `compare` call — and form `rebase_key = "<rebase_sha>..<base_tip_sha>"`. Both budget fields below are keyed on that pair, matching the watcher's Step 5; writing a bare SHA instead leaves an entry the watcher ignores, and the rebase re-dispatches forever.
|
|
24
24
|
|
|
25
25
|
### Step 2 — Rebase onto base (resolve conflicts if any)
|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ Run the rebase from [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before
|
|
|
29
29
|
- **Clean replay** — a behind-only branch (and any rebase that hits no conflicts) replays without intervention. Proceed to Step 3.
|
|
30
30
|
- **Conflicts** — handle per [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
|
|
31
31
|
- `never` → abort and escalate per Step 5 (`kind: "rebase-conflict"`). The watcher keeps polling; the user resolves on GitHub.
|
|
32
|
-
- `always` → resolve
|
|
32
|
+
- `always` → resolve via [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md); Steps 3 and 5 below are this mode's instance of the [`../_shared/verify-or-rollback-gate.md`](../_shared/verify-or-rollback-gate.md).
|
|
33
33
|
|
|
34
34
|
### Step 3 — Verify the resolution
|
|
35
35
|
|
|
@@ -37,21 +37,15 @@ Build (typecheck + lint on the changed surface) + unit suite must pass. Run E2E
|
|
|
37
37
|
|
|
38
38
|
### Step 4 — Force-push + respawn
|
|
39
39
|
|
|
40
|
-
Push with `--force-with-lease` (the rebase rewrote history). Append the new SHA to `last_seen.pushed_shas`; increment `last_seen.conflict_resolve_attempts[
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
/loop 1m /muggle:muggle-pr-followup <slug> <n>
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
The watcher cancelled its own cron when it dispatched this cycle ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md) Step 5b), so this restart is the single live watcher. Its next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
|
|
40
|
+
Push with `--force-with-lease` (the rebase rewrote history). Append the new SHA to `last_seen.pushed_shas`; increment `last_seen.conflict_resolve_attempts[rebase_key]` — both whole-file rewrites (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md), never the Edit tool. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md). Its next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
|
|
47
41
|
|
|
48
42
|
### Step 5 — Escalate (can't resolve / budget spent)
|
|
49
43
|
|
|
50
|
-
When `autoResolveConflicts=never`, the resolution failed verification, or `conflict_resolve_attempts[
|
|
44
|
+
When `autoResolveConflicts=never`, the resolution failed verification, or `conflict_resolve_attempts[rebase_key]` has reached 2:
|
|
51
45
|
|
|
52
|
-
1. Add `
|
|
46
|
+
1. Add `rebase_key` to `last_seen.conflict_escalated_keys` so the watcher does not re-dispatch this head against this base. If the base later moves, the key changes and the branch re-arms on its own — a conflict the user resolved upstream stops being this watcher's dead end.
|
|
53
47
|
2. Emit one terminal escalation naming the PR and the conflicting files (or the failing verification, for a behind-only rebase that didn't verify).
|
|
54
|
-
3. Respawn the watcher (
|
|
48
|
+
3. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md) — it keeps polling for the user's manual resolution or any new reviews.
|
|
55
49
|
|
|
56
50
|
### Step 6 — Telemetry
|
|
57
51
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Respawn the watcher
|
|
2
|
+
|
|
3
|
+
The single, guaranteed watcher restart shared by every watcher-dispatched `/muggle-do` mode — address-reviews, fix-ci, and rebase. The watcher cancels its own cron the moment it dispatches a cycle ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md) Steps 4 / 5 / 5b), so for the duration of the cycle **no cron is armed for this slot**. Respawning is what arms the next one. This is a runtime dispatch of the watcher's slash command, not a doc dependency on it — allowed per the one-way rule in [`../CLAUDE.md`](../CLAUDE.md).
|
|
4
|
+
|
|
5
|
+
## The guarantee
|
|
6
|
+
|
|
7
|
+
Respawn is the **last action on every exit path that leaves the PR open** — the happy path *and* every escalation, early-exit, or abort. Since the dispatch already cancelled the cron, any open-PR exit that skips respawn leaves the slot with no cron and no next tick: the poller stops silently and stays stopped until a session restart or a reconcile sweep re-arms it. That silent stop is the exact failure this file exists to prevent, so treat "did I respawn on this branch?" as a checklist item on every exit, not only the success case.
|
|
8
|
+
|
|
9
|
+
The one exception is a **terminal** PR (merged or closed): a terminal PR needs no watcher, so the terminal branch finalizes instead of respawning.
|
|
10
|
+
|
|
11
|
+
## Procedure
|
|
12
|
+
|
|
13
|
+
Run as the final action of the turn:
|
|
14
|
+
|
|
15
|
+
1. **Refresh PR state** — `github` per [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md), `gitlab` per [`../_shared/vcs/gitlab/mr-metadata.md`](../_shared/vcs/gitlab/mr-metadata.md).
|
|
16
|
+
2. **If merged or closed** → write `result.md` per [`../muggle-pr-followup/state-schemas.md`](../muggle-pr-followup/state-schemas.md#resultmd) and **do not respawn**. The PR is terminal; the watcher's job is done.
|
|
17
|
+
3. **Otherwise** → dispatch, verbatim, as the turn's last action:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/loop 1m /muggle:muggle-pr-followup <slug> <n>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Exactly one cron results — the dispatch already cancelled the prior one, so this is never a duplicate. The next tick self-records the new cron id ([`../muggle-pr-followup/record-cron-id.md`](../muggle-pr-followup/record-cron-id.md)).
|
|
24
|
+
|
|
25
|
+
## Recovery net
|
|
26
|
+
|
|
27
|
+
This helper guarantees respawn on the paths that reach it, but a cycle that crashes or errors out *before* its exit path can still drop the respawn. [`../muggle-pr-followup/reconcile.md`](../muggle-pr-followup/reconcile.md) is the backstop: it re-arms any open slot whose watcher went silent (no tick within the staleness window), recovering a dropped respawn on the next sweep.
|
|
@@ -7,10 +7,12 @@ This folder holds the watcher loop that drives one PR toward merge-ready. The wa
|
|
|
7
7
|
- [`SKILL.md`](SKILL.md) — public entry. Routing between bootstrap (URL input), tick (slug + PR number), and auto-track (no args). Read first.
|
|
8
8
|
- [`auto-track.md`](auto-track.md) — the no-args procedure: discovers PRs pushed this session (any repo) and seeds one poll-only watcher each. Seeds no E2E context — the watcher only watches.
|
|
9
9
|
- [`bootstrap.md`](bootstrap.md) — the bootstrap procedure (resolves the validation context once when the PR has a testable surface — else seeds poll-only like auto-track — then dispatches the first watcher).
|
|
10
|
-
- [`contract.md`](contract.md) — the watcher per-tick procedure (poll → dispatch → exit).
|
|
10
|
+
- [`contract.md`](contract.md) — the watcher per-tick procedure (poll → dispatch → exit; on a human-blocked PR it reminds the owner each tick at the normal `1m` cadence, resuming evaluation on external change).
|
|
11
|
+
- [`blocked-tick.md`](blocked-tick.md) — the conditional blocked-path detail: fingerprint, flag-and-remind (Step 7), and remind-or-resume (Step 2.5), all at the normal `1m` cadence. Referenced by `contract.md`; runs only while `last_seen.blocked` is set.
|
|
11
12
|
- [`finalize.md`](finalize.md) — shared termination sequence for a terminal PR (mark terminal, `result.md`, log/telemetry, unschedule cron, post-merge cleanup handoff). Called by `contract.md` and `reconcile.md`.
|
|
12
|
-
- [`cancel-cron.md`](cancel-cron.md) —
|
|
13
|
-
- [`
|
|
13
|
+
- [`cancel-cron.md`](cancel-cron.md) — stops this watcher's cron, recorded-id-first (survives `CronList` going blind) with a `CronList`-match fallback, plus the tool-call-not-shell guard. Referenced by `contract.md` and `finalize.md`.
|
|
14
|
+
- [`record-cron-id.md`](record-cron-id.md) — the per-tick self-record that keeps this slot's cron id in `cron.json` deletable after a compaction blinds `CronList`. Referenced by `contract.md` Step 0.
|
|
15
|
+
- [`reconcile.md`](reconcile.md) — sweep that finalizes slots whose PR went terminal while polling lapsed, deletes orphaned crons, and re-arms open slots whose watcher stopped silently (dropped respawn); runs on demand, at the top of auto-track, and nudged by a session-start hook.
|
|
14
16
|
- [`state-schemas.md`](state-schemas.md) — canonical JSON shapes of session state files.
|
|
15
17
|
- [`output-templates.md`](output-templates.md) — TOC of message templates; per-group files in `output-templates/`.
|
|
16
18
|
|
|
@@ -14,6 +14,10 @@ A watcher that babysits one open PR toward **merge-ready** — review threads ad
|
|
|
14
14
|
|
|
15
15
|
**Per-PR isolation.** One watcher per PR. Multi-PR work runs N independent watchers.
|
|
16
16
|
|
|
17
|
+
**Active reminders when blocked pending a human.** When a PR can't progress without the user — an escalated rebase/CI budget spent, or an ambiguous review awaiting direction — the watcher keeps its `1m` poll and turns each blocked tick into a **one-line reminder**: the pending act plus a reference back to the decision context, nudging the owner every poll until they act ([`contract.md`](contract.md) Steps 2.5, 7). The block reminds but never stops or slows: each tick stays cheap (a fingerprint check, one line out), and the block clears the instant a push, review, or CI/deploy state moves.
|
|
18
|
+
|
|
19
|
+
**Cron lifecycle.** Each tick records its `/loop` cron id to `cron.json` while `CronList` can still see it ([`record-cron-id.md`](record-cron-id.md)), so teardown can delete the cron by id after a session continue / compaction blinds `CronList` to it. Reconcile ([`reconcile.md`](reconcile.md)) sweeps crons whose PR is terminal or whose slot is gone, and re-arms an open slot whose watcher stopped silently.
|
|
20
|
+
|
|
17
21
|
## Routing
|
|
18
22
|
|
|
19
23
|
The skill recognizes its mode by inspecting `$ARGUMENTS` and falling back to on-disk state. It never runs procedure inline — it identifies the mode and routes to the appropriate procedure file.
|
|
@@ -29,7 +33,9 @@ The skill recognizes its mode by inspecting `$ARGUMENTS` and falling back to on-
|
|
|
29
33
|
| `help` / `?` | — | **help:** list active loops per [`output-templates/help.md`](output-templates/help.md) |
|
|
30
34
|
| `reconcile` / `sweep` (optional `<slug>`) | — | **reconcile** → [`reconcile.md`](reconcile.md) |
|
|
31
35
|
|
|
32
|
-
Auto-track runs **reconcile** first, so a no-arg invocation also finalizes any slot whose PR merged or closed while its watcher was down (expired cron, ended session). Reconcile never
|
|
36
|
+
Auto-track runs **reconcile** first, so a no-arg invocation also finalizes any slot whose PR merged or closed while its watcher was down (expired cron, ended session) and re-arms any open slot whose watcher stopped silently (a dropped respawn). Reconcile recovers a watcher that was already running; it never seeds a first watcher for a PR — that is auto-track's / bootstrap's job.
|
|
37
|
+
|
|
38
|
+
**Reconcile also runs at session start** — a `SessionStart` hook ([`../../hooks/README.md`](../../hooks/README.md)) surfaces the sweep when open slots exist, catching a watcher that died with its session (end, or 7-day `/loop` expiry) before a manual sweep would. See [`reconcile.md`](reconcile.md#triggers).
|
|
33
39
|
|
|
34
40
|
Bootstrap accepts three optional trailing flags:
|
|
35
41
|
|
|
@@ -51,7 +51,7 @@ For each PR URL in the track list, run the [`bootstrap.md`](bootstrap.md) proced
|
|
|
51
51
|
- **Existing slot → skip silently** (never the slot-conflict abort); add it to the *skipped* list.
|
|
52
52
|
- **`caller = "auto-track"`** in the bootstrap telemetry event.
|
|
53
53
|
|
|
54
|
-
Everything else is unchanged: URL parse, metadata + terminal-PR abort, slug, `lastBodyReviewId` 0 (line-comment threads are picked up live from thread state; body-only reviews from id 0), and the `prs.json`/`last_seen.json`/`state.md` writes minus the pre-flight block.
|
|
54
|
+
Everything else is unchanged: URL parse, metadata + terminal-PR abort, slug, `lastBodyReviewId` 0 (line-comment threads are picked up live from thread state; body-only reviews from id 0), and the `prs.json`/`last_seen.json`/`cron.json`/`state.md` writes minus the pre-flight block.
|
|
55
55
|
|
|
56
56
|
### Step 5 — Print the summary
|
|
57
57
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Blocked-tick procedure
|
|
2
|
+
|
|
3
|
+
The watcher's conditional path for a PR **blocked pending a human** — a durable block only the user can clear (an escalated rebase or CI budget spent, or an ambiguous review awaiting direction). Entered from [`contract.md`](contract.md) Step 7 (flag), then driven each subsequent tick by Step 2.5 (remind-or-resume). None of this runs on a normal tick: when `last_seen.blocked` is absent, the watcher skips straight through.
|
|
4
|
+
|
|
5
|
+
**Governing rule — remind at the normal `1m` cadence, never stop.** The poll stays at `1m` whether or not the PR is blocked — a `1m` tick is cheap (one fingerprint check, one line out) and keeps the owner nudged and an external unblock caught within a minute. The `blocked` state changes no cadence; its only job is (a) the reason-specific one-line reminder each tick and (b) fingerprint-based auto-resume. No cadence swap, no separate `reminded` flag — while `blocked` is set, the watcher reminds every tick by definition.
|
|
6
|
+
|
|
7
|
+
## The fingerprint
|
|
8
|
+
|
|
9
|
+
The external state a block is waiting on. Recompute from live state each blocked tick:
|
|
10
|
+
|
|
11
|
+
- `head_sha` — from the tick's [`contract.md`](contract.md) Step 1 refresh.
|
|
12
|
+
- `latest_review_id` — `max(id)` over submitted reviews per [`../_shared/vcs/github/submitted-reviews.md`](../_shared/vcs/github/submitted-reviews.md) (`0` if none).
|
|
13
|
+
- `ci_digest` — the CI rollup digest for `head_sha` per [`../_shared/vcs/github/pr-checks.md`](../_shared/vcs/github/pr-checks.md): the bucket plus each check's name and conclusion, sorted into one stable string.
|
|
14
|
+
|
|
15
|
+
Any component moving means the block may have cleared — a new push (`head_sha`, which also clears the per-SHA escalation sets), a new review (`latest_review_id`), or a CI/deploy state change (`ci_digest`, which is how an external staging deploy is caught).
|
|
16
|
+
|
|
17
|
+
## Flag the block (from Step 7)
|
|
18
|
+
|
|
19
|
+
When an idle tick is a durable human-block and `last_seen.blocked` is not already set:
|
|
20
|
+
|
|
21
|
+
1. Increment `last_seen.idle_tick_count`.
|
|
22
|
+
2. Write `last_seen.blocked = { reason, since: <now>, fingerprint }` (reuse the `latest_review_id` / `ci_digest` already fetched this tick).
|
|
23
|
+
3. **Remind the owner** — emit the one-line reminder per [`output-templates/blocked-reminder.md`](output-templates/blocked-reminder.md): the pending act plus a reference back to the decision context.
|
|
24
|
+
4. Append a `blocked reason=<reason>` line to `followup.log` per [`output-templates/watcher-log.md`](output-templates/watcher-log.md); emit a `tick` event with `idle: true`, `blocked: true`, and the same other fields as a transient idle. Exit. The `1m` cron is unchanged — no swap.
|
|
25
|
+
|
|
26
|
+
## Remind or resume (the Step 2.5 gate)
|
|
27
|
+
|
|
28
|
+
Every subsequent tick while `last_seen.blocked` is present: recompute the fingerprint and compare to `last_seen.blocked.fingerprint`.
|
|
29
|
+
|
|
30
|
+
- **Unchanged** → still blocked. Re-emit the one-line reminder per [`output-templates/blocked-reminder.md`](output-templates/blocked-reminder.md). Increment `last_seen.idle_tick_count`, append a `blocked reason=<reason>` line to `followup.log`, emit a `tick` event with `idle: true`, `blocked: true`. Exit. The `1m` cron is unchanged.
|
|
31
|
+
- **Changed** → clear `last_seen.blocked` and **fall through to [`contract.md`](contract.md) Step 3** to re-evaluate against the moved state this same tick. The cron is already `1m`, so no swap is needed: if Step 3–6 dispatches, that cancels the `1m` cron and `/muggle-do` respawns `1m` (normal single-thread); if it idles transient, the `1m` cron is already correct; if it idles back into the block, Step 7 re-flags.
|
|
32
|
+
|
|
33
|
+
## Invariants
|
|
34
|
+
|
|
35
|
+
- Cadence is `1m` whether blocked or active — the block never changes the poll interval. There is no cadence swap, so a blocked slot is never left cron-less by one.
|
|
36
|
+
- The poll never stops — a blocked tick keeps firing and reminding; only a terminal PR or an explicit teardown removes the cron.
|
|
37
|
+
- Every blocked tick emits exactly one owner reminder (implied by `blocked: true`, no separate flag) and no PR-side post.
|
|
38
|
+
- The block clears the instant any fingerprint component moves; an external unblock is caught within one `1m` tick.
|
|
@@ -71,7 +71,9 @@ Write under `~/.muggle-ai/muggle-do/sessions/<slug>/`:
|
|
|
71
71
|
|
|
72
72
|
**`prs.json`** — see [`state-schemas.md`](state-schemas.md#prsjson). One entry, `state` = `"open"`, `head_sha` from Step 2's `headRefOid`.
|
|
73
73
|
|
|
74
|
-
**`last_seen.json`** — see [`state-schemas.md`](state-schemas.md#last_seenjson). One key (`"<owner>/<repo>#<n>"`), `lastBodyReviewId` from Step 6, `last_pushed_sha: null`, `idle_tick_count: 0`, `cycles_completed: 0`, `escalated_review_ids: []`, `pushed_shas: []`.
|
|
74
|
+
**`last_seen.json`** — see [`state-schemas.md`](state-schemas.md#last_seenjson). One key (`"<owner>/<repo>#<n>"`), `lastBodyReviewId` from Step 6, `last_pushed_sha: null`, `idle_tick_count: 0`, `cycles_completed: 0`, `escalated_review_ids: []`, `pushed_shas: []`. Omit `blocked` — the watcher starts unblocked.
|
|
75
|
+
|
|
76
|
+
**`cron.json`** — see [`state-schemas.md`](state-schemas.md#cronjson). `cron_id: null` (Step 8 dispatches `/loop` as the last action, so the id isn't observable yet — the first tick self-records it per [`record-cron-id.md`](record-cron-id.md)), `command: "/muggle:muggle-pr-followup <slug> <n>"`, `interval: "1m"`, `recorded_at: <now>`.
|
|
75
77
|
|
|
76
78
|
**`state.md`** — see [`state-schemas.md`](state-schemas.md#statemd). `Bootstrapped from URL: yes`. Cache the loop-user login. If Step 6.5 resolved a validation context, append the `## Pre-flight answers` block with its fields, per [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md#persisted-fields). If it seeded poll-only, write **no** such block — a missing block is a clean E2E skip.
|
|
77
79
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Cancel the watcher's cron
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deletes this slot's watcher cron, and nothing more — the caller owns whatever comes next (respawn or terminal teardown). Used by the stale-fire guard and terminal unschedule ([`finalize.md`](finalize.md) Step 4) and each single-thread "stop this watcher" before a `/muggle-do` dispatch ([`contract.md`](contract.md) Steps 4–6).
|
|
4
4
|
|
|
5
5
|
> **`CronList` and `CronDelete` are Claude Code tool calls, not shell commands.** Invoke them directly through the tool system. Never wrap them in a Bash/shell call: `bash -c "CronDelete …"` fails with "command not found", which a `2>/dev/null` on the line swallows, so the delete silently no-ops and the per-minute cron keeps firing — every later tick hits the stale-fire guard and re-fires until the 7-day expiry.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
2. Find the job whose command ends with `/muggle:muggle-pr-followup <slug> <n>` — the exact two-arg match for this slot's PR.
|
|
9
|
-
3. Call the `CronDelete` tool with that job's id.
|
|
7
|
+
Two lookups, recorded-id first so the delete still works when `CronList` has gone blind to the cron (survived a session continue / compaction — see [`state-schemas.md`](state-schemas.md#cronjson)):
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
1. **By recorded id.** Read `cron.json` ([`state-schemas.md`](state-schemas.md#cronjson)). If `cron_id` is non-null, call `CronDelete` with it. This is the only handle that survives `CronList` blindness.
|
|
10
|
+
2. **By `CronList` match (fallback).** Call `CronList`, find the job whose command ends with `/muggle:muggle-pr-followup <slug> <n>` — the exact two-arg match for this slot's PR — and `CronDelete` its id. This catches a stale recorded id (a since-respawned cron whose new id no tick has recorded yet) and the case where `cron.json` is absent.
|
|
11
|
+
|
|
12
|
+
Both lookups are no-ops when nothing matches — a manually-run tick, or a cron that already expired. Deleting an already-gone id is harmless. Do **not** delete `cron.json` itself here; a terminal slot keeps it as a record, and reconcile ([`reconcile.md`](reconcile.md)) may still read it to sweep a straggler.
|
|
@@ -29,13 +29,15 @@ Every `increment`/`reset` this procedure applies to `last_seen.json`, and the `p
|
|
|
29
29
|
|
|
30
30
|
## Procedure
|
|
31
31
|
|
|
32
|
-
### Step 0 — Stale-fire guard
|
|
32
|
+
### Step 0 — Stale-fire guard, then record this cron's id
|
|
33
33
|
|
|
34
34
|
If `prs.json[0].state` on disk is already `merged` or `closed`, this slot was finalized by a prior tick and this is a stale (queued) fire — per-minute cron fires enqueued while the session was busy still drain after the cron is cancelled. Defensively cancel any lingering cron for this slug per [`cancel-cron.md`](cancel-cron.md) (no-op if none), append a `stale-tick` line to `followup.log`, and exit. Do not re-fetch or re-finalize.
|
|
35
35
|
|
|
36
|
+
Otherwise, self-record this watcher's cron id per [`record-cron-id.md`](record-cron-id.md) before proceeding. Recording every tick — while `CronList` can still see the cron — is what keeps the id a valid `CronDelete` target after a session continue / compaction blinds `CronList` to it, so teardown ([`finalize.md`](finalize.md), [`reconcile.md`](reconcile.md)) can always kill the orphan.
|
|
37
|
+
|
|
36
38
|
### Step 1 — Refresh PR state
|
|
37
39
|
|
|
38
|
-
Per [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md). Update `prs.json[0].head_sha` and `prs.json[0].state` from the response; keep `mergeable` (conflict signal) for Step 5, and run the recipe's `compare` call to capture `behind_by` (out-of-date signal)
|
|
40
|
+
Per [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md). Update `prs.json[0].head_sha` and `prs.json[0].state` from the response; keep `mergeable` (conflict signal) for Step 5, and run the recipe's `compare` call to capture both `behind_by` (out-of-date signal) and `.base_commit.sha` — the base branch tip, Step 5's `base_tip_sha`.
|
|
39
41
|
|
|
40
42
|
### Step 2 — Termination check
|
|
41
43
|
|
|
@@ -52,6 +54,12 @@ If `state` is `MERGED` or `CLOSED`:
|
|
|
52
54
|
3. Exit. The watcher has unscheduled itself; no future ticks fire for this PR.
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
### Step 2.5 — Blocked-tick gate
|
|
58
|
+
|
|
59
|
+
Only when `last_seen.blocked` is present (the watcher is awaiting the owner on a durable human-block, flagged in Step 7). When absent, skip straight to Step 3.
|
|
60
|
+
|
|
61
|
+
Run the remind-or-resume gate per [`blocked-tick.md`](blocked-tick.md): recompute the fingerprint, re-emit the one-line owner reminder and stay blocked while it holds, or clear the block and fall through to Step 3 the moment it moves. While blocked the watcher keeps the normal `1m` cadence and reminds the owner each tick — the block reminds rather than backs off, and never changes the poll interval.
|
|
62
|
+
|
|
55
63
|
### Step 3 — Compute the actionable set from live thread state
|
|
56
64
|
|
|
57
65
|
The watcher's dispatch trigger is **derived from current provider state**, not a stored review-id cursor — see the [thread-state baseline design](../../../../muggle-ai-brain/architecture/2026-06-06-pr-followup-thread-state-baseline-design.md). Resolve the provider per [`../_shared/vcs/detect-vcs.md`](../_shared/vcs/detect-vcs.md), then:
|
|
@@ -94,7 +102,11 @@ A merge-ready branch is **current with its base** — neither conflicting nor be
|
|
|
94
102
|
|
|
95
103
|
This trigger is **independent of approval and CI state**: an out-of-date branch is rebased whether or not it has been reviewed, approved, or has green checks. The watcher acts on staleness directly — it never waits for an approval to surface it.
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
Rebase dedup is keyed on the **pair** `rebase_key = "<head_sha>..<base_tip_sha>"`, not on the head alone. Whether a branch conflicts is a function of both sides, so a head-only key wedges a PR permanently the first time the base moves: the head cannot change while nobody pushes, so one stale entry suppresses every genuinely new conflict that base movement introduces, forever. Take `base_tip_sha` from the Step 1 compare's `.base_commit.sha` (the base branch tip, which advances when the base does) — **never** `.merge_base_commit.sha`, which does not move when only the base advances and so would never re-arm.
|
|
106
|
+
|
|
107
|
+
Entries written by an older watcher are bare head SHAs with no `..` — ignore them when reading `conflict_escalated_keys`, which re-arms any slot a head-only key had wedged.
|
|
108
|
+
|
|
109
|
+
If a rebase is due **and** `conflict_resolve_attempts[rebase_key] < 2` **and** `rebase_key` ∉ `conflict_escalated_keys` → dispatch and exit:
|
|
98
110
|
|
|
99
111
|
1. Reset `last_seen.idle_tick_count` to 0.
|
|
100
112
|
2. **Stop this watcher (single-thread):** cancel its cron exactly as in Step 4 — `/muggle-do`'s rebase respawns it when the cycle is done.
|
|
@@ -106,7 +118,7 @@ If a rebase is due **and** `conflict_resolve_attempts[head_sha] < 2` **and** `he
|
|
|
106
118
|
4. Append a dispatching line to `followup.log`; emit a `tick` event with `rebase_needed: true`, `dispatched_rebase: true`.
|
|
107
119
|
5. Exit. The dev cycle owns the PR; its respawn restarts the watcher, whose next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
|
|
108
120
|
|
|
109
|
-
Otherwise — `behind_by == 0` and not conflicting (`mergeable == UNKNOWN` is fine here: `behind_by` is exact while GitHub is still computing conflict state, so a stale branch still triggers), or budget spent (`conflict_resolve_attempts[
|
|
121
|
+
Otherwise — `behind_by == 0` and not conflicting (`mergeable == UNKNOWN` is fine here: `behind_by` is exact while GitHub is still computing conflict state, so a stale branch still triggers), or budget spent (`conflict_resolve_attempts[rebase_key] >= 2` or `rebase_key` ∈ `conflict_escalated_keys`) → fall through to CI.
|
|
110
122
|
|
|
111
123
|
### Step 6 — No actionable feedback, branch current → poll CI for the head SHA
|
|
112
124
|
|
|
@@ -125,10 +137,20 @@ Fetch the CI rollup for `prs.json[0].head_sha`, provider resolved as in Step 3
|
|
|
125
137
|
5. Exit. The dev cycle owns the PR; its respawn restarts the watcher, whose next tick re-checks CI on the new head SHA — CI itself is the verify loop.
|
|
126
138
|
- **One or more red, but `ci_fix_attempts[head_sha] >= 3` or `head_sha` ∈ `ci_escalated_shas`** → idle. The fix budget is spent; `/muggle-do`'s fix-ci stage already recorded the escalation. The watcher does not re-dispatch.
|
|
127
139
|
|
|
128
|
-
### Step 7 — Idle
|
|
140
|
+
### Step 7 — Idle (remind when blocked pending a human)
|
|
141
|
+
|
|
142
|
+
Any idle branch (Steps 4–6 that did not dispatch). First classify **why** this tick idled. It is **blocked pending a human** when the head is under a durable block that only the user can clear:
|
|
143
|
+
|
|
144
|
+
- `rebase_key` ∈ `conflict_escalated_keys` — a rebase `/muggle-do` 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; or
|
|
145
|
+
- `head_sha` ∈ `ci_escalated_shas` — CI the fix-ci stage gave up on, reason `ci_escalated`; or
|
|
146
|
+
- `last_seen.escalated_review_ids` is non-empty with the actionable set empty — an ambiguous review awaiting the user's direction, reason `reviews_escalated`.
|
|
147
|
+
|
|
148
|
+
Everything else that idles is **transient** — green and waiting for the next review, CI still pending, or `mergeable == UNKNOWN` — and must keep the responsive `1m` cadence; those turn a state on their own and the watcher should catch it promptly.
|
|
149
|
+
|
|
150
|
+
**Transient idle** (no durable block): unchanged — increment `last_seen.idle_tick_count`, append an idle line to `followup.log` per [`output-templates/watcher-log.md`](output-templates/watcher-log.md), emit a `tick` event with `idle: true`, `blocked: false`, `actionable_threads: 0`, `dispatched_review_ids: []`, `rebase_needed: <bool>`, `dispatched_rebase: false`, `checks_red: <count or 0>`, `dispatched_ci_fix: false`. Exit. The next tick fires in 1 min via `/loop`.
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
**Blocked pending a human** (a durable block, and `last_seen.blocked` not already set): enter the blocked path per [`blocked-tick.md`](blocked-tick.md) — flag `last_seen.blocked` and emit the one-line owner reminder (the watcher reminds each tick at the normal `1m` cadence rather than backing off). From the next tick on, the Step 2.5 gate carries the block. Exit.
|
|
131
153
|
|
|
132
154
|
## Output
|
|
133
155
|
|
|
134
|
-
|
|
156
|
+
The watcher stays invisible to the **GitHub reviewer** — it never posts to the PR from a tick. To the **loop owner** in the session it emits: the turn preamble, the one-line blocked reminder while awaiting the owner (Steps 2.5 / 7), and (when a dispatch fires) the `/muggle-do` directive. Nothing else.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "muggle-pr-followup",
|
|
3
|
+
"notes": "These evals test the TICK DECISION behavior. A real tick needs live GitHub state and the Cron tools, so each prompt hands the model a concrete slot + provider state and asks for the step-by-step plan it would execute; assertions check the plan takes the branch the contract mandates. Run via skill-creator's eval runner — CI does not execute these (ci.yml gates on vitest/check-skill-deps; skill-eval.yml runs gate scenarios sourced from muggle-ai-brain and the routing eval).",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 0,
|
|
7
|
+
"eval_name": "actionable-threads-dispatch-address-reviews",
|
|
8
|
+
"prompt": "Tick muggle-ai-works-pr500 500. The slot is open. The PR has two unresolved, not-outdated review threads whose newest comments are from a human (no muggle-do:bot marker), CI is fully green, and the branch is level with master. Walk me through the plan for this tick.",
|
|
9
|
+
"files": [],
|
|
10
|
+
"assertions": [
|
|
11
|
+
{ "name": "computes_actionable_from_live_thread_state", "text": "Plan derives the actionable set from current unresolved-thread state, not from a stored review-id cursor." },
|
|
12
|
+
{ "name": "classifies_by_loop_marker", "text": "Plan decides a thread is actionable by the absence of the muggle-do:bot marker on the newest comment, never by author login." },
|
|
13
|
+
{ "name": "cancels_own_cron_before_dispatch", "text": "Plan cancels this watcher's own cron before dispatching, so no tick overlaps the running cycle." },
|
|
14
|
+
{ "name": "dispatches_address_reviews", "text": "Plan dispatches /muggle-do with an address-reviews directive carrying the PR URL, slug, and owning review ids, then exits." },
|
|
15
|
+
{ "name": "stays_a_dumb_pipe", "text": "Plan does NOT classify reviews, post replies, resolve threads, or escalate — those belong to /muggle-do." }
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": 1,
|
|
20
|
+
"eval_name": "reviews-preempt-ci",
|
|
21
|
+
"prompt": "Tick muggle-ai-works-pr501 501. The slot is open. There is one unresolved human review thread AND two red required checks on the head SHA. Walk me through the plan for this tick.",
|
|
22
|
+
"files": [],
|
|
23
|
+
"assertions": [
|
|
24
|
+
{ "name": "dispatches_address_reviews_only", "text": "Plan dispatches address-reviews for the review thread." },
|
|
25
|
+
{ "name": "does_not_poll_or_dispatch_ci", "text": "Plan does NOT dispatch fix-ci on this tick — actionable feedback preempts the CI branch entirely." },
|
|
26
|
+
{ "name": "single_dispatch_then_exit", "text": "Plan performs exactly one dispatch and exits rather than handling both concerns in one tick." }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": 2,
|
|
31
|
+
"eval_name": "behind-branch-dispatches-rebase",
|
|
32
|
+
"prompt": "Tick muggle-ai-works-pr502 502. The slot is open, there are no unresolved review threads, CI is green. GitHub reports mergeStateStatus BLOCKED and mergeable MERGEABLE, and the compare call shows behind_by 4. Walk me through the plan for this tick.",
|
|
33
|
+
"files": [],
|
|
34
|
+
"assertions": [
|
|
35
|
+
{ "name": "reads_staleness_from_compare", "text": "Plan determines the branch is out of date from behind_by on the compare call (commit ancestry)." },
|
|
36
|
+
{ "name": "does_not_trust_mergeStateStatus", "text": "Plan does NOT conclude the branch is current just because mergeStateStatus reads BLOCKED rather than BEHIND." },
|
|
37
|
+
{ "name": "dispatches_rebase", "text": "Plan dispatches /muggle-do with a rebase directive (PR URL + slug, no review ids, no check names)." },
|
|
38
|
+
{ "name": "keys_dedup_on_head_and_base_pair", "text": "Plan keys the rebase attempt/escalation lookup on the head..base_tip pair, not the head SHA alone." }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": 3,
|
|
43
|
+
"eval_name": "red-ci-within-budget-dispatches-fix-ci",
|
|
44
|
+
"prompt": "Tick muggle-ai-works-pr503 503. The slot is open, no unresolved review threads, branch is level with its base. Two checks are red on the head SHA. ci_fix_attempts for that SHA is 1 and the SHA is not in ci_escalated_shas. Walk me through the plan for this tick.",
|
|
45
|
+
"files": [],
|
|
46
|
+
"assertions": [
|
|
47
|
+
{ "name": "checks_attempt_budget", "text": "Plan verifies ci_fix_attempts for the head SHA is under the cap of 3 before dispatching." },
|
|
48
|
+
{ "name": "checks_escalated_set", "text": "Plan verifies the head SHA is not in ci_escalated_shas." },
|
|
49
|
+
{ "name": "dispatches_fix_ci_with_check_names", "text": "Plan dispatches /muggle-do with a fix-ci directive carrying the red check names." },
|
|
50
|
+
{ "name": "cancels_own_cron_before_dispatch", "text": "Plan cancels this watcher's cron before dispatching." }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": 4,
|
|
55
|
+
"eval_name": "red-ci-over-budget-idles",
|
|
56
|
+
"prompt": "Tick muggle-ai-works-pr504 504. The slot is open, no unresolved review threads, branch is level with its base. Three checks are red on the head SHA, ci_fix_attempts for that SHA is 3, and the SHA is in ci_escalated_shas. Walk me through the plan for this tick.",
|
|
57
|
+
"files": [],
|
|
58
|
+
"assertions": [
|
|
59
|
+
{ "name": "does_not_dispatch", "text": "Plan does NOT dispatch fix-ci — the budget is spent and the SHA is already escalated." },
|
|
60
|
+
{ "name": "treats_as_durable_block", "text": "Plan classifies this idle as blocked pending a human (reason ci_escalated), not as a transient idle." },
|
|
61
|
+
{ "name": "reminds_owner_at_1m", "text": "Plan emits a one-line owner reminder and keeps the normal 1m cadence — it does not back off, slow down, or stop polling." },
|
|
62
|
+
{ "name": "no_pr_side_post", "text": "Plan does NOT post anything to the PR for a blocked tick; the reminder goes to the loop owner in-session." }
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": 5,
|
|
67
|
+
"eval_name": "merged-pr-finalizes-without-respawn",
|
|
68
|
+
"prompt": "Tick muggle-ai-works-pr505 505. Refreshing the PR shows state MERGED. The slot on disk still says open. Walk me through the plan for this tick.",
|
|
69
|
+
"files": [],
|
|
70
|
+
"assertions": [
|
|
71
|
+
{ "name": "finalizes_the_slot", "text": "Plan finalizes the slot: marks it terminal, writes result.md, logs, and unschedules this watcher's cron." },
|
|
72
|
+
{ "name": "does_not_respawn", "text": "Plan does NOT respawn or re-arm a watcher — a terminal PR needs none." },
|
|
73
|
+
{ "name": "hands_off_terminal_wrapup", "text": "Plan hands the terminal wrap-up to /muggle-do post-merge cleanup as the turn's last action." },
|
|
74
|
+
{ "name": "unschedules_recorded_id_first", "text": "Plan cancels the cron by the id recorded in cron.json first, falling back to a CronList match — so teardown works even after CronList has gone blind." }
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": 6,
|
|
79
|
+
"eval_name": "stale-queued-fire-does-not-refinalize",
|
|
80
|
+
"prompt": "Tick muggle-ai-works-pr506 506. Reading the slot, prs.json already records state merged and result.md exists — this fire was queued before the cron was cancelled. Walk me through the plan for this tick.",
|
|
81
|
+
"files": [],
|
|
82
|
+
"assertions": [
|
|
83
|
+
{ "name": "detects_stale_fire", "text": "Plan recognises this as a stale queued fire from the on-disk terminal state before fetching anything." },
|
|
84
|
+
{ "name": "does_not_refetch_or_refinalize", "text": "Plan does NOT re-fetch the PR, re-write result.md, or re-run the terminal handoff." },
|
|
85
|
+
{ "name": "defensively_cancels_and_logs", "text": "Plan defensively cancels any lingering cron for the slug and appends a stale-tick line to followup.log, then exits." }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Blocked reminder
|
|
2
|
+
|
|
3
|
+
The watcher's one-line nudge to the loop owner, emitted every tick a PR sits blocked pending a human ([`../contract.md`](../contract.md) Steps 2.5 / 7). It goes to the **session** — the owner running the loop — never to the PR, so it never notifies the GitHub reviewer. Keep it to **one line**: the pending act plus a reference to trace back to the decision context.
|
|
4
|
+
|
|
5
|
+
## Shape
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
⏳ <owner>/<repo>#<n> awaiting you (<duration>): <pending act>. → <reference>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `<duration>` — how long the block has stood, from `last_seen.blocked.since` (e.g. `2h`, `1d`).
|
|
12
|
+
- `<pending act>` — the one thing the owner must do, keyed by `reason`.
|
|
13
|
+
- `<reference>` — where to trace the decision back to: the review, the PR checks, or the blocked SHA.
|
|
14
|
+
|
|
15
|
+
## By reason
|
|
16
|
+
|
|
17
|
+
- **`reviews_escalated`** — an ambiguous review awaits your direction:
|
|
18
|
+
```
|
|
19
|
+
⏳ acme/widget#142 awaiting you (2h): pick a direction on ambiguous review #<id> from <login>. → reply on the review or tell me here: <review-url>
|
|
20
|
+
```
|
|
21
|
+
- **`conflict_escalated`** — a rebase the loop gave up on:
|
|
22
|
+
```
|
|
23
|
+
⏳ acme/widget#142 awaiting you (2h): resolve the rebase conflict on <short-sha> (autoResolveConflicts=never), then push — or set the pref to always. → <pr-url>
|
|
24
|
+
```
|
|
25
|
+
- **`ci_escalated`** — the CI fix budget is spent:
|
|
26
|
+
```
|
|
27
|
+
⏳ acme/widget#142 awaiting you (2h): the failing checks on <short-sha> need your call — fix and push, or advise here. → <pr-url>/checks
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The reminder repeats every blocked tick (at the normal `1m` cadence) until the owner acts; the watcher stops it the moment the fingerprint moves (a push, a new review, or a CI/deploy change).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Watcher tick log lines
|
|
2
2
|
|
|
3
|
-
The watcher
|
|
3
|
+
The watcher only appends to `followup.log` during normal operation; the one visible exception is the one-line owner reminder on a blocked tick ([`blocked-reminder.md`](blocked-reminder.md)). All user-facing escalations come from `/muggle-do`.
|
|
4
4
|
|
|
5
5
|
## Idle tick
|
|
6
6
|
|
|
@@ -14,6 +14,16 @@ The watcher does **not** print to the user during normal operation. It only appe
|
|
|
14
14
|
<ISO-8601> tick pr=<n> threads=<count> dispatched=<id1>,<id2>,...
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Blocked tick
|
|
18
|
+
|
|
19
|
+
The tick idled on a durable human-block ([`../contract.md`](../contract.md) Step 7) and emitted the one-line owner reminder ([`blocked-reminder.md`](blocked-reminder.md)). One per tick for as long as the block stands, at the normal `1m` cadence:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
<ISO-8601> tick pr=<n> blocked reason=<conflict_escalated|ci_escalated|reviews_escalated>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When the fingerprint moves the block clears and the tick logs a normal idle or dispatching line — there is no separate unblock line.
|
|
26
|
+
|
|
17
27
|
## Terminal tick
|
|
18
28
|
|
|
19
29
|
```
|
|
@@ -7,7 +7,8 @@ All user-facing message text used by the watcher, bootstrap, and `/muggle-do` in
|
|
|
7
7
|
| Group | Use case |
|
|
8
8
|
| :---- | :------- |
|
|
9
9
|
| [`bootstrap`](output-templates/bootstrap.md) | Bootstrap success summary + all bootstrap aborts. |
|
|
10
|
-
| [`watcher-log`](output-templates/watcher-log.md) | The
|
|
10
|
+
| [`watcher-log`](output-templates/watcher-log.md) | The `followup.log` line shapes (idle, dispatching, blocked, terminal). |
|
|
11
|
+
| [`blocked-reminder`](output-templates/blocked-reminder.md) | The watcher's one-line owner reminder while a PR is blocked pending a human. |
|
|
11
12
|
| [`escalation`](output-templates/escalation.md) | `/muggle-do` terminal escalation messages (ambiguous, design-adjustment). |
|
|
12
13
|
| [`inline-reply`](output-templates/inline-reply.md) | Per-comment inline reply + top-level fallback for body-only reviews. |
|
|
13
14
|
| [`resolve-reminder`](output-templates/resolve-reminder.md) | Top-level PR comment for the resolve-reminder stage. |
|