@muggleai/works 5.4.0 → 5.4.2
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/README.md +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/scripts/ensure-electron-app.sh +22 -2
- package/dist/plugin/skills/_shared/rebase-before-e2e.md +1 -1
- package/dist/plugin/skills/_shared/resolve-rebase-conflicts.md +1 -1
- package/dist/plugin/skills/_shared/session-state-writes.md +22 -0
- package/dist/plugin/skills/do/address-reviews.md +2 -0
- package/dist/plugin/skills/do/fix-ci.md +1 -1
- package/dist/plugin/skills/do/open-prs/update.md +1 -1
- package/dist/plugin/skills/do/resolve-conflicts.md +3 -3
- package/dist/plugin/skills/muggle-do/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
- package/dist/plugin/skills/muggle-pr-followup/cancel-cron.md +11 -0
- package/dist/plugin/skills/muggle-pr-followup/contract.md +6 -2
- package/dist/plugin/skills/muggle-pr-followup/finalize.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +1 -1
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoResolveConflicts.md +1 -1
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +1 -1
- package/dist/release-manifest.json +4 -4
- package/package.json +10 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/scripts/ensure-electron-app.sh +22 -2
- package/plugin/skills/_shared/rebase-before-e2e.md +1 -1
- package/plugin/skills/_shared/resolve-rebase-conflicts.md +1 -1
- package/plugin/skills/_shared/session-state-writes.md +22 -0
- package/plugin/skills/do/address-reviews.md +2 -0
- package/plugin/skills/do/fix-ci.md +1 -1
- package/plugin/skills/do/open-prs/update.md +1 -1
- package/plugin/skills/do/resolve-conflicts.md +3 -3
- package/plugin/skills/muggle-do/SKILL.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
- package/plugin/skills/muggle-pr-followup/cancel-cron.md +11 -0
- package/plugin/skills/muggle-pr-followup/contract.md +6 -2
- package/plugin/skills/muggle-pr-followup/finalize.md +1 -1
- package/plugin/skills/muggle-pr-followup/state-schemas.md +1 -1
- package/plugin/skills/muggle-preferences/preference-gates/autoResolveConflicts.md +1 -1
- package/plugin/skills/muggle-test-feature-local/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -555,7 +555,7 @@ CI/CD and publishing
|
|
|
555
555
|
| `verify-end-user-upgrade.yml` | Weekly + manual | Existing-user upgrade validation (cleanup + re-download + health checks) |
|
|
556
556
|
| `publish-works-to-npm.yml` | Tag `v*` or manual | Verify (including release checksums), audit, smoke-install, publish to npm |
|
|
557
557
|
|
|
558
|
-
**Publishing `@muggleai/works`:** use the maintainer-only skill **`.claude/skills/muggle-works-npm-release/SKILL.md`** — a repo-local project skill (invoke `/muggle-works-npm-release` in Claude Code, or `/mrelease`, while working in this repo; mirrored to `.cursor/skills/` for Cursor). It is intentionally not in the published plugin (`plugin/skills/`) since it is maintainer-only. It does the bump + `pnpm run sync:versions`, local verify, `chore(release)` PR, merge, then `workflow_dispatch` with an explicit `version`. Do not rely on tagging alone while `package.json` / marketplace manifests on `master` are still old — CI can publish a version that does not match the checked-in manifests. Tag `v*` push remains a valid workflow trigger when it matches the merged release commit.
|
|
558
|
+
**Publishing `@muggleai/works`:** use the maintainer-only skill **`.claude/skills/muggle-works-npm-release/SKILL.md`** — a repo-local project skill (invoke `/muggle-works-npm-release` in Claude Code, or `/mrelease`, while working in this repo; mirrored to `.cursor/skills/` for Cursor). To invoke it from any project — not only when the cwd is this repo — run `pnpm run link:maintainer-skills`, which symlinks the maintainer skills from `.claude/skills/` into `~/.claude/skills/`; it is idempotent and safe to re-run if a skill moves. It is intentionally not in the published plugin (`plugin/skills/`) since it is maintainer-only. It does the bump + `pnpm run sync:versions`, local verify, `chore(release)` PR, merge, then `workflow_dispatch` with an explicit `version`. Do not rely on tagging alone while `package.json` / marketplace manifests on `master` are still old — CI can publish a version that does not match the checked-in manifests. Tag `v*` push remains a valid workflow trigger when it matches the merged release commit.
|
|
559
559
|
|
|
560
560
|
Release tag strategy
|
|
561
561
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "5.4.
|
|
4
|
+
"version": "5.4.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "5.4.
|
|
5
|
+
"version": "5.4.2",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
+
# Epoch mtime of a file, or 0 if unreadable. GNU coreutils (Linux/WSL/Git Bash)
|
|
6
|
+
# need `stat -c %Y`; BSD/macOS need `stat -f %m`. The wrong form doesn't fail
|
|
7
|
+
# cleanly — GNU `stat -f` prints filesystem info to stdout and exits non-zero —
|
|
8
|
+
# so each form is tried alone and accepted only when all-digits, keeping stray
|
|
9
|
+
# text like `File:` out of the caller's arithmetic.
|
|
10
|
+
file_mtime() {
|
|
11
|
+
local f="$1" m
|
|
12
|
+
m=$(stat -c %Y "$f" 2>/dev/null) || m=""
|
|
13
|
+
case "$m" in ''|*[!0-9]*) ;; *) printf '%s\n' "$m"; return 0 ;; esac
|
|
14
|
+
m=$(stat -f %m "$f" 2>/dev/null) || m=""
|
|
15
|
+
case "$m" in ''|*[!0-9]*) ;; *) printf '%s\n' "$m"; return 0 ;; esac
|
|
16
|
+
printf '0\n'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Sourcing with MUGGLE_ENSURE_ELECTRON_LIB_ONLY=1 exposes the helper above
|
|
20
|
+
# without running the hook — used by the unit test.
|
|
21
|
+
if [ -n "${MUGGLE_ENSURE_ELECTRON_LIB_ONLY:-}" ]; then
|
|
22
|
+
return 0 2>/dev/null || exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
5
25
|
# Ensure the Electron browser test runner is installed/up to date (silent, best-effort).
|
|
6
26
|
#
|
|
7
27
|
# Bounded + cached: this script runs from a SessionStart hook on every Claude
|
|
@@ -16,7 +36,7 @@ ensure_ttl=$((24 * 60 * 60))
|
|
|
16
36
|
ensure_now=$(date +%s)
|
|
17
37
|
ensure_last=0
|
|
18
38
|
if [ -f "${ensure_marker}" ]; then
|
|
19
|
-
ensure_last=$(
|
|
39
|
+
ensure_last=$(file_mtime "${ensure_marker}")
|
|
20
40
|
fi
|
|
21
41
|
|
|
22
42
|
if [ $((ensure_now - ensure_last)) -ge "${ensure_ttl}" ]; then
|
|
@@ -60,7 +80,7 @@ version_check() {
|
|
|
60
80
|
now=$(date +%s)
|
|
61
81
|
|
|
62
82
|
if [ -f "$cache_file" ]; then
|
|
63
|
-
mtime=$(
|
|
83
|
+
mtime=$(file_mtime "$cache_file")
|
|
64
84
|
age=$((now - mtime))
|
|
65
85
|
if [ "$age" -lt "$ttl" ]; then
|
|
66
86
|
cached=$(cat "$cache_file" 2>/dev/null || true)
|
|
@@ -15,7 +15,7 @@ Pass `{behind}` and `{default}` to the picker prompts. On `always`:
|
|
|
15
15
|
1. Capture the rollback point: `pre_rebase_sha=$(git rev-parse HEAD)`.
|
|
16
16
|
2. `git rebase origin/${default}`.
|
|
17
17
|
3. On conflict, branch by [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
|
|
18
|
-
- `never`
|
|
18
|
+
- `never` → `git rebase --abort`; stop and report, naming the conflicted files. Never auto-resolve.
|
|
19
19
|
- `always` → hand off to [`resolve-rebase-conflicts.md`](resolve-rebase-conflicts.md) with `pre_rebase_sha`; it resolves, runs the verify-or-rollback gate, and either proceeds or restores `pre_rebase_sha` and escalates.
|
|
20
20
|
|
|
21
21
|
Stale branches produce false failures and false greens — that's why this gate exists.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auto-Resolve Rebase Conflicts
|
|
2
2
|
|
|
3
|
-
The autonomous conflict-resolution body, run when a rebase onto `origin/{default}` reports conflicts **and** [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md) is `always`. Under
|
|
3
|
+
The autonomous conflict-resolution body, run when a rebase onto `origin/{default}` reports conflicts **and** [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md) is `always`. Under `never` the caller aborts and escalates instead, and this file never runs. The caller hands off the inputs below; this file names no caller — the dependency runs one way.
|
|
4
4
|
|
|
5
5
|
Contract: never push an auto-resolved rebase that has not passed the verify gate, and always keep the branch restorable to its pre-rebase state.
|
|
6
6
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Writing session-state files
|
|
2
|
+
|
|
3
|
+
How to update the session-state JSON — `last_seen.json` and `prs.json` under `~/.muggle-ai/muggle-do/sessions/<slug>/`. Field shapes: [`../muggle-pr-followup/state-schemas.md`](../muggle-pr-followup/state-schemas.md). Both the watcher and `/muggle-do` write these.
|
|
4
|
+
|
|
5
|
+
## Mechanism — tool-based, OS-agnostic
|
|
6
|
+
|
|
7
|
+
To apply any `increment` / `reset` / `set` / `append` a procedure calls for:
|
|
8
|
+
|
|
9
|
+
1. **Read** the whole file (Read tool).
|
|
10
|
+
2. Change the one field in the parsed JSON.
|
|
11
|
+
3. **Write** the whole file back (Write tool).
|
|
12
|
+
|
|
13
|
+
A whole-file rewrite with the Read and Write tools. These are platform-independent — no shell — so the same three steps hold on Windows, macOS, and Linux.
|
|
14
|
+
|
|
15
|
+
**Never use the Edit tool on these files.** Edit needs its `old_string` to match the file's exact bytes, but the on-disk formatting isn't guaranteed to match the shapes documented in `state-schemas.md` — a writer may emit the JSON on a single line. A mismatched `old_string` silently fails: the edit is dropped ("malformed edit") and the value never changes. A whole-file Write can't miss.
|
|
16
|
+
|
|
17
|
+
If you script the rewrite instead of using the Write tool, any tool that replaces the **whole file** is fine — e.g. `jq '…' file > tmp && mv tmp file` in a POSIX shell, or the equivalent `Get-Content`/`Set-Content` in PowerShell. The rule is only: whole file, never a partial Edit.
|
|
18
|
+
|
|
19
|
+
## Field map
|
|
20
|
+
|
|
21
|
+
- `last_seen.json` — one object keyed by `"<owner>/<repo>#<n>"`. Mutate fields under that key: `idle_tick_count`, `cycles_completed` (counters), `last_pushed_sha`, `lastBodyReviewId` (scalars), `pushed_shas`, `escalated_review_ids`, `ci_escalated_shas`, `conflict_escalated_shas` (arrays), `ci_fix_attempts[<sha>]`, `conflict_resolve_attempts[<sha>]` (per-SHA maps).
|
|
22
|
+
- `prs.json` — a one-element array. Mutate `[0]`: `head_sha`, `state`.
|
|
@@ -103,6 +103,8 @@ Invoke [`per-comment-replies.md`](per-comment-replies.md) with the actionable re
|
|
|
103
103
|
|
|
104
104
|
### Step 5 — Update session state
|
|
105
105
|
|
|
106
|
+
Apply each field write below as a whole-file rewrite (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md) — never the Edit tool.
|
|
107
|
+
|
|
106
108
|
- `last_seen.cycles_completed` += 1
|
|
107
109
|
- `last_seen.last_pushed_sha` = the new head SHA (update.md already wrote this; verify)
|
|
108
110
|
- `last_seen.lastBodyReviewId` = max(body-only input review ids ∪ last_seen.lastBodyReviewId) — line-comment threads need no watermark; they fall out of the actionable set once the per-comment reply carries the loop marker.
|
|
@@ -39,7 +39,7 @@ Commit per the `fix(ci): <check> — <what>` convention ([`../_shared/pr-followu
|
|
|
39
39
|
|
|
40
40
|
### Step 5 — Update state + respawn
|
|
41
41
|
|
|
42
|
-
- Increment `last_seen.ci_fix_attempts[red_sha]
|
|
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
43
|
- Respawn the watcher: `/loop 1m /muggle:muggle-pr-followup <slug> <n>`. The watcher cancelled its own cron when it dispatched this fix-ci cycle ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md) Step 5), so this restart is the single live watcher. 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)
|
|
@@ -24,7 +24,7 @@ Resolve the provider once per [`../../_shared/vcs/detect-vcs.md`](../../_shared/
|
|
|
24
24
|
|
|
25
25
|
1. **Push:** per [`../../_shared/vcs/github/push-to-branch.md`](../../_shared/vcs/github/push-to-branch.md). Capture the new SHA.
|
|
26
26
|
|
|
27
|
-
2. **Append new SHA** to `last_seen.json[<key>].pushed_shas` (the resolve-reminder stage uses this to recognize threads addressed by the loop). Set `last_seen.last_pushed_sha` to the new SHA too.
|
|
27
|
+
2. **Append new SHA** to `last_seen.json[<key>].pushed_shas` (the resolve-reminder stage uses this to recognize threads addressed by the loop). Set `last_seen.last_pushed_sha` to the new SHA too. Both are whole-file rewrites (Read → change field → Write) per [`../../_shared/session-state-writes.md`](../../_shared/session-state-writes.md) — never the Edit tool.
|
|
28
28
|
|
|
29
29
|
3. **Refresh title if state changed.** Compare the new state against the current PR title prefix:
|
|
30
30
|
- E2E now passing, current title has `[E2E FAILING]` → strip the prefix per [`../../_shared/vcs/github/pr-edit.md`](../../_shared/vcs/github/pr-edit.md).
|
|
@@ -28,7 +28,7 @@ Run the rebase from [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before
|
|
|
28
28
|
|
|
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
32
|
- `always` → resolve behind the verify-or-rollback gate in [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md).
|
|
33
33
|
|
|
34
34
|
### Step 3 — Verify the resolution
|
|
@@ -37,7 +37,7 @@ 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[rebase_sha]
|
|
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_sha]` — 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 as the last action:
|
|
41
41
|
|
|
42
42
|
```
|
|
43
43
|
/loop 1m /muggle:muggle-pr-followup <slug> <n>
|
|
@@ -61,4 +61,4 @@ Emit one `muggle-do:cycle` event ([`../_shared/telemetry-events/muggle-do-cycle.
|
|
|
61
61
|
|
|
62
62
|
- Max 2 rebase attempts per SHA; then escalate rather than churn.
|
|
63
63
|
- Never push an unverified rebase — verify-or-rollback always.
|
|
64
|
-
-
|
|
64
|
+
- Resolve `autoResolveConflicts` from the configured preference (per the gate contract — don't assume a default): `always` resolves conflicts behind the verify-or-rollback gate, `never` escalates to the user. A clean behind-only rebase needs neither.
|
|
@@ -62,7 +62,7 @@ When invoked with the directive (PR URL + slug + review ids), routes to [`../do/
|
|
|
62
62
|
| Preference | Gate |
|
|
63
63
|
| :--------- | :--- |
|
|
64
64
|
| `autoE2ETest` | Stage 6 — run E2E every cycle (default `always`), or fold into pre-flight |
|
|
65
|
-
| `autoResolveConflicts` | On rebase conflict —
|
|
65
|
+
| `autoResolveConflicts` | On rebase conflict — `always` resolves autonomously behind a verify-or-rollback gate, `never` aborts + escalates; resolve from the configured preference (don't assume a default) |
|
|
66
66
|
| `autoRouteBuildToMuggleDo` | Front-door guardrail — route build/implement/fix requests through this pipeline (build delegated to superpowers); fired by the UserPromptSubmit guardrail, default `ask` |
|
|
67
67
|
|
|
68
68
|
`autoUseWorktree`, `autoRebase`, `autoResolveConflicts`, `autoCreatePR`, `autoCleanup`, `postPRVisualWalkthrough` fire from per-stage files.
|
|
@@ -9,6 +9,7 @@ This folder holds the watcher loop that drives one PR toward merge-ready. The wa
|
|
|
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
10
|
- [`contract.md`](contract.md) — the watcher per-tick procedure (poll → dispatch → exit).
|
|
11
11
|
- [`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) — the find-and-delete that stops this watcher's cron, with the tool-call-not-shell guard. Referenced by `contract.md` and `finalize.md`.
|
|
12
13
|
- [`reconcile.md`](reconcile.md) — sweep that finalizes slots whose PR went terminal while polling lapsed; runs at the top of auto-track and on demand.
|
|
13
14
|
- [`state-schemas.md`](state-schemas.md) — canonical JSON shapes of session state files.
|
|
14
15
|
- [`output-templates.md`](output-templates.md) — TOC of message templates; per-group files in `output-templates/`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Cancel the watcher's cron
|
|
2
|
+
|
|
3
|
+
The find-and-delete every tick uses to stop its own loop: 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
|
+
|
|
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
|
+
|
|
7
|
+
1. Call the `CronList` tool.
|
|
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.
|
|
10
|
+
|
|
11
|
+
No-op when none matches — a manually-run tick, or a cron that already expired.
|
|
@@ -23,11 +23,15 @@ Read these from `~/.muggle-ai/muggle-do/sessions/<slug>/`:
|
|
|
23
23
|
|
|
24
24
|
If either file is missing or the PR is not in `prs.json`, the tick is a no-op. Log an error line in `followup.log` and exit. The watcher must not be invoked in this state — if it happens, the slot is corrupt.
|
|
25
25
|
|
|
26
|
+
## Writing state
|
|
27
|
+
|
|
28
|
+
Every `increment`/`reset` this procedure applies to `last_seen.json`, and the `prs.json` refresh in Step 1, is a **whole-file rewrite** (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md). **Never** patch session JSON with the Edit tool — an exact-string match against these files silently fails ("malformed edit") and drops the update, so the counter never advances.
|
|
29
|
+
|
|
26
30
|
## Procedure
|
|
27
31
|
|
|
28
32
|
### Step 0 — Stale-fire guard
|
|
29
33
|
|
|
30
|
-
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
|
|
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.
|
|
31
35
|
|
|
32
36
|
### Step 1 — Refresh PR state
|
|
33
37
|
|
|
@@ -67,7 +71,7 @@ The watcher's dispatch trigger is **derived from current provider state**, not a
|
|
|
67
71
|
The watcher does **not** classify. Classification, batching, replying, escalation, and cycle execution all live in `/muggle-do`. The watcher hands over the dispatch ids from Step 3 (GitHub: owning review ids; GitLab: discussion ids) and exits — `/muggle-do`'s address-reviews re-derives the unresolved threads itself (its authority), so the watcher only needs to decide *that* there is work, not enumerate it exhaustively.
|
|
68
72
|
|
|
69
73
|
1. Reset `last_seen.idle_tick_count` to 0.
|
|
70
|
-
2. **Stop this watcher (single-thread).** Cancel its cron so no tick fires while the dev cycle runs
|
|
74
|
+
2. **Stop this watcher (single-thread).** Cancel its cron so no tick fires while the dev cycle runs, per [`cancel-cron.md`](cancel-cron.md). `/muggle-do` respawns the watcher when the cycle finishes — exactly one cron ever, and no tick overlaps a running cycle.
|
|
71
75
|
3. Dispatch `/muggle-do` with an *address-reviews* directive carrying:
|
|
72
76
|
- PR URL (from `prs.json[0].url`)
|
|
73
77
|
- Session slug (from the invocation arguments)
|
|
@@ -26,4 +26,4 @@ Append the terminal line per [`output-templates/watcher-log.md`](output-template
|
|
|
26
26
|
|
|
27
27
|
### Step 4 — Unschedule the cron
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Cancel this slot's cron per [`cancel-cron.md`](cancel-cron.md). No-op when none matches — a manually-run tick, or a cron that already expired. Recurring `/loop` crons auto-expire after 7 days; that lapse is the gap [`reconcile.md`](reconcile.md) exists to catch.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical shapes for the JSON files in a PR-follow-up session slot. The slot path is `~/.muggle-ai/muggle-do/sessions/<slug>/` (under the user's home, shared across repos; `muggle-do` is the current and only caller).
|
|
4
4
|
|
|
5
|
-
All files are atomic writes —
|
|
5
|
+
All files are **whole-file atomic writes** — rewrite the entire file each time, never a partial Edit. The mechanism (tool-based, OS-agnostic) and the reason Edit fails on these files live in [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md).
|
|
6
6
|
|
|
7
7
|
## Legacy location
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `autoResolveConflicts`
|
|
2
2
|
|
|
3
|
-
When a rebase onto `origin/{default}` hits conflicts,
|
|
3
|
+
When a rebase onto `origin/{default}` hits conflicts, `always` resolves them autonomously behind a verify-or-rollback gate and `never` aborts the rebase and escalates. Take the effective value from the configured preference per [`../README.md`](../README.md#resolution) (the injected `Muggle Test Preferences` line) — don't assume a default; an unset key resolves to `ask`.
|
|
4
4
|
|
|
5
5
|
**Picker 1** — header `Resolve rebase conflicts?`, question `"Rebase onto origin/{default} hit conflicts in {conflicted} file(s) — resolve them autonomously?"`
|
|
6
6
|
- `Resolve autonomously` — `Resolve the conflicts, then re-verify (build + unit + E2E) before any push; roll back and escalate if verification fails.` → `always`
|
|
@@ -153,7 +153,7 @@ Resolve the `showElectronBrowser` gate **first**, then call `muggle-local-execut
|
|
|
153
153
|
|
|
154
154
|
Gate `showElectronBrowser` (per `preference-gates/README.md`). Reuse the choice within a session. The runner shows the browser by default, so treat `showUi` as a **hide switch** — include it only to turn the browser off:
|
|
155
155
|
- `always` (show it) → **omit `showUi` entirely** — no `showUi` key in the call. Passing `showUi: false` here is a bug: it hides the browser the user wanted to watch.
|
|
156
|
-
- `never` (hide it) → pass `showUi: false
|
|
156
|
+
- `never` (hide it) → pass `showUi: false` **explicitly**. Omitting `showUi` here is a bug: it falls back to the default (shown) and reveals the browser the user turned off.
|
|
157
157
|
- `ask` → you **must** call `AskUserQuestion` (Picker 1 from `preference-gates/showElectronBrowser.md`) **before** the execute call, then map the answer to the `always`/`never` action above. Do not decide for the user.
|
|
158
158
|
|
|
159
159
|
So the execute call carries **no `showUi` key** for `always`, or `showUi: false` for `never` — never `showUi: true`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release": "5.4.
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-
|
|
2
|
+
"release": "5.4.2",
|
|
3
|
+
"buildId": "run-62-1",
|
|
4
|
+
"commitSha": "d87dd32f6b01e48c82ff97aa568835bd2e5a7fc3",
|
|
5
|
+
"buildTime": "2026-07-05T08:59:08Z",
|
|
6
6
|
"serviceName": "muggle-ai-works-mcp"
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muggleai/works",
|
|
3
3
|
"mcpName": "io.github.multiplex-ai/muggle",
|
|
4
|
-
"version": "5.4.
|
|
4
|
+
"version": "5.4.2",
|
|
5
5
|
"description": "Ship quality products with AI-powered E2E acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
"build:release": "npm run build",
|
|
23
23
|
"verify:plugin": "node scripts/verify-plugin-marketplace.mjs",
|
|
24
24
|
"verify:contracts": "node scripts/verify-compatibility-contracts.mjs",
|
|
25
|
+
"smoke:cli": "node scripts/smoke-cli.mjs",
|
|
25
26
|
"verify:electron-release-checksums": "node scripts/verify-electron-release-checksums.mjs",
|
|
26
27
|
"verify:upgrade-experience": "node scripts/verify-upgrade-experience.mjs",
|
|
28
|
+
"link:maintainer-skills": "node scripts/link-maintainer-skills.mjs",
|
|
27
29
|
"build:workspace": "turbo run build",
|
|
28
30
|
"typecheck:workspace": "turbo run typecheck",
|
|
29
31
|
"lint:workspace": "turbo run lint",
|
|
@@ -39,17 +41,18 @@
|
|
|
39
41
|
"typecheck": "tsc --noEmit",
|
|
40
42
|
"test": "vitest run",
|
|
41
43
|
"test:watch": "vitest",
|
|
42
|
-
"test:gates:behavioral": "tsx internal/skill-gate-eval/src/run.ts"
|
|
44
|
+
"test:gates:behavioral": "tsx internal/skill-gate-eval/src/run.ts",
|
|
45
|
+
"eval:studio-gen": "tsx internal/studio-gen-eval/src/run.ts"
|
|
43
46
|
},
|
|
44
47
|
"muggleConfig": {
|
|
45
|
-
"electronAppVersion": "1.6.
|
|
48
|
+
"electronAppVersion": "1.6.6",
|
|
46
49
|
"downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
|
|
47
50
|
"runtimeTargetDefault": "production",
|
|
48
51
|
"checksums": {
|
|
49
|
-
"darwin-arm64": "
|
|
50
|
-
"darwin-x64": "
|
|
51
|
-
"linux-x64": "
|
|
52
|
-
"win32-x64": "
|
|
52
|
+
"darwin-arm64": "c905921dda5412bdbb66735cdb81b0e0f76f4211c9ec129eae599d910213547e",
|
|
53
|
+
"darwin-x64": "3ad51b6babadf73ae607e965dd713fb9ad8a530e6c15c47779c34a96ea90dab4",
|
|
54
|
+
"linux-x64": "444c603453967f91b4a4ccb12f4344d7bed8d83b5b99d04fc43924c6c45b08f7",
|
|
55
|
+
"win32-x64": "ab7864489da53920af13905671aca4960cc141e41637590044757e47a439a9d7"
|
|
53
56
|
}
|
|
54
57
|
},
|
|
55
58
|
"dependencies": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "5.4.
|
|
4
|
+
"version": "5.4.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "5.4.
|
|
5
|
+
"version": "5.4.2",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
|
+
# Epoch mtime of a file, or 0 if unreadable. GNU coreutils (Linux/WSL/Git Bash)
|
|
6
|
+
# need `stat -c %Y`; BSD/macOS need `stat -f %m`. The wrong form doesn't fail
|
|
7
|
+
# cleanly — GNU `stat -f` prints filesystem info to stdout and exits non-zero —
|
|
8
|
+
# so each form is tried alone and accepted only when all-digits, keeping stray
|
|
9
|
+
# text like `File:` out of the caller's arithmetic.
|
|
10
|
+
file_mtime() {
|
|
11
|
+
local f="$1" m
|
|
12
|
+
m=$(stat -c %Y "$f" 2>/dev/null) || m=""
|
|
13
|
+
case "$m" in ''|*[!0-9]*) ;; *) printf '%s\n' "$m"; return 0 ;; esac
|
|
14
|
+
m=$(stat -f %m "$f" 2>/dev/null) || m=""
|
|
15
|
+
case "$m" in ''|*[!0-9]*) ;; *) printf '%s\n' "$m"; return 0 ;; esac
|
|
16
|
+
printf '0\n'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Sourcing with MUGGLE_ENSURE_ELECTRON_LIB_ONLY=1 exposes the helper above
|
|
20
|
+
# without running the hook — used by the unit test.
|
|
21
|
+
if [ -n "${MUGGLE_ENSURE_ELECTRON_LIB_ONLY:-}" ]; then
|
|
22
|
+
return 0 2>/dev/null || exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
5
25
|
# Ensure the Electron browser test runner is installed/up to date (silent, best-effort).
|
|
6
26
|
#
|
|
7
27
|
# Bounded + cached: this script runs from a SessionStart hook on every Claude
|
|
@@ -16,7 +36,7 @@ ensure_ttl=$((24 * 60 * 60))
|
|
|
16
36
|
ensure_now=$(date +%s)
|
|
17
37
|
ensure_last=0
|
|
18
38
|
if [ -f "${ensure_marker}" ]; then
|
|
19
|
-
ensure_last=$(
|
|
39
|
+
ensure_last=$(file_mtime "${ensure_marker}")
|
|
20
40
|
fi
|
|
21
41
|
|
|
22
42
|
if [ $((ensure_now - ensure_last)) -ge "${ensure_ttl}" ]; then
|
|
@@ -60,7 +80,7 @@ version_check() {
|
|
|
60
80
|
now=$(date +%s)
|
|
61
81
|
|
|
62
82
|
if [ -f "$cache_file" ]; then
|
|
63
|
-
mtime=$(
|
|
83
|
+
mtime=$(file_mtime "$cache_file")
|
|
64
84
|
age=$((now - mtime))
|
|
65
85
|
if [ "$age" -lt "$ttl" ]; then
|
|
66
86
|
cached=$(cat "$cache_file" 2>/dev/null || true)
|
|
@@ -15,7 +15,7 @@ Pass `{behind}` and `{default}` to the picker prompts. On `always`:
|
|
|
15
15
|
1. Capture the rollback point: `pre_rebase_sha=$(git rev-parse HEAD)`.
|
|
16
16
|
2. `git rebase origin/${default}`.
|
|
17
17
|
3. On conflict, branch by [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
|
|
18
|
-
- `never`
|
|
18
|
+
- `never` → `git rebase --abort`; stop and report, naming the conflicted files. Never auto-resolve.
|
|
19
19
|
- `always` → hand off to [`resolve-rebase-conflicts.md`](resolve-rebase-conflicts.md) with `pre_rebase_sha`; it resolves, runs the verify-or-rollback gate, and either proceeds or restores `pre_rebase_sha` and escalates.
|
|
20
20
|
|
|
21
21
|
Stale branches produce false failures and false greens — that's why this gate exists.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auto-Resolve Rebase Conflicts
|
|
2
2
|
|
|
3
|
-
The autonomous conflict-resolution body, run when a rebase onto `origin/{default}` reports conflicts **and** [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md) is `always`. Under
|
|
3
|
+
The autonomous conflict-resolution body, run when a rebase onto `origin/{default}` reports conflicts **and** [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md) is `always`. Under `never` the caller aborts and escalates instead, and this file never runs. The caller hands off the inputs below; this file names no caller — the dependency runs one way.
|
|
4
4
|
|
|
5
5
|
Contract: never push an auto-resolved rebase that has not passed the verify gate, and always keep the branch restorable to its pre-rebase state.
|
|
6
6
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Writing session-state files
|
|
2
|
+
|
|
3
|
+
How to update the session-state JSON — `last_seen.json` and `prs.json` under `~/.muggle-ai/muggle-do/sessions/<slug>/`. Field shapes: [`../muggle-pr-followup/state-schemas.md`](../muggle-pr-followup/state-schemas.md). Both the watcher and `/muggle-do` write these.
|
|
4
|
+
|
|
5
|
+
## Mechanism — tool-based, OS-agnostic
|
|
6
|
+
|
|
7
|
+
To apply any `increment` / `reset` / `set` / `append` a procedure calls for:
|
|
8
|
+
|
|
9
|
+
1. **Read** the whole file (Read tool).
|
|
10
|
+
2. Change the one field in the parsed JSON.
|
|
11
|
+
3. **Write** the whole file back (Write tool).
|
|
12
|
+
|
|
13
|
+
A whole-file rewrite with the Read and Write tools. These are platform-independent — no shell — so the same three steps hold on Windows, macOS, and Linux.
|
|
14
|
+
|
|
15
|
+
**Never use the Edit tool on these files.** Edit needs its `old_string` to match the file's exact bytes, but the on-disk formatting isn't guaranteed to match the shapes documented in `state-schemas.md` — a writer may emit the JSON on a single line. A mismatched `old_string` silently fails: the edit is dropped ("malformed edit") and the value never changes. A whole-file Write can't miss.
|
|
16
|
+
|
|
17
|
+
If you script the rewrite instead of using the Write tool, any tool that replaces the **whole file** is fine — e.g. `jq '…' file > tmp && mv tmp file` in a POSIX shell, or the equivalent `Get-Content`/`Set-Content` in PowerShell. The rule is only: whole file, never a partial Edit.
|
|
18
|
+
|
|
19
|
+
## Field map
|
|
20
|
+
|
|
21
|
+
- `last_seen.json` — one object keyed by `"<owner>/<repo>#<n>"`. Mutate fields under that key: `idle_tick_count`, `cycles_completed` (counters), `last_pushed_sha`, `lastBodyReviewId` (scalars), `pushed_shas`, `escalated_review_ids`, `ci_escalated_shas`, `conflict_escalated_shas` (arrays), `ci_fix_attempts[<sha>]`, `conflict_resolve_attempts[<sha>]` (per-SHA maps).
|
|
22
|
+
- `prs.json` — a one-element array. Mutate `[0]`: `head_sha`, `state`.
|
|
@@ -103,6 +103,8 @@ Invoke [`per-comment-replies.md`](per-comment-replies.md) with the actionable re
|
|
|
103
103
|
|
|
104
104
|
### Step 5 — Update session state
|
|
105
105
|
|
|
106
|
+
Apply each field write below as a whole-file rewrite (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md) — never the Edit tool.
|
|
107
|
+
|
|
106
108
|
- `last_seen.cycles_completed` += 1
|
|
107
109
|
- `last_seen.last_pushed_sha` = the new head SHA (update.md already wrote this; verify)
|
|
108
110
|
- `last_seen.lastBodyReviewId` = max(body-only input review ids ∪ last_seen.lastBodyReviewId) — line-comment threads need no watermark; they fall out of the actionable set once the per-comment reply carries the loop marker.
|
|
@@ -39,7 +39,7 @@ Commit per the `fix(ci): <check> — <what>` convention ([`../_shared/pr-followu
|
|
|
39
39
|
|
|
40
40
|
### Step 5 — Update state + respawn
|
|
41
41
|
|
|
42
|
-
- Increment `last_seen.ci_fix_attempts[red_sha]
|
|
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
43
|
- Respawn the watcher: `/loop 1m /muggle:muggle-pr-followup <slug> <n>`. The watcher cancelled its own cron when it dispatched this fix-ci cycle ([`../muggle-pr-followup/contract.md`](../muggle-pr-followup/contract.md) Step 5), so this restart is the single live watcher. 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)
|
|
@@ -24,7 +24,7 @@ Resolve the provider once per [`../../_shared/vcs/detect-vcs.md`](../../_shared/
|
|
|
24
24
|
|
|
25
25
|
1. **Push:** per [`../../_shared/vcs/github/push-to-branch.md`](../../_shared/vcs/github/push-to-branch.md). Capture the new SHA.
|
|
26
26
|
|
|
27
|
-
2. **Append new SHA** to `last_seen.json[<key>].pushed_shas` (the resolve-reminder stage uses this to recognize threads addressed by the loop). Set `last_seen.last_pushed_sha` to the new SHA too.
|
|
27
|
+
2. **Append new SHA** to `last_seen.json[<key>].pushed_shas` (the resolve-reminder stage uses this to recognize threads addressed by the loop). Set `last_seen.last_pushed_sha` to the new SHA too. Both are whole-file rewrites (Read → change field → Write) per [`../../_shared/session-state-writes.md`](../../_shared/session-state-writes.md) — never the Edit tool.
|
|
28
28
|
|
|
29
29
|
3. **Refresh title if state changed.** Compare the new state against the current PR title prefix:
|
|
30
30
|
- E2E now passing, current title has `[E2E FAILING]` → strip the prefix per [`../../_shared/vcs/github/pr-edit.md`](../../_shared/vcs/github/pr-edit.md).
|
|
@@ -28,7 +28,7 @@ Run the rebase from [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before
|
|
|
28
28
|
|
|
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
32
|
- `always` → resolve behind the verify-or-rollback gate in [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md).
|
|
33
33
|
|
|
34
34
|
### Step 3 — Verify the resolution
|
|
@@ -37,7 +37,7 @@ 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[rebase_sha]
|
|
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_sha]` — 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 as the last action:
|
|
41
41
|
|
|
42
42
|
```
|
|
43
43
|
/loop 1m /muggle:muggle-pr-followup <slug> <n>
|
|
@@ -61,4 +61,4 @@ Emit one `muggle-do:cycle` event ([`../_shared/telemetry-events/muggle-do-cycle.
|
|
|
61
61
|
|
|
62
62
|
- Max 2 rebase attempts per SHA; then escalate rather than churn.
|
|
63
63
|
- Never push an unverified rebase — verify-or-rollback always.
|
|
64
|
-
-
|
|
64
|
+
- Resolve `autoResolveConflicts` from the configured preference (per the gate contract — don't assume a default): `always` resolves conflicts behind the verify-or-rollback gate, `never` escalates to the user. A clean behind-only rebase needs neither.
|
|
@@ -62,7 +62,7 @@ When invoked with the directive (PR URL + slug + review ids), routes to [`../do/
|
|
|
62
62
|
| Preference | Gate |
|
|
63
63
|
| :--------- | :--- |
|
|
64
64
|
| `autoE2ETest` | Stage 6 — run E2E every cycle (default `always`), or fold into pre-flight |
|
|
65
|
-
| `autoResolveConflicts` | On rebase conflict —
|
|
65
|
+
| `autoResolveConflicts` | On rebase conflict — `always` resolves autonomously behind a verify-or-rollback gate, `never` aborts + escalates; resolve from the configured preference (don't assume a default) |
|
|
66
66
|
| `autoRouteBuildToMuggleDo` | Front-door guardrail — route build/implement/fix requests through this pipeline (build delegated to superpowers); fired by the UserPromptSubmit guardrail, default `ask` |
|
|
67
67
|
|
|
68
68
|
`autoUseWorktree`, `autoRebase`, `autoResolveConflicts`, `autoCreatePR`, `autoCleanup`, `postPRVisualWalkthrough` fire from per-stage files.
|
|
@@ -9,6 +9,7 @@ This folder holds the watcher loop that drives one PR toward merge-ready. The wa
|
|
|
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
10
|
- [`contract.md`](contract.md) — the watcher per-tick procedure (poll → dispatch → exit).
|
|
11
11
|
- [`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) — the find-and-delete that stops this watcher's cron, with the tool-call-not-shell guard. Referenced by `contract.md` and `finalize.md`.
|
|
12
13
|
- [`reconcile.md`](reconcile.md) — sweep that finalizes slots whose PR went terminal while polling lapsed; runs at the top of auto-track and on demand.
|
|
13
14
|
- [`state-schemas.md`](state-schemas.md) — canonical JSON shapes of session state files.
|
|
14
15
|
- [`output-templates.md`](output-templates.md) — TOC of message templates; per-group files in `output-templates/`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Cancel the watcher's cron
|
|
2
|
+
|
|
3
|
+
The find-and-delete every tick uses to stop its own loop: 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
|
+
|
|
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
|
+
|
|
7
|
+
1. Call the `CronList` tool.
|
|
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.
|
|
10
|
+
|
|
11
|
+
No-op when none matches — a manually-run tick, or a cron that already expired.
|
|
@@ -23,11 +23,15 @@ Read these from `~/.muggle-ai/muggle-do/sessions/<slug>/`:
|
|
|
23
23
|
|
|
24
24
|
If either file is missing or the PR is not in `prs.json`, the tick is a no-op. Log an error line in `followup.log` and exit. The watcher must not be invoked in this state — if it happens, the slot is corrupt.
|
|
25
25
|
|
|
26
|
+
## Writing state
|
|
27
|
+
|
|
28
|
+
Every `increment`/`reset` this procedure applies to `last_seen.json`, and the `prs.json` refresh in Step 1, is a **whole-file rewrite** (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md). **Never** patch session JSON with the Edit tool — an exact-string match against these files silently fails ("malformed edit") and drops the update, so the counter never advances.
|
|
29
|
+
|
|
26
30
|
## Procedure
|
|
27
31
|
|
|
28
32
|
### Step 0 — Stale-fire guard
|
|
29
33
|
|
|
30
|
-
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
|
|
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.
|
|
31
35
|
|
|
32
36
|
### Step 1 — Refresh PR state
|
|
33
37
|
|
|
@@ -67,7 +71,7 @@ The watcher's dispatch trigger is **derived from current provider state**, not a
|
|
|
67
71
|
The watcher does **not** classify. Classification, batching, replying, escalation, and cycle execution all live in `/muggle-do`. The watcher hands over the dispatch ids from Step 3 (GitHub: owning review ids; GitLab: discussion ids) and exits — `/muggle-do`'s address-reviews re-derives the unresolved threads itself (its authority), so the watcher only needs to decide *that* there is work, not enumerate it exhaustively.
|
|
68
72
|
|
|
69
73
|
1. Reset `last_seen.idle_tick_count` to 0.
|
|
70
|
-
2. **Stop this watcher (single-thread).** Cancel its cron so no tick fires while the dev cycle runs
|
|
74
|
+
2. **Stop this watcher (single-thread).** Cancel its cron so no tick fires while the dev cycle runs, per [`cancel-cron.md`](cancel-cron.md). `/muggle-do` respawns the watcher when the cycle finishes — exactly one cron ever, and no tick overlaps a running cycle.
|
|
71
75
|
3. Dispatch `/muggle-do` with an *address-reviews* directive carrying:
|
|
72
76
|
- PR URL (from `prs.json[0].url`)
|
|
73
77
|
- Session slug (from the invocation arguments)
|
|
@@ -26,4 +26,4 @@ Append the terminal line per [`output-templates/watcher-log.md`](output-template
|
|
|
26
26
|
|
|
27
27
|
### Step 4 — Unschedule the cron
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Cancel this slot's cron per [`cancel-cron.md`](cancel-cron.md). No-op when none matches — a manually-run tick, or a cron that already expired. Recurring `/loop` crons auto-expire after 7 days; that lapse is the gap [`reconcile.md`](reconcile.md) exists to catch.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Canonical shapes for the JSON files in a PR-follow-up session slot. The slot path is `~/.muggle-ai/muggle-do/sessions/<slug>/` (under the user's home, shared across repos; `muggle-do` is the current and only caller).
|
|
4
4
|
|
|
5
|
-
All files are atomic writes —
|
|
5
|
+
All files are **whole-file atomic writes** — rewrite the entire file each time, never a partial Edit. The mechanism (tool-based, OS-agnostic) and the reason Edit fails on these files live in [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md).
|
|
6
6
|
|
|
7
7
|
## Legacy location
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `autoResolveConflicts`
|
|
2
2
|
|
|
3
|
-
When a rebase onto `origin/{default}` hits conflicts,
|
|
3
|
+
When a rebase onto `origin/{default}` hits conflicts, `always` resolves them autonomously behind a verify-or-rollback gate and `never` aborts the rebase and escalates. Take the effective value from the configured preference per [`../README.md`](../README.md#resolution) (the injected `Muggle Test Preferences` line) — don't assume a default; an unset key resolves to `ask`.
|
|
4
4
|
|
|
5
5
|
**Picker 1** — header `Resolve rebase conflicts?`, question `"Rebase onto origin/{default} hit conflicts in {conflicted} file(s) — resolve them autonomously?"`
|
|
6
6
|
- `Resolve autonomously` — `Resolve the conflicts, then re-verify (build + unit + E2E) before any push; roll back and escalate if verification fails.` → `always`
|
|
@@ -153,7 +153,7 @@ Resolve the `showElectronBrowser` gate **first**, then call `muggle-local-execut
|
|
|
153
153
|
|
|
154
154
|
Gate `showElectronBrowser` (per `preference-gates/README.md`). Reuse the choice within a session. The runner shows the browser by default, so treat `showUi` as a **hide switch** — include it only to turn the browser off:
|
|
155
155
|
- `always` (show it) → **omit `showUi` entirely** — no `showUi` key in the call. Passing `showUi: false` here is a bug: it hides the browser the user wanted to watch.
|
|
156
|
-
- `never` (hide it) → pass `showUi: false
|
|
156
|
+
- `never` (hide it) → pass `showUi: false` **explicitly**. Omitting `showUi` here is a bug: it falls back to the default (shown) and reveals the browser the user turned off.
|
|
157
157
|
- `ask` → you **must** call `AskUserQuestion` (Picker 1 from `preference-gates/showElectronBrowser.md`) **before** the execute call, then map the answer to the `always`/`never` action above. Do not decide for the user.
|
|
158
158
|
|
|
159
159
|
So the execute call carries **no `showUi` key** for `always`, or `showUi: false` for `never` — never `showUi: true`.
|