@muggleai/works 5.6.0 → 5.8.0
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/{chunk-LTULYOOW.js → chunk-5PYK7DWB.js} +15 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/README.md +15 -3
- package/dist/plugin/hooks/hooks.json +41 -1
- package/dist/plugin/scripts/gc-state.sh +66 -0
- package/dist/plugin/scripts/guard-run-job-object.ps1 +256 -0
- package/dist/plugin/scripts/guard-run.mjs +327 -0
- package/dist/plugin/scripts/guardrail-offer-ran.sh +35 -0
- package/dist/plugin/scripts/guardrail-pr-terminal.sh +21 -0
- package/dist/plugin/scripts/guardrail-record-tests.sh +5 -4
- package/dist/plugin/scripts/guardrail-terminal-gate.sh +38 -0
- package/dist/plugin/scripts/guardrails.mjs +91 -3
- package/dist/plugin/scripts/pr-watch-guards.sh +46 -0
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
- package/dist/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
- package/dist/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
- package/dist/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
- package/dist/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
- package/{plugin/skills/_shared/vcs/github → dist/plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
- package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
- package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
- package/dist/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
- package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
- package/dist/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
- package/dist/plugin/skills/_shared/vcs/github.md +5 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
- package/dist/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
- package/dist/plugin/skills/_shared/vcs/gitlab.md +6 -3
- package/dist/plugin/skills/_shared/vcs/post-signature.md +31 -0
- package/dist/plugin/skills/do/build.md +58 -56
- package/dist/plugin/skills/do/open-prs/forward.md +85 -84
- package/dist/plugin/skills/do/open-prs/update.md +3 -3
- package/dist/plugin/skills/do/per-comment-replies.md +2 -2
- package/dist/plugin/skills/do/resolve-conflicts.md +59 -58
- package/dist/plugin/skills/do/unit-tests.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
- package/dist/plugin/skills/muggle-pr-followup/contract.md +171 -158
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
- package/dist/plugin/skills/muggle-pr-followup/stop.md +31 -0
- package/dist/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +14 -5
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
- package/dist/release-manifest.json +4 -4
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/hooks/README.md +15 -3
- package/plugin/hooks/hooks.json +41 -1
- package/plugin/scripts/gc-state.sh +66 -0
- package/plugin/scripts/guard-run-job-object.ps1 +256 -0
- package/plugin/scripts/guard-run.mjs +327 -0
- package/plugin/scripts/guardrail-offer-ran.sh +35 -0
- package/plugin/scripts/guardrail-pr-terminal.sh +21 -0
- package/plugin/scripts/guardrail-record-tests.sh +5 -4
- package/plugin/scripts/guardrail-terminal-gate.sh +38 -0
- package/plugin/scripts/guardrails.mjs +91 -3
- package/plugin/scripts/pr-watch-guards.sh +46 -0
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
- package/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
- package/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
- package/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
- package/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
- package/{dist/plugin/skills/_shared/vcs/github → plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
- package/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
- package/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
- package/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
- package/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
- package/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
- package/plugin/skills/_shared/vcs/github.md +5 -2
- package/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
- package/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
- package/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
- package/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
- package/plugin/skills/_shared/vcs/gitlab.md +6 -3
- package/plugin/skills/_shared/vcs/post-signature.md +31 -0
- package/plugin/skills/do/build.md +58 -56
- package/plugin/skills/do/open-prs/forward.md +85 -84
- package/plugin/skills/do/open-prs/update.md +3 -3
- package/plugin/skills/do/per-comment-replies.md +2 -2
- package/plugin/skills/do/resolve-conflicts.md +59 -58
- package/plugin/skills/do/unit-tests.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
- package/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
- package/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
- package/plugin/skills/muggle-pr-followup/contract.md +171 -158
- package/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
- package/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
- package/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
- package/plugin/skills/muggle-pr-followup/stop.md +31 -0
- package/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +14 -5
- package/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
- package/dist/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
- package/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
|
@@ -1,175 +1,191 @@
|
|
|
1
|
-
# Session State Schemas
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
## Legacy location
|
|
8
|
-
|
|
9
|
-
Before the move to the user's home, slots lived at the repo-relative `.muggle-do/sessions/<slug>/` (one per working tree, still gitignored). Bootstrap's Step 5 migrates a legacy slot to the home-dir location on the next run for that PR; nothing else reads the old path. The state is ephemeral and reconstructible from GitHub, so an un-migrated slot costs only a re-bootstrap, not data.
|
|
10
|
-
|
|
11
|
-
## `prs.json`
|
|
12
|
-
|
|
13
|
-
A list of one entry. (Historical: the file is an array for forward-compat with the original session-wide model. Today, each PR has its own session slot, so the array always has exactly one entry.)
|
|
14
|
-
|
|
15
|
-
```json
|
|
16
|
-
[
|
|
17
|
-
{
|
|
18
|
-
"repo": "<owner>/<repo>",
|
|
19
|
-
"provider": "github" | "gitlab",
|
|
20
|
-
"number": <int>,
|
|
21
|
-
"url": "https://github.com/<owner>/<repo>/pull/<number>",
|
|
22
|
-
"head_sha": "<40-char-hex-sha>",
|
|
23
|
-
"state": "open" | "merged" | "closed"
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
- `provider` selects the recipe set (`gh` vs `glab`). Absent ⇒ `github` — existing slots predate the field and stay GitHub.
|
|
29
|
-
- Under GitLab, `number` holds the MR `iid` (per-project, not the global MR id); `head_sha` is unchanged. GitLab's `opened` normalizes to `open`; `merged` and `closed` already align.
|
|
30
|
-
- `state` is the **observed** state from the last `gh pr view`. The watcher refreshes it each tick.
|
|
31
|
-
- Terminal states (`merged`, `closed`) are sticky — once set, the watcher writes `result.md` and exits without rescheduling.
|
|
32
|
-
|
|
33
|
-
## `cron.json`
|
|
34
|
-
|
|
35
|
-
A durable, on-disk handle to this slot's watcher cron. Its whole reason to exist: `CronList` goes **blind to crons that outlive a session continue / compaction** (the watcher's `/loop` cron survives, but the tool can no longer enumerate it), so a teardown that can only find crons through `CronList` can never delete the orphan — it re-fires until the 7-day `/loop` expiry. A cron id recorded to disk **while the cron was still visible** stays a valid `CronDelete` target afterward. See [`record-cron-id.md`](record-cron-id.md) (who writes it) and [`cancel-cron.md`](cancel-cron.md) (who deletes by it).
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"cron_id": "<scheduler-id-or-null>",
|
|
40
|
-
"command": "/muggle:muggle-pr-followup <slug> <n>",
|
|
41
|
-
"interval": "1m",
|
|
42
|
-
"recorded_at": "<ISO-8601>"
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
- `cron_id`: the scheduler id of the live `/loop` cron for this slot. Bootstrap seeds `null` (it dispatches `/loop` as its last action and cannot yet see the id); the first tick self-records the real id per [`record-cron-id.md`](record-cron-id.md). `null` again for the one tick after `/muggle-do` respawns the watcher (a dispatch cancels the old cron and the respawn arms a new one whose id is unknown until the next tick observes it).
|
|
47
|
-
- `command`: the exact two-arg dispatch, the same string [`cancel-cron.md`](cancel-cron.md) matches on as its `CronList` fallback.
|
|
48
|
-
- `interval`: the poll cadence — always `1m`. The watcher polls at `1m` whether or not the PR is blocked; a blocked PR reminds at `1m`, it does not back off (see [`blocked-tick.md`](blocked-tick.md) and [`contract.md`](contract.md) Steps 2.5 / 7). Recorded for teardown/forensics.
|
|
49
|
-
|
|
50
|
-
## `last_seen.json`
|
|
51
|
-
|
|
52
|
-
Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{
|
|
56
|
-
"<owner>/<repo>#<n>": {
|
|
57
|
-
"lastBodyReviewId": <int>,
|
|
58
|
-
"last_pushed_sha": "<sha-or-null>",
|
|
59
|
-
"idle_tick_count": <int>,
|
|
60
|
-
"cycles_completed": <int>,
|
|
61
|
-
"escalated_review_ids": [<int>, ...],
|
|
62
|
-
"pushed_shas": ["<sha>", ...],
|
|
63
|
-
"ci_fix_attempts": { "<sha>": <int> },
|
|
64
|
-
"ci_escalated_shas": ["<sha>", ...],
|
|
65
|
-
"conflict_resolve_attempts": { "<head-sha>..<base-tip-sha>": <int> },
|
|
66
|
-
"conflict_escalated_keys": ["<head-sha>..<base-tip-sha>", ...],
|
|
67
|
-
"blocked": {
|
|
68
|
-
"reason": "conflict_escalated" | "ci_escalated" | "reviews_escalated",
|
|
69
|
-
"since": "<ISO-8601>",
|
|
70
|
-
"fingerprint": {
|
|
71
|
-
"head_sha": "<sha>",
|
|
72
|
-
"latest_review_id": <int>,
|
|
73
|
-
"ci_digest": "<string>"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
- `lastBodyReviewId`: narrow watermark for **body-only** reviews (a submitted review carrying no line comments). The watcher dispatches a body-only review only when `id > lastBodyReviewId`. Line-comment threads do **not** use it — they are dispatched from live thread state (unresolved + not outdated + newest comment unmarked by the loop), so there is no cursor that can pin past them. Bootstrap sets it to the highest existing submitted review id with `--forward-only`, else `0`. The cursor keeps its name under GitLab, where it holds the highest note / discussion id.
|
|
81
|
-
- `last_pushed_sha`: most recent SHA `/muggle-do` pushed in this PR's life; `null` until the first push.
|
|
82
|
-
- `idle_tick_count`: incremented each tick whose actionable set is empty. Reset to 0 on any tick that dispatches `/muggle-do`. Diagnostic only — does not gate behavior.
|
|
83
|
-
- `cycles_completed`: incremented each time `/muggle-do` completes an address-reviews invocation (regardless of actionable/ambiguous/mixed).
|
|
84
|
-
- `escalated_review_ids`: review ids classified as ambiguous by `/muggle-do`. The watcher excludes these from the actionable set (both body-only reviews and the threads they own) so the same ambiguous review is never re-dispatched.
|
|
85
|
-
- `pushed_shas`: every SHA `/muggle-do` has pushed for this PR. Append-only. Used by the resolve-reminder stage to recognize threads addressed by the loop.
|
|
86
|
-
- `ci_fix_attempts`: per-SHA count of fix-ci cycles `/muggle-do` has run. The watcher stops dispatching fix-ci for a SHA once its count reaches 3. Keyed by head SHA.
|
|
87
|
-
- `ci_escalated_shas`: head SHAs whose CI the fix-ci stage gave up on (attempts exhausted or only out-of-scope checks). The watcher excludes these from CI dispatch so a hopeless SHA is never re-fixed.
|
|
88
|
-
- `conflict_resolve_attempts`: count of rebase cycles `/muggle-do` has run (behind-only or conflicting — both rebase onto the base). The watcher stops dispatching once a key's count reaches 2. Keyed by `rebase_key` — `"<head_sha>..<base_tip_sha>"`, the head paired with the base branch tip it was measured against.
|
|
89
|
-
- `conflict_escalated_keys`: `rebase_key`s whose rebase `/muggle-do` gave up on (attempts exhausted, or a conflict under `autoResolveConflicts=never`). The watcher excludes these from rebase dispatch so a hopeless pairing is never re-attempted.
|
|
90
|
-
|
|
91
|
-
Both are keyed on the pair, not the head alone, because whether a branch conflicts depends on both sides. Under a head-only key, a base that moves produces a genuinely new conflict against an unchanged head — and the stale entry suppresses it permanently, because nothing can change the head while the branch sits blocked. Pairing re-arms the budget whenever either side moves. Legacy entries written before this change are bare SHAs with no `..`; readers ignore them, which un-wedges any slot they had blocked.
|
|
92
|
-
|
|
93
|
-
Unlike these, `ci_fix_attempts` / `ci_escalated_shas` stay keyed on the head SHA alone — a CI result is a function of the head only, so base movement must not re-arm them.
|
|
94
|
-
- `blocked`: present only while the watcher is **awaiting the owner** on a PR that cannot progress without a human ([`contract.md`](contract.md) Step 7). Absent ⇒ the watcher is in its normal dispatch flow. When present, the watcher **keeps the normal `1m` cadence** and each tick is a reminder-or-resume check ([`contract.md`](contract.md) Step 2.5): it re-emits a one-line reminder to the owner, recomputes the `fingerprint`, and clears the block the moment any component moves. Its value is the reason-specific reminder plus fingerprint auto-resume.
|
|
95
|
-
- `reason`: which durable block is being awaited — `conflict_escalated` (`rebase_key` ∈ `conflict_escalated_keys`), `ci_escalated` (`head_sha` ∈ `ci_escalated_shas`), or `reviews_escalated` (a review sits in `escalated_review_ids` awaiting the user, actionable set empty). Selects the reminder wording; the resume decision is fingerprint-driven, not reason-driven.
|
|
96
|
-
- `since`: when the block was first flagged — lets the reminder state how long the owner has been the blocker.
|
|
97
|
-
- `fingerprint`: the external state the block is waiting on. `head_sha` moves on a new push (which also clears the per-SHA escalation sets, keyed by SHA); `latest_review_id` is `max(id)` over submitted reviews and moves when a reviewer submits anything new; `ci_digest` is a stable digest of the head SHA's CI rollup (bucket + each check's name/conclusion, sorted) and moves when a check flips, a rerun lands, or an external check such as a staging deploy posts. Any change clears the block and resumes evaluation.
|
|
98
|
-
|
|
99
|
-
## `state.md`
|
|
100
|
-
|
|
101
|
-
Free-form markdown. No required schema beyond a few well-known fields the caller-agnostic code reads:
|
|
102
|
-
|
|
103
|
-
```markdown
|
|
104
|
-
# Session — <slug>
|
|
105
|
-
|
|
106
|
-
**PR:** <url>
|
|
107
|
-
**Slug:** <slug>
|
|
108
|
-
**Loop user:** <github-login> ← cached from `gh api user`
|
|
109
|
-
**Created:** <ISO-8601>
|
|
110
|
-
**Bootstrapped from URL:** <yes | no>
|
|
111
|
-
|
|
112
|
-
## Pre-flight answers
|
|
113
|
-
|
|
114
|
-
- Validation: <local-e2e | staging-replay | unit-only | skip>
|
|
115
|
-
- Local URL: <url | N/A>
|
|
116
|
-
- Backend status: <up | down | N/A>
|
|
117
|
-
- Muggle Test project: <name> (<uuid>)
|
|
118
|
-
- Test credentials: <existing | new | skip>
|
|
119
|
-
- Auth status: <ok | re-authed | N/A>
|
|
120
|
-
- Working tree: <path>
|
|
121
|
-
|
|
122
|
-
...free-form notes added by /muggle-do and bootstrap...
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
The `## Pre-flight answers` block is the **E2E validation context** consumed by `do/e2e-acceptance.md` Step 0 — seeded by bootstrap (Step 6.5) or by pre-flight's output block. Fields: [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md#persisted-fields).
|
|
126
|
-
|
|
127
|
-
The watcher does **not** read or write `state.md`. Only bootstrap, `/muggle-do`, and the caller's stages touch it.
|
|
128
|
-
|
|
129
|
-
## `followup.log`
|
|
130
|
-
|
|
131
|
-
Append-only line-per-tick log. One line per watcher tick, plus one line per `/muggle-do` invocation. Format is loose, but each line starts with an ISO-8601 timestamp:
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
2026-05-20T12:34:56Z tick pr=154 threads=0 idle
|
|
135
|
-
2026-05-20T12:35:56Z tick pr=154 threads=1 dispatched=4295962800
|
|
136
|
-
2026-05-20T12:36:14Z muggle-do cycle review_ids=[4295962800] outcome=pushed head_sha=abc1234
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Used for forensics only — never read back by skills.
|
|
140
|
-
|
|
141
|
-
## `result.md`
|
|
142
|
-
|
|
143
|
-
Written exactly once when the PR's watcher exits terminally (PR merged or closed). Free-form markdown summarizing the PR's life under this loop:
|
|
144
|
-
|
|
145
|
-
```markdown
|
|
146
|
-
# Result — <slug>
|
|
147
|
-
|
|
148
|
-
**PR:** <url>
|
|
149
|
-
**Final state:** merged | closed
|
|
150
|
-
**Cycles completed:** <int>
|
|
151
|
-
**Pushed SHAs:** <comma-separated list>
|
|
152
|
-
**Escalated review ids:** <comma-separated list or "none">
|
|
153
|
-
|
|
154
|
-
## Timeline
|
|
155
|
-
|
|
156
|
-
- <ISO-8601> bootstrap (lastBodyReviewId <id>; line-comment threads state-derived)
|
|
157
|
-
- <ISO-8601> review <id> from <login> — actionable, pushed <sha>
|
|
158
|
-
- <ISO-8601> review <id> from <login> — ambiguous, escalated
|
|
159
|
-
- ...
|
|
160
|
-
- <ISO-8601> PR <merged|closed> — watcher terminal
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Not in the slot
|
|
164
|
-
|
|
165
|
-
`cycle.json` and `requirements.md` are not seeded or read. `/muggle-do` reads reviews off GitHub each invocation.
|
|
166
|
-
|
|
167
|
-
## `watch-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1
|
+
# Session State Schemas
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
|
|
7
|
+
## Legacy location
|
|
8
|
+
|
|
9
|
+
Before the move to the user's home, slots lived at the repo-relative `.muggle-do/sessions/<slug>/` (one per working tree, still gitignored). Bootstrap's Step 5 migrates a legacy slot to the home-dir location on the next run for that PR; nothing else reads the old path. The state is ephemeral and reconstructible from GitHub, so an un-migrated slot costs only a re-bootstrap, not data.
|
|
10
|
+
|
|
11
|
+
## `prs.json`
|
|
12
|
+
|
|
13
|
+
A list of one entry. (Historical: the file is an array for forward-compat with the original session-wide model. Today, each PR has its own session slot, so the array always has exactly one entry.)
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
"repo": "<owner>/<repo>",
|
|
19
|
+
"provider": "github" | "gitlab",
|
|
20
|
+
"number": <int>,
|
|
21
|
+
"url": "https://github.com/<owner>/<repo>/pull/<number>",
|
|
22
|
+
"head_sha": "<40-char-hex-sha>",
|
|
23
|
+
"state": "open" | "merged" | "closed"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `provider` selects the recipe set (`gh` vs `glab`). Absent ⇒ `github` — existing slots predate the field and stay GitHub.
|
|
29
|
+
- Under GitLab, `number` holds the MR `iid` (per-project, not the global MR id); `head_sha` is unchanged. GitLab's `opened` normalizes to `open`; `merged` and `closed` already align.
|
|
30
|
+
- `state` is the **observed** state from the last `gh pr view`. The watcher refreshes it each tick.
|
|
31
|
+
- Terminal states (`merged`, `closed`) are sticky — once set, the watcher writes `result.md` and exits without rescheduling.
|
|
32
|
+
|
|
33
|
+
## `cron.json`
|
|
34
|
+
|
|
35
|
+
A durable, on-disk handle to this slot's watcher cron. Its whole reason to exist: `CronList` goes **blind to crons that outlive a session continue / compaction** (the watcher's `/loop` cron survives, but the tool can no longer enumerate it), so a teardown that can only find crons through `CronList` can never delete the orphan — it re-fires until the 7-day `/loop` expiry. A cron id recorded to disk **while the cron was still visible** stays a valid `CronDelete` target afterward. See [`record-cron-id.md`](record-cron-id.md) (who writes it) and [`cancel-cron.md`](cancel-cron.md) (who deletes by it).
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"cron_id": "<scheduler-id-or-null>",
|
|
40
|
+
"command": "/muggle:muggle-pr-followup <slug> <n>",
|
|
41
|
+
"interval": "1m",
|
|
42
|
+
"recorded_at": "<ISO-8601>"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `cron_id`: the scheduler id of the live `/loop` cron for this slot. Bootstrap seeds `null` (it dispatches `/loop` as its last action and cannot yet see the id); the first tick self-records the real id per [`record-cron-id.md`](record-cron-id.md). `null` again for the one tick after `/muggle-do` respawns the watcher (a dispatch cancels the old cron and the respawn arms a new one whose id is unknown until the next tick observes it).
|
|
47
|
+
- `command`: the exact two-arg dispatch, the same string [`cancel-cron.md`](cancel-cron.md) matches on as its `CronList` fallback.
|
|
48
|
+
- `interval`: the poll cadence — always `1m`. The watcher polls at `1m` whether or not the PR is blocked; a blocked PR reminds at `1m`, it does not back off (see [`blocked-tick.md`](blocked-tick.md) and [`contract.md`](contract.md) Steps 2.5 / 7). Recorded for teardown/forensics.
|
|
49
|
+
|
|
50
|
+
## `last_seen.json`
|
|
51
|
+
|
|
52
|
+
Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"<owner>/<repo>#<n>": {
|
|
57
|
+
"lastBodyReviewId": <int>,
|
|
58
|
+
"last_pushed_sha": "<sha-or-null>",
|
|
59
|
+
"idle_tick_count": <int>,
|
|
60
|
+
"cycles_completed": <int>,
|
|
61
|
+
"escalated_review_ids": [<int>, ...],
|
|
62
|
+
"pushed_shas": ["<sha>", ...],
|
|
63
|
+
"ci_fix_attempts": { "<sha>": <int> },
|
|
64
|
+
"ci_escalated_shas": ["<sha>", ...],
|
|
65
|
+
"conflict_resolve_attempts": { "<head-sha>..<base-tip-sha>": <int> },
|
|
66
|
+
"conflict_escalated_keys": ["<head-sha>..<base-tip-sha>", ...],
|
|
67
|
+
"blocked": {
|
|
68
|
+
"reason": "conflict_escalated" | "ci_escalated" | "reviews_escalated",
|
|
69
|
+
"since": "<ISO-8601>",
|
|
70
|
+
"fingerprint": {
|
|
71
|
+
"head_sha": "<sha>",
|
|
72
|
+
"latest_review_id": <int>,
|
|
73
|
+
"ci_digest": "<string>"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `lastBodyReviewId`: narrow watermark for **body-only** reviews (a submitted review carrying no line comments). The watcher dispatches a body-only review only when `id > lastBodyReviewId`. Line-comment threads do **not** use it — they are dispatched from live thread state (unresolved + not outdated + newest comment unmarked by the loop), so there is no cursor that can pin past them. Bootstrap sets it to the highest existing submitted review id with `--forward-only`, else `0`. The cursor keeps its name under GitLab, where it holds the highest note / discussion id.
|
|
81
|
+
- `last_pushed_sha`: most recent SHA `/muggle-do` pushed in this PR's life; `null` until the first push.
|
|
82
|
+
- `idle_tick_count`: incremented each tick whose actionable set is empty. Reset to 0 on any tick that dispatches `/muggle-do`. Diagnostic only — does not gate behavior.
|
|
83
|
+
- `cycles_completed`: incremented each time `/muggle-do` completes an address-reviews invocation (regardless of actionable/ambiguous/mixed).
|
|
84
|
+
- `escalated_review_ids`: review ids classified as ambiguous by `/muggle-do`. The watcher excludes these from the actionable set (both body-only reviews and the threads they own) so the same ambiguous review is never re-dispatched.
|
|
85
|
+
- `pushed_shas`: every SHA `/muggle-do` has pushed for this PR. Append-only. Used by the resolve-reminder stage to recognize threads addressed by the loop.
|
|
86
|
+
- `ci_fix_attempts`: per-SHA count of fix-ci cycles `/muggle-do` has run. The watcher stops dispatching fix-ci for a SHA once its count reaches 3. Keyed by head SHA.
|
|
87
|
+
- `ci_escalated_shas`: head SHAs whose CI the fix-ci stage gave up on (attempts exhausted or only out-of-scope checks). The watcher excludes these from CI dispatch so a hopeless SHA is never re-fixed.
|
|
88
|
+
- `conflict_resolve_attempts`: count of rebase cycles `/muggle-do` has run (behind-only or conflicting — both rebase onto the base). The watcher stops dispatching once a key's count reaches 2. Keyed by `rebase_key` — `"<head_sha>..<base_tip_sha>"`, the head paired with the base branch tip it was measured against.
|
|
89
|
+
- `conflict_escalated_keys`: `rebase_key`s whose rebase `/muggle-do` gave up on (attempts exhausted, or a conflict under `autoResolveConflicts=never`). The watcher excludes these from rebase dispatch so a hopeless pairing is never re-attempted.
|
|
90
|
+
|
|
91
|
+
Both are keyed on the pair, not the head alone, because whether a branch conflicts depends on both sides. Under a head-only key, a base that moves produces a genuinely new conflict against an unchanged head — and the stale entry suppresses it permanently, because nothing can change the head while the branch sits blocked. Pairing re-arms the budget whenever either side moves. Legacy entries written before this change are bare SHAs with no `..`; readers ignore them, which un-wedges any slot they had blocked.
|
|
92
|
+
|
|
93
|
+
Unlike these, `ci_fix_attempts` / `ci_escalated_shas` stay keyed on the head SHA alone — a CI result is a function of the head only, so base movement must not re-arm them.
|
|
94
|
+
- `blocked`: present only while the watcher is **awaiting the owner** on a PR that cannot progress without a human ([`contract.md`](contract.md) Step 7). Absent ⇒ the watcher is in its normal dispatch flow. When present, the watcher **keeps the normal `1m` cadence** and each tick is a reminder-or-resume check ([`contract.md`](contract.md) Step 2.5): it re-emits a one-line reminder to the owner, recomputes the `fingerprint`, and clears the block the moment any component moves. Its value is the reason-specific reminder plus fingerprint auto-resume.
|
|
95
|
+
- `reason`: which durable block is being awaited — `conflict_escalated` (`rebase_key` ∈ `conflict_escalated_keys`), `ci_escalated` (`head_sha` ∈ `ci_escalated_shas`), or `reviews_escalated` (a review sits in `escalated_review_ids` awaiting the user, actionable set empty). Selects the reminder wording; the resume decision is fingerprint-driven, not reason-driven.
|
|
96
|
+
- `since`: when the block was first flagged — lets the reminder state how long the owner has been the blocker.
|
|
97
|
+
- `fingerprint`: the external state the block is waiting on. `head_sha` moves on a new push (which also clears the per-SHA escalation sets, keyed by SHA); `latest_review_id` is `max(id)` over submitted reviews and moves when a reviewer submits anything new; `ci_digest` is a stable digest of the head SHA's CI rollup (bucket + each check's name/conclusion, sorted) and moves when a check flips, a rerun lands, or an external check such as a staging deploy posts. Any change clears the block and resumes evaluation.
|
|
98
|
+
|
|
99
|
+
## `state.md`
|
|
100
|
+
|
|
101
|
+
Free-form markdown. No required schema beyond a few well-known fields the caller-agnostic code reads:
|
|
102
|
+
|
|
103
|
+
```markdown
|
|
104
|
+
# Session — <slug>
|
|
105
|
+
|
|
106
|
+
**PR:** <url>
|
|
107
|
+
**Slug:** <slug>
|
|
108
|
+
**Loop user:** <github-login> ← cached from `gh api user`
|
|
109
|
+
**Created:** <ISO-8601>
|
|
110
|
+
**Bootstrapped from URL:** <yes | no>
|
|
111
|
+
|
|
112
|
+
## Pre-flight answers
|
|
113
|
+
|
|
114
|
+
- Validation: <local-e2e | staging-replay | unit-only | skip>
|
|
115
|
+
- Local URL: <url | N/A>
|
|
116
|
+
- Backend status: <up | down | N/A>
|
|
117
|
+
- Muggle Test project: <name> (<uuid>)
|
|
118
|
+
- Test credentials: <existing | new | skip>
|
|
119
|
+
- Auth status: <ok | re-authed | N/A>
|
|
120
|
+
- Working tree: <path>
|
|
121
|
+
|
|
122
|
+
...free-form notes added by /muggle-do and bootstrap...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `## Pre-flight answers` block is the **E2E validation context** consumed by `do/e2e-acceptance.md` Step 0 — seeded by bootstrap (Step 6.5) or by pre-flight's output block. Fields: [`../_shared/resolve-e2e-validation-context.md`](../_shared/resolve-e2e-validation-context.md#persisted-fields).
|
|
126
|
+
|
|
127
|
+
The watcher does **not** read or write `state.md`. Only bootstrap, `/muggle-do`, and the caller's stages touch it.
|
|
128
|
+
|
|
129
|
+
## `followup.log`
|
|
130
|
+
|
|
131
|
+
Append-only line-per-tick log. One line per watcher tick, plus one line per `/muggle-do` invocation. Format is loose, but each line starts with an ISO-8601 timestamp:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
2026-05-20T12:34:56Z tick pr=154 threads=0 idle
|
|
135
|
+
2026-05-20T12:35:56Z tick pr=154 threads=1 dispatched=4295962800
|
|
136
|
+
2026-05-20T12:36:14Z muggle-do cycle review_ids=[4295962800] outcome=pushed head_sha=abc1234
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Used for forensics only — never read back by skills.
|
|
140
|
+
|
|
141
|
+
## `result.md`
|
|
142
|
+
|
|
143
|
+
Written exactly once when the PR's watcher exits terminally (PR merged or closed). Free-form markdown summarizing the PR's life under this loop:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
# Result — <slug>
|
|
147
|
+
|
|
148
|
+
**PR:** <url>
|
|
149
|
+
**Final state:** merged | closed
|
|
150
|
+
**Cycles completed:** <int>
|
|
151
|
+
**Pushed SHAs:** <comma-separated list>
|
|
152
|
+
**Escalated review ids:** <comma-separated list or "none">
|
|
153
|
+
|
|
154
|
+
## Timeline
|
|
155
|
+
|
|
156
|
+
- <ISO-8601> bootstrap (lastBodyReviewId <id>; line-comment threads state-derived)
|
|
157
|
+
- <ISO-8601> review <id> from <login> — actionable, pushed <sha>
|
|
158
|
+
- <ISO-8601> review <id> from <login> — ambiguous, escalated
|
|
159
|
+
- ...
|
|
160
|
+
- <ISO-8601> PR <merged|closed> — watcher terminal
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Not in the slot
|
|
164
|
+
|
|
165
|
+
`cycle.json` and `requirements.md` are not seeded or read. `/muggle-do` reads reviews off GitHub each invocation.
|
|
166
|
+
|
|
167
|
+
## `watch-heartbeat`
|
|
168
|
+
|
|
169
|
+
Touched (mtime refreshed) by the watch loop every iteration — content irrelevant. The slot's liveness beacon: a quiet monitor writes no log lines, so mtime is the only proof it is still polling. Read by [`reconcile.md`](reconcile.md) Step 3.6; a beacon older than 15 minutes means the poller is dead.
|
|
170
|
+
|
|
171
|
+
## `watch-watermark.env`
|
|
172
|
+
|
|
173
|
+
The watch loop's comparison floor — plain `KEY=VALUE` lines, one file per slot:
|
|
174
|
+
|
|
175
|
+
- `REV` — highest submitted-review id already handled
|
|
176
|
+
- `COM` — highest thread-comment id already handled
|
|
177
|
+
- `THREADS` — semicolon-joined ids of threads already known unresolved
|
|
178
|
+
- `CIRED` — head SHA whose settled-red CI the drain already handled; empty when the checks are green, still pending, or unseen. The CI floor is a SHA rather than a monotonic id because the check rollup flips green↔red and resets on each push — keying on the head SHA fires the loop once per red head and re-arms on the next push ([`arm-watcher.md`](arm-watcher.md)).
|
|
179
|
+
|
|
180
|
+
Written whole-file by the orchestrating session — seeded at arm time to the **drain's own observed max** (the ids the drain read, snapshotted at that read) and advanced after every cycle to the **handled wave's snapshot** (the max review-id and comment-id the cycle read at the start of handling, captured before replies were posted). **Never** derived from a fresh fetch taken after the drain or after replies land: a later fetch includes ids that arrived in the handling window and were never read, and marking them seen swallows them ([`arm-watcher.md`](arm-watcher.md)). The advance also never moves any floor past an unresolved thread the cycle left unaddressed. Read by the watch loop each iteration; the loop never writes it. A stale watermark makes the next reported event the loop's own reply.
|
|
181
|
+
|
|
182
|
+
**Never `source` this file, and quote or extract values.** `THREADS` holds bare semicolons: sourced unquoted, the shell splits the line at the first `;` and silently drops every id after it — the loop then re-reports known threads as new. The watch loop must extract values verbatim (e.g. `grep '^THREADS=' | cut -d= -f2- | tr -d '"\r'`), tolerating quotes and CRLF; writers should quote the value anyway.
|
|
183
|
+
|
|
184
|
+
## `watch.pid`
|
|
185
|
+
|
|
186
|
+
A single line: the process id of the watch loop that currently owns this slot. Written once by the loop itself on start (`echo "$$" > watch.pid`); read by two parties, never rewritten in place:
|
|
187
|
+
|
|
188
|
+
- **arm-watcher's pre-arm dedup** ([`arm-watcher.md`](arm-watcher.md) Step 3) reads it and, if the PID is live (`kill -0`), skips arming — one live watcher per slot.
|
|
189
|
+
- **the loop's own supersede guard** (`watcher_superseded`, [`../../scripts/pr-watch-guards.sh`](../../scripts/pr-watch-guards.sh)) compares it to `$$` each iteration and exits when they differ — a newer arm that overwrote the file steps the old loop down.
|
|
190
|
+
|
|
191
|
+
This is the lease that keeps orphaned watchers from accumulating across sessions: the in-session monitor task dying does not stop the detached OS loop it launched (notably on Windows), so slot ownership is tracked by a durable PID on disk, not by the live task list. It is not seeded, migrated, or cleaned up — a stale PID (loop already dead) simply fails the `kill -0` liveness check and the next arm proceeds.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Stop Procedure
|
|
2
|
+
|
|
3
|
+
The procedure for the **stop mode** of `muggle-pr-followup` — the owner's kill switch. One command tears down every polling substrate a slot has (monitor, cron) and leaves the slot in a state where anything that survives teardown — an orphaned cron in an unreachable scheduler, a queued fire still draining — arrives as an inert one-line absorb ([`contract.md`](contract.md) Step 0). Routing is in [`SKILL.md`](SKILL.md#routing).
|
|
4
|
+
|
|
5
|
+
Stopping is not finalizing: the PR stays open on the provider, and no `result.md` is written. A stopped slot can be resumed later by renaming `<slug>.stopped` back to `<slug>` and re-arming per [`arm-watcher.md`](arm-watcher.md).
|
|
6
|
+
|
|
7
|
+
## Input
|
|
8
|
+
|
|
9
|
+
`$ARGUMENTS` is `stop`, optionally followed by a `<slug>` to scope to one slot. No slug stops **every** open slot and writes the global kill file.
|
|
10
|
+
|
|
11
|
+
## Procedure
|
|
12
|
+
|
|
13
|
+
Run per slot in scope (every dir under `~/.muggle-ai/muggle-do/sessions/` with a `prs.json` and no `result.md`, or just the named one):
|
|
14
|
+
|
|
15
|
+
1. **Stop the monitor.** `TaskStop` any running monitor whose command references this slot's `watch.sh`. On Windows also kill surviving grandchildren: `TaskStop` reaps the wrapper but the inner `bash <slot>/watch.sh` process can outlive it and keep touching `watch-heartbeat` — enumerate processes whose command line contains `<slug>/watch.sh` and kill them. A survivor here also blocks the Step 3 rename (open handles inside the dir).
|
|
16
|
+
2. **Cancel the cron.** Per [`cancel-cron.md`](cancel-cron.md) — recorded-id first, `CronList` match as fallback. A cron neither can reach is orphaned; it stays inert via the absorb and dies with its session.
|
|
17
|
+
3. **Mark the slot stopped.** Rename `~/.muggle-ai/muggle-do/sessions/<slug>/` → `<slug>.stopped/`. This single rename is the load-bearing act: the tick absorb keys on it and [`reconcile.md`](reconcile.md) skips `*.stopped` dirs — so no recovery path of any kind re-arms the slot.
|
|
18
|
+
|
|
19
|
+
Then, for a no-slug (stop-everything) invocation only:
|
|
20
|
+
|
|
21
|
+
4. **Write the global kill file.** `~/.muggle-ai/muggle-do/polling.disabled` (empty file). Ticks absorb on it before even resolving a slot, and reconcile's re-arm step refuses while it exists. Bootstrap deliberately ignores it — pasting a PR URL is an explicit request to watch, and it removes the kill file as its first act so the new watch isn't stillborn.
|
|
22
|
+
|
|
23
|
+
## Output
|
|
24
|
+
|
|
25
|
+
One line per slot (`stopped: <slug> — monitor <killed|none>, cron <cancelled|orphaned|none>`), plus `polling disabled globally` when the kill file was written. Report an orphaned cron honestly: it may keep firing one-line absorbs until its host session ends — that is the harness's floor, not a teardown failure.
|
|
26
|
+
|
|
27
|
+
## Invariants
|
|
28
|
+
|
|
29
|
+
- **Idempotent.** Stopping a stopped slot is a no-op; re-running never errors.
|
|
30
|
+
- **Nothing revives a stopped slot.** Tick, reconcile, and auto-track all treat `*.stopped` as invisible. Only the owner resumes one (rename back + re-arm), and only bootstrap overrides the global kill file.
|
|
31
|
+
- **Orphans are inert, never invisible.** The absorb line keeps a firing orphan observable without spending anything on it.
|
|
@@ -144,16 +144,22 @@ gh pr view --json number,url,title 2>/dev/null
|
|
|
144
144
|
|
|
145
145
|
### 3A.2: Post the body as a PR comment
|
|
146
146
|
|
|
147
|
-
Extract the `body` field with `jq -r` (not `sed`) so JSON escape sequences are properly decoded, then pipe to `--body-file
|
|
147
|
+
Extract the `body` field with `jq -r` (not `sed`) so JSON escape sequences are properly decoded, append the Muggle Works signature (command `/muggle-pr-visual-walkthrough`) per [`../_shared/vcs/post-signature.md`](../_shared/vcs/post-signature.md), then pipe to `--body-file -`. The renderer's sentinel stays at the top of `body`, so the report-format guardrail still recognises the post:
|
|
148
148
|
|
|
149
149
|
```bash
|
|
150
|
-
|
|
150
|
+
{
|
|
151
|
+
jq -r '.body' /tmp/muggle-pr-section.json
|
|
152
|
+
printf '\n\n%s\n' '🤖 _Posted by `/muggle-pr-visual-walkthrough` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_'
|
|
153
|
+
} | gh pr comment <pr-number> --body-file -
|
|
151
154
|
```
|
|
152
155
|
|
|
153
156
|
### 3A.3: Post the overflow comment only if the CLI emitted one
|
|
154
157
|
|
|
155
158
|
```bash
|
|
156
|
-
|
|
159
|
+
{
|
|
160
|
+
jq -r '.comment' /tmp/muggle-pr-section.json
|
|
161
|
+
printf '\n\n%s\n' '🤖 _Posted by `/muggle-pr-visual-walkthrough` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_'
|
|
162
|
+
} | gh pr comment <pr-number> --body-file -
|
|
157
163
|
```
|
|
158
164
|
|
|
159
165
|
**Skip this step entirely if `comment` is `null`** — do not post a placeholder. The CLI decides fit-vs-overflow; never post the overflow comment speculatively.
|
|
@@ -172,10 +178,13 @@ Instead of posting, **return** the CLI output to the caller's context so they ca
|
|
|
172
178
|
|
|
173
179
|
1. **Embed `body`** in their PR body, concatenated after `## Changes`. `body` already includes its own `## E2E Acceptance Results` header — do not add another.
|
|
174
180
|
2. **Create the PR** with `gh pr create --title "..." --body "..."` using the concatenated body.
|
|
175
|
-
3. **Post `comment` as a follow-up only if the CLI emitted one
|
|
181
|
+
3. **Post `comment` as a follow-up only if the CLI emitted one**, ending the posted body with the signature (the caller owns this post, so command `/muggle-do`) per [`../_shared/vcs/post-signature.md`](../_shared/vcs/post-signature.md):
|
|
176
182
|
|
|
177
183
|
```bash
|
|
178
|
-
|
|
184
|
+
{
|
|
185
|
+
jq -r '.comment' /tmp/muggle-pr-section.json
|
|
186
|
+
printf '\n\n%s\n' '🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_'
|
|
187
|
+
} | gh pr comment <new-pr-number> --body-file -
|
|
179
188
|
```
|
|
180
189
|
|
|
181
190
|
Skip if `comment` is `null`.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Start services
|
|
2
2
|
|
|
3
|
-
For each service, launch in the background:
|
|
3
|
+
For each service, launch in the background through the shipped process cap: wrap the service's start command as `node <plugin-root>/scripts/guard-run.mjs --service -- <command>` (resolve `<plugin-root>` from `CLAUDE_PLUGIN_ROOT`). The wrapper is plain node and picks the cap mechanism per OS itself, so the instruction is the same everywhere; it kernel-bounds the service's process tree (a runaway spawn loop dies at the cap, not the machine) while letting the service outlive this session. From the Bash tool:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
cd "<service-dir>" && nohup <command> > /tmp/muggle-prepare-<service-name>.log 2>&1 &
|
|
6
|
+
cd "<service-dir>" && nohup node "${CLAUDE_PLUGIN_ROOT}/scripts/guard-run.mjs" --service -- <command> > /tmp/muggle-prepare-<service-name>.log 2>&1 &
|
|
7
7
|
echo $!
|
|
8
8
|
```
|
|
9
9
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release": "5.
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-
|
|
2
|
+
"release": "5.8.0",
|
|
3
|
+
"buildId": "run-67-1",
|
|
4
|
+
"commitSha": "f0fbb486a0d7886d880eaec3129bc53964524f01",
|
|
5
|
+
"buildTime": "2026-08-01T07:54:45Z",
|
|
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
|
+
"version": "5.8.0",
|
|
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",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
"eval:studio-gen": "tsx internal/studio-gen-eval/src/run.ts"
|
|
47
47
|
},
|
|
48
48
|
"muggleConfig": {
|
|
49
|
-
"electronAppVersion": "1.6.
|
|
49
|
+
"electronAppVersion": "1.6.11",
|
|
50
50
|
"downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
|
|
51
51
|
"runtimeTargetDefault": "production",
|
|
52
52
|
"checksums": {
|
|
53
|
-
"darwin-arm64": "
|
|
54
|
-
"darwin-x64": "
|
|
55
|
-
"linux-x64": "
|
|
56
|
-
"win32-x64": "
|
|
53
|
+
"darwin-arm64": "97f7f5ba622dc3baf86457184bc44030c777d7dbac58b8c1c5418970662e7a07",
|
|
54
|
+
"darwin-x64": "c8c2745282583144b34110ef45c4745e03c03c4ac2e020c820daa51124c3fc50",
|
|
55
|
+
"linux-x64": "d85e14ed38cb8035b932aef26148934e3aec6e036b6bb80ff8927cbde901a935",
|
|
56
|
+
"win32-x64": "6e38a5b017ee5756985e1507def6c5a92d357a4bbd4e4032b6e490ad05dd09a2"
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"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
|
+
"version": "5.8.0",
|
|
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.
|
|
5
|
+
"version": "5.8.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/plugin/hooks/README.md
CHANGED
|
@@ -20,7 +20,7 @@ A guardrail emits one of two strengths:
|
|
|
20
20
|
- **Advise** — `additionalContext` (PostToolUse/UserPromptSubmit) or a plain Stop message. A soft nudge the model can ignore.
|
|
21
21
|
- **Enforce** — a `Stop` `decision: "block"` that refuses to end the turn, or a `PreToolUse` `permissionDecision: "deny"` that refuses a tool call. The model cannot proceed until the condition is met.
|
|
22
22
|
|
|
23
|
-
Enforcement is reserved for the handoffs that were being skipped: the E2E acceptance run
|
|
23
|
+
Enforcement is reserved for the handoffs that were being skipped: the E2E acceptance run, posting a deterministically-rendered report, and the post-merge handoff. Each enforcing gate carries an escape so it can't trap a turn — the E2E gate accepts an explicit skip declaration (`echo "MUGGLE_E2E_SKIP: <reason>"`, session-durable) and hard-releases after `MAX_E2E_BLOCKS` (3) blocks; the report gate only denies a body it can positively see is a hand-written report and fails open otherwise; the post-merge gate hard-releases after `MAX_PR_TERMINAL_BLOCKS` (3) blocks, and only the AskUserQuestion next-options offer clears it — nothing else resets its counter.
|
|
24
24
|
|
|
25
25
|
## Mechanism
|
|
26
26
|
|
|
@@ -31,13 +31,25 @@ Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json
|
|
|
31
31
|
| Hook event | Wrapper | Strength | Condition | Preference | Effect |
|
|
32
32
|
| :--------- | :------ | :------- | :-------- | :--------- | :----- |
|
|
33
33
|
| `PostToolUse` (Bash) | `guardrail-pr-opened.sh` | advise | a `gh pr create`/`gh pr ready` just succeeded | `autoWatchPR` | start a `muggle-pr-followup` watcher on the new PR |
|
|
34
|
-
| `PostToolUse` (Bash + muggle execute/replay MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, an E2E run happened, or an `echo "MUGGLE_E2E_SKIP: <reason>"` marker declared E2E un-runnable | — | set `unitTestsGreen` / `e2eRun` / `e2eSkipped` session state |
|
|
34
|
+
| `PostToolUse` (Bash + muggle execute/replay/skill-emit MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, an E2E run happened (execute/replay call, or the muggle-test skill's own telemetry emit — which registers a clean SKIP verdict too), or an `echo "MUGGLE_E2E_SKIP: <reason>"` marker declared E2E un-runnable | — | set `unitTestsGreen` / `e2eRun` / `e2eSkipped` session state |
|
|
35
|
+
| `PostToolUse` (Bash + Monitor) | `guardrail-pr-terminal.sh` | advise | a PR just went terminal — a `gh pr merge`/`gh pr close` success line or the watch monitor's `TERMINAL pr=N` exit line (never bare `"state":"MERGED"` metadata) | — | record `terminalPending`, direct the post-merge handoff: finalize the watcher slot, tear down per `autoCleanup`, offer next options via AskUserQuestion |
|
|
36
|
+
| `PostToolUse` (AskUserQuestion) | `guardrail-offer-ran.sh` | record | a next-options offer ran while a terminal PR was pending | — | clear `terminalPending` — the only exit for the post-merge Stop gate |
|
|
35
37
|
| `PreToolUse` (Bash) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
|
|
36
38
|
| `Stop` | `guardrail-e2e-gate.sh` | **enforce** | unit tests passed this session, no E2E ran yet, and no skip was recorded | `autoE2ETest` | **block** the turn until E2E runs via `muggle-test` or a `MUGGLE_E2E_SKIP` marker records a legitimate skip (full message once, one-line reminders after; releases after 3 blocks) |
|
|
39
|
+
| `Stop` | `guardrail-terminal-gate.sh` | **enforce** | a PR went terminal this session and the AskUserQuestion next-options offer hasn't run since | — | **block** the turn until the post-merge handoff runs (full message once, one-line reminders after; releases after 3 blocks; nothing but the offer resets the counter) |
|
|
37
40
|
| `UserPromptSubmit` | `guardrail-build-router.sh` | advise | a build/implement/fix request (first one this session) | `autoRouteBuildToMuggleDo` | route the work through `muggle-do` (build delegated to superpowers) |
|
|
38
41
|
|
|
39
42
|
## Session-start reconcile nudge
|
|
40
43
|
|
|
41
44
|
`SessionStart` (`scripts/reconcile-stale-watchers.sh`) — a standalone advisory, not part of the `guardrails.mjs` decision tree above.
|
|
42
45
|
|
|
43
|
-
`muggle-pr-followup` watchers are session-only `/loop`
|
|
46
|
+
`muggle-pr-followup` watchers are session-only (a monitor or `/loop` cron); they die with their session, leaving open PRs with no live poller. This is by design — a review is addressed only inside a session that carries the context to address it. The skill's [`reconcile`](../skills/muggle-pr-followup/reconcile.md) procedure recovers them at the next session start — finalizes slots whose PR went terminal, sweeps orphan crons, re-arms silently-stopped open watchers — but re-arming needs Claude tools a shell hook can't call. So this hook nudges rather than acts: it scans `~/.muggle-ai/muggle-do/sessions/*/` for open slots (a `prs.json` with no `result.md`) and, **only when one or more exist**, injects `additionalContext` telling the agent to run `/muggle:muggle-pr-followup reconcile`. Zero open slots → it emits nothing. A pure directory scan (no `gh`, no writes), so it's cheap enough for every session start.
|
|
47
|
+
|
|
48
|
+
## Session-start state GC
|
|
49
|
+
|
|
50
|
+
`SessionStart` (`scripts/gc-state.sh`) — prunes ephemeral state that nothing else garbage-collects, so it doesn't grow without bound (the per-session guardrails files and finalized watcher slots otherwise accumulate one-per-session forever). Collection is keyed on **inactivity, never creation age**, so state a long-lived session still relies on is never deleted out from under it:
|
|
51
|
+
|
|
52
|
+
- `~/.muggle-ai/guardrails/*.json` (one per session) is pruned only after 14 days of **no activity**. An in-use session keeps rewriting its file (`guardrails.mjs` on every guarded tool call) and this hook refreshes it on every resume, so its mtime tracks last activity — a session that runs for months stays live indefinitely, and only one that has genuinely gone quiet for the whole window (i.e. ended) is collected.
|
|
53
|
+
- Finalized watcher slots (`result.md` present) are pruned 30 days after finalize; their `followup.log` is forensic-only. An **open** slot — a PR watched for any length of time — has no `result.md` and is never touched.
|
|
54
|
+
|
|
55
|
+
Both windows are overridable (`MUGGLE_GUARDRAILS_TTL_DAYS`, `MUGGLE_SLOT_TTL_DAYS`). TTL-gated to once per day via a `~/.cache/muggle/state-gc-checked` marker (the current-session refresh runs every start regardless); silent and best-effort, never blocks session start. Never touches an open slot or the current session's own state.
|