@muggleai/works 5.7.0 → 5.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{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/agents/test-prepare-runner.md +47 -0
- package/dist/plugin/agents/visual-walkthrough-builder.md +58 -0
- package/dist/plugin/hooks/README.md +11 -2
- package/dist/plugin/hooks/hooks.json +7 -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-record-tests.sh +5 -4
- package/dist/plugin/scripts/guardrails.mjs +6 -3
- package/dist/plugin/scripts/pr-watch-guards.sh +46 -0
- package/dist/plugin/skills/CLAUDE.md +39 -36
- 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 +41 -227
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +130 -129
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
- package/dist/release-manifest.json +4 -4
- package/package.json +7 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/agents/test-prepare-runner.md +47 -0
- package/plugin/agents/visual-walkthrough-builder.md +58 -0
- package/plugin/hooks/README.md +11 -2
- package/plugin/hooks/hooks.json +7 -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-record-tests.sh +5 -4
- package/plugin/scripts/guardrails.mjs +6 -3
- package/plugin/scripts/pr-watch-guards.sh +46 -0
- package/plugin/skills/CLAUDE.md +39 -36
- 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 +41 -227
- package/plugin/skills/muggle-test-prepare/SKILL.md +130 -129
- 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
|
@@ -22,7 +22,7 @@ Skip `autoCreatePR` (it gates creation, not update). The PR's title is left inta
|
|
|
22
22
|
|
|
23
23
|
Resolve the provider once per [`../../_shared/vcs/detect-vcs.md`](../../_shared/vcs/detect-vcs.md). Wherever Steps 3–4 below edit title/description: `github` uses `gh pr edit` per [`../../_shared/vcs/github/pr-edit.md`](../../_shared/vcs/github/pr-edit.md); `gitlab` uses `glab mr update --title --description` per [`../../_shared/vcs/gitlab/mr-edit.md`](../../_shared/vcs/gitlab/mr-edit.md).
|
|
24
24
|
|
|
25
|
-
1. **Push:** per [`../../_shared/vcs/
|
|
25
|
+
1. **Push:** per [`../../_shared/vcs/common/push-to-branch.md`](../../_shared/vcs/common/push-to-branch.md). Capture the new SHA.
|
|
26
26
|
|
|
27
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
|
|
|
@@ -32,11 +32,11 @@ Resolve the provider once per [`../../_shared/vcs/detect-vcs.md`](../../_shared/
|
|
|
32
32
|
- Validation now ran (was unit-only/skip, now has E2E report) → strip `[UNVERIFIED]` or `[UNIT-ONLY]`.
|
|
33
33
|
- Otherwise → no title change.
|
|
34
34
|
|
|
35
|
-
4. **Refresh body when validation outcome changed** — only when the `## Validation` section's content differs from what's in the body. Use the `--body-file` form in [`../../_shared/vcs/github/pr-edit.md`](../../_shared/vcs/github/pr-edit.md). Preserve `## Goal` and `## Acceptance Criteria` verbatim.
|
|
35
|
+
4. **Refresh body when validation outcome changed** — only when the `## Validation` section's content differs from what's in the body. Use the `--body-file` form in [`../../_shared/vcs/github/pr-edit.md`](../../_shared/vcs/github/pr-edit.md). Preserve `## Goal` and `## Acceptance Criteria` verbatim. Re-stamp the signature: delete the existing block from the `<!-- muggle-works:signature -->` marker to the end, then append the editable-body signature (command `/muggle-do`) as the last thing in the body per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md) — this keeps exactly one signature across refreshes.
|
|
36
36
|
|
|
37
37
|
5. **Visual walkthrough comment** — only when an E2E report exists. Fire [`postPRVisualWalkthrough`](../../muggle-preferences/preference-gates/postPRVisualWalkthrough.md) (PR number from `prs.json`); on skip, record `skipped (gate)` and continue. Otherwise invoke [`../../muggle-pr-visual-walkthrough/SKILL.md`](../../muggle-pr-visual-walkthrough/SKILL.md) Mode A — a fresh comment per cycle; do not edit prior walkthrough comments.
|
|
38
38
|
|
|
39
|
-
6. **Overflow comment** — same rule as forward mode: post when the walkthrough skill returns non-null `comment`, via [`../../_shared/vcs/github/top-level-comment.md`](../../_shared/vcs/github/top-level-comment.md).
|
|
39
|
+
6. **Overflow comment** — same rule as forward mode: post when the walkthrough skill returns non-null `comment`, via [`../../_shared/vcs/github/top-level-comment.md`](../../_shared/vcs/github/top-level-comment.md). End the posted body with the signature line (command `/muggle-do`) per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md).
|
|
40
40
|
|
|
41
41
|
## Handoff
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ Reply body uses the template in [`../muggle-pr-followup/output-templates/inline-
|
|
|
44
44
|
Addressed in <short-sha>: <one-line summary of the change made for THIS comment>.
|
|
45
45
|
|
|
46
46
|
<!-- muggle-do:bot -->
|
|
47
|
-
🤖
|
|
47
|
+
🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
`<short-sha>` is the first 7 chars of `new_sha`; the body must contain that substring so the resolve-reminder stage knows which push addressed the thread. The trailing signature block is mandatory — its `<!-- muggle-do:bot -->` marker is what identifies the reply as loop-authored (see [`../_shared/pr-followup-helpers/loop-signature.md`](../_shared/pr-followup-helpers/loop-signature.md)).
|
|
@@ -60,7 +60,7 @@ If an actionable review has a non-empty `body` and **zero** line comments, GitHu
|
|
|
60
60
|
Re: review #<review_id> — addressed in <short-sha>: <one-line summary>.
|
|
61
61
|
|
|
62
62
|
<!-- muggle-do:bot -->
|
|
63
|
-
🤖
|
|
63
|
+
🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Posted per [`../_shared/vcs/github/top-level-comment.md`](../_shared/vcs/github/top-level-comment.md). Fires at most once per actionable review-with-no-line-comments. Does not fire if the review has line comments — Step 2 covers those.
|
|
@@ -1,58 +1,59 @@
|
|
|
1
|
-
# Rebase (watcher-dispatched)
|
|
2
|
-
|
|
3
|
-
Rebase a PR's branch onto its base — whether it's merely **behind** (out of date, no conflict) or actually **conflicting** — behind a verify-or-rollback gate, then force-push, so a PR doesn't sit stale or un-mergeable forever. A dumb-pipe dispatch like fix-ci: the watcher detects the branch is behind or conflicting and hands off; the executor owns the rebase (and any conflict resolution), never the decision to dispatch.
|
|
4
|
-
|
|
5
|
-
## Turn preamble
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
**/muggle-do rebase** — rebasing <owner>/<repo>#<n> onto <base> to bring the branch up to date.
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Input
|
|
12
|
-
|
|
13
|
-
`$ARGUMENTS` carries a `github.com/.../pull/<n>` URL, `slug=<slug>`, and a `rebase` directive (no review ids, no failing check names). Parse all three.
|
|
14
|
-
|
|
15
|
-
## Inputs from disk
|
|
16
|
-
|
|
17
|
-
From `~/.muggle-ai/muggle-do/sessions/<slug>/`: `prs.json` (PR + branch + `head_sha`), `last_seen.json` (`conflict_resolve_attempts`, `conflict_escalated_keys`, `pushed_shas`), `state.md` (worktree path, validation strategy, base branch).
|
|
18
|
-
|
|
19
|
-
## Procedure
|
|
20
|
-
|
|
21
|
-
### Step 1 — Re-attach
|
|
22
|
-
|
|
23
|
-
Materialize the PR branch in its worktree per [`../_shared/pr-branch-worktree.md`](../_shared/pr-branch-worktree.md) (or use `state.md`'s `worktreePath`). Capture `rebase_sha = prs.json[0].head_sha` and the base branch (`baseRefName` from [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md)). Capture the base tip too — `.base_commit.sha` from that recipe's `compare` call — and form `rebase_key = "<rebase_sha>..<base_tip_sha>"`. Both budget fields below are keyed on that pair, matching the watcher's Step 5; writing a bare SHA instead leaves an entry the watcher ignores, and the rebase re-dispatches forever.
|
|
24
|
-
|
|
25
|
-
### Step 2 — Rebase onto base (resolve conflicts if any)
|
|
26
|
-
|
|
27
|
-
Run the rebase from [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before-e2e.md) against the base branch, taking its `always` path unconditionally — this programmatic mode never asks, so skip the `autoRebase` prompt (the watcher already decided a rebase is due).
|
|
28
|
-
|
|
29
|
-
- **Clean replay** — a behind-only branch (and any rebase that hits no conflicts) replays without intervention. Proceed to Step 3.
|
|
30
|
-
- **Conflicts** — handle per [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
|
|
31
|
-
- `never` → abort and escalate per Step 5 (`kind: "rebase-conflict"`). The watcher keeps polling; the user resolves on GitHub.
|
|
32
|
-
- `always` → resolve via [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md); Steps 3 and 5 below are this mode's instance of the [`../_shared/verify-or-rollback-gate.md`](../_shared/verify-or-rollback-gate.md).
|
|
33
|
-
|
|
34
|
-
### Step 3 — Verify the resolution
|
|
35
|
-
|
|
36
|
-
Build (typecheck + lint on the changed surface) + unit suite must pass. Run E2E per [`e2e-acceptance.md`](e2e-acceptance.md) when app logic changed and the session carries validation context. A rebase that does not verify is rolled back → escalate per Step 5. **Never push an unverified rebase.**
|
|
37
|
-
|
|
38
|
-
### Step 4 — Force-push + respawn
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
### Step 5 — Escalate (can't resolve / budget spent)
|
|
43
|
-
|
|
44
|
-
When `autoResolveConflicts=never`, the resolution failed verification, or `conflict_resolve_attempts[rebase_key]` has reached 2:
|
|
45
|
-
|
|
46
|
-
1. Add `rebase_key` to `last_seen.conflict_escalated_keys` so the watcher does not re-dispatch this head against this base. If the base later moves, the key changes and the branch re-arms on its own — a conflict the user resolved upstream stops being this watcher's dead end.
|
|
47
|
-
2. Emit one terminal escalation naming the PR and the conflicting files (or the failing verification, for a behind-only rebase that didn't verify).
|
|
48
|
-
3. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md) — it keeps polling for the user's manual resolution or any new reviews.
|
|
49
|
-
|
|
50
|
-
### Step 6 — Telemetry
|
|
51
|
-
|
|
52
|
-
Emit one `muggle-do:cycle` event ([`../_shared/telemetry-events/muggle-do-cycle.md`](../_shared/telemetry-events/muggle-do-cycle.md)) with `outcome: "rebased"` (a verified rebase pushed — behind-only or conflicts resolved) or `"rebase-escalated"`.
|
|
53
|
-
|
|
54
|
-
## Guardrails
|
|
55
|
-
|
|
56
|
-
- Max 2 rebase attempts per SHA; then escalate rather than churn.
|
|
57
|
-
- Never push an unverified rebase — verify-or-rollback always.
|
|
58
|
-
-
|
|
1
|
+
# Rebase (watcher-dispatched)
|
|
2
|
+
|
|
3
|
+
Rebase a PR's branch onto its base — whether it's merely **behind** (out of date, no conflict) or actually **conflicting** — behind a verify-or-rollback gate, then force-push, so a PR doesn't sit stale or un-mergeable forever. A dumb-pipe dispatch like fix-ci: the watcher detects the branch is behind or conflicting and hands off; the executor owns the rebase (and any conflict resolution), never the decision to dispatch.
|
|
4
|
+
|
|
5
|
+
## Turn preamble
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
**/muggle-do rebase** — rebasing <owner>/<repo>#<n> onto <base> to bring the branch up to date.
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Input
|
|
12
|
+
|
|
13
|
+
`$ARGUMENTS` carries a `github.com/.../pull/<n>` URL, `slug=<slug>`, and a `rebase` directive (no review ids, no failing check names). Parse all three.
|
|
14
|
+
|
|
15
|
+
## Inputs from disk
|
|
16
|
+
|
|
17
|
+
From `~/.muggle-ai/muggle-do/sessions/<slug>/`: `prs.json` (PR + branch + `head_sha`), `last_seen.json` (`conflict_resolve_attempts`, `conflict_escalated_keys`, `pushed_shas`), `state.md` (worktree path, validation strategy, base branch).
|
|
18
|
+
|
|
19
|
+
## Procedure
|
|
20
|
+
|
|
21
|
+
### Step 1 — Re-attach
|
|
22
|
+
|
|
23
|
+
Materialize the PR branch in its worktree per [`../_shared/pr-branch-worktree.md`](../_shared/pr-branch-worktree.md) (or use `state.md`'s `worktreePath`). Capture `rebase_sha = prs.json[0].head_sha` and the base branch (`baseRefName` from [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md)). Capture the base tip too — `.base_commit.sha` from that recipe's `compare` call — and form `rebase_key = "<rebase_sha>..<base_tip_sha>"`. Both budget fields below are keyed on that pair, matching the watcher's Step 5; writing a bare SHA instead leaves an entry the watcher ignores, and the rebase re-dispatches forever.
|
|
24
|
+
|
|
25
|
+
### Step 2 — Rebase onto base (resolve conflicts if any)
|
|
26
|
+
|
|
27
|
+
Run the rebase from [`../_shared/rebase-before-e2e.md`](../_shared/rebase-before-e2e.md) against the base branch, taking its `always` path unconditionally — this programmatic mode never asks, so skip the `autoRebase` prompt (the watcher already decided a rebase is due).
|
|
28
|
+
|
|
29
|
+
- **Clean replay** — a behind-only branch (and any rebase that hits no conflicts) replays without intervention. Proceed to Step 3.
|
|
30
|
+
- **Conflicts** — handle per [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
|
|
31
|
+
- `never` → abort and escalate per Step 5 (`kind: "rebase-conflict"`). The watcher keeps polling; the user resolves on GitHub.
|
|
32
|
+
- `always` → resolve via [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md); Steps 3 and 5 below are this mode's instance of the [`../_shared/verify-or-rollback-gate.md`](../_shared/verify-or-rollback-gate.md).
|
|
33
|
+
|
|
34
|
+
### Step 3 — Verify the resolution
|
|
35
|
+
|
|
36
|
+
Build (typecheck + lint on the changed surface) + unit suite must pass. Run E2E per [`e2e-acceptance.md`](e2e-acceptance.md) when app logic changed and the session carries validation context. A rebase that does not verify is rolled back → escalate per Step 5. **Never push an unverified rebase.**
|
|
37
|
+
|
|
38
|
+
### Step 4 — Force-push + respawn
|
|
39
|
+
|
|
40
|
+
Force-push per [`../_shared/vcs/common/push-to-branch.md`](../_shared/vcs/common/push-to-branch.md) (tool-agnostic instruction; handles the signing gate for rebased commits and directs to provider-specific force-push paths). Use `git push --force-with-lease` for the force operation. Append the new SHA to `last_seen.pushed_shas`; increment `last_seen.conflict_resolve_attempts[rebase_key]` — both whole-file rewrites (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md), never the Edit tool. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md). Its next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
|
|
41
|
+
|
|
42
|
+
### Step 5 — Escalate (can't resolve / budget spent)
|
|
43
|
+
|
|
44
|
+
When `autoResolveConflicts=never`, the resolution failed verification, or `conflict_resolve_attempts[rebase_key]` has reached 2:
|
|
45
|
+
|
|
46
|
+
1. Add `rebase_key` to `last_seen.conflict_escalated_keys` so the watcher does not re-dispatch this head against this base. If the base later moves, the key changes and the branch re-arms on its own — a conflict the user resolved upstream stops being this watcher's dead end.
|
|
47
|
+
2. Emit one terminal escalation naming the PR and the conflicting files (or the failing verification, for a behind-only rebase that didn't verify).
|
|
48
|
+
3. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md) — it keeps polling for the user's manual resolution or any new reviews.
|
|
49
|
+
|
|
50
|
+
### Step 6 — Telemetry
|
|
51
|
+
|
|
52
|
+
Emit one `muggle-do:cycle` event ([`../_shared/telemetry-events/muggle-do-cycle.md`](../_shared/telemetry-events/muggle-do-cycle.md)) with `outcome: "rebased"` (a verified rebase pushed — behind-only or conflicts resolved) or `"rebase-escalated"`.
|
|
53
|
+
|
|
54
|
+
## Guardrails
|
|
55
|
+
|
|
56
|
+
- Max 2 rebase attempts per SHA; then escalate rather than churn.
|
|
57
|
+
- Never push an unverified rebase — verify-or-rollback always.
|
|
58
|
+
- Never push unsigned commits — every push or force-push goes through the signing gate in [`../_shared/vcs/common/push-to-branch.md`](../_shared/vcs/common/push-to-branch.md).
|
|
59
|
+
- 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.
|
|
@@ -19,7 +19,7 @@ You receive:
|
|
|
19
19
|
|
|
20
20
|
For each repo:
|
|
21
21
|
|
|
22
|
-
1. **Run the test command** using Bash in the repo's directory. Use the provided test command (default: `pnpm test`).
|
|
22
|
+
1. **Run the test command** using Bash in the repo's directory. Use the provided test command (default: `pnpm test`). Suites must run in one-shot mode (`CI=true`, `--watchAll=false`, `vitest run`) — never watch mode: an orphaned watcher fork-bombs on Windows. Launch through the shipped cap so the process tree is kernel-bounded and reaped if this run is killed: `node "${CLAUDE_PLUGIN_ROOT}/scripts/guard-run.mjs" -- <test command>`.
|
|
23
23
|
2. **Capture the full output** — both stdout and stderr.
|
|
24
24
|
3. **Determine pass/fail** — exit code 0 means pass, anything else means fail.
|
|
25
25
|
4. **If tests fail**, extract the specific failing test names/descriptions from the output.
|
|
@@ -14,6 +14,7 @@ This folder holds the watcher loop that drives one PR toward merge-ready. The wa
|
|
|
14
14
|
- [`cancel-cron.md`](cancel-cron.md) — stops this watcher's cron, recorded-id-first (survives `CronList` going blind) with a `CronList`-match fallback, plus the tool-call-not-shell guard. Referenced by `contract.md` and `finalize.md`.
|
|
15
15
|
- [`record-cron-id.md`](record-cron-id.md) — the per-tick self-record that keeps this slot's cron id in `cron.json` deletable after a compaction blinds `CronList`. Referenced by `contract.md` Step 0.
|
|
16
16
|
- [`reconcile.md`](reconcile.md) — sweep that finalizes slots whose PR went terminal while polling lapsed, deletes orphaned crons, and re-arms open slots whose watcher stopped silently (dropped respawn); runs on demand, at the top of auto-track, and nudged by a session-start hook.
|
|
17
|
+
- [`stop.md`](stop.md) — the owner's kill switch: tears down monitor + cron per slot, renames the slot to `<slug>.stopped` (invisible to every recovery path), and — for stop-everything — writes the global kill file; surviving orphaned crons arrive as one-line absorbs.
|
|
17
18
|
- [`state-schemas.md`](state-schemas.md) — canonical JSON shapes of session state files.
|
|
18
19
|
- [`output-templates.md`](output-templates.md) — TOC of message templates; per-group files in `output-templates/`.
|
|
19
20
|
|
|
@@ -14,11 +14,13 @@ A watcher that babysits one open PR toward **merge-ready** — review threads ad
|
|
|
14
14
|
|
|
15
15
|
**Per-PR isolation.** One watcher per PR. Multi-PR work runs N independent watchers.
|
|
16
16
|
|
|
17
|
-
**Arming.** Bootstrap, auto-track, and the executor's post-cycle respawn all arm the watch the same way — one drain tick, then one persistent labeled monitor per PR — visible as a running task for as long as the PR is polled, gone at terminal ([`arm-watcher.md`](arm-watcher.md)). Crons
|
|
17
|
+
**Arming.** Bootstrap, auto-track, and the executor's post-cycle respawn all arm the watch the same way — one drain tick, then one persistent labeled monitor per PR — visible as a running task for as long as the PR is polled, gone at terminal ([`arm-watcher.md`](arm-watcher.md)). Crons only ever deliver a recovery tick, never the ongoing cadence: a cron-delivered tick re-arms the monitor and cancels its own cron ([`contract.md`](contract.md) Step 7.5), because every cron fire is a model turn while the monitor polls token-free.
|
|
18
18
|
|
|
19
19
|
**One reminder when blocked pending a human.** When a PR can't progress without the user — an escalated rebase/CI budget spent, or an ambiguous review awaiting direction — the tick that flags the block emits **one** one-line reminder: the pending act plus a reference back to the decision context ([`contract.md`](contract.md) Steps 2.5, 7). After that the watch stays visible and silent — no repeat nagging — and the block clears the instant a wake finds a push, review, or CI/deploy state moved.
|
|
20
20
|
|
|
21
|
-
**Cron lifecycle.** Each tick records its `/loop` cron id to `cron.json` while `CronList` can still see it ([`record-cron-id.md`](record-cron-id.md)), so teardown can delete the cron by id after a session continue / compaction blinds `CronList` to it. Reconcile ([`reconcile.md`](reconcile.md)) sweeps crons whose PR is terminal or whose slot is gone, and re-arms an open slot whose watcher stopped silently.
|
|
21
|
+
**Cron lifecycle.** Each tick records its `/loop` cron id to `cron.json` while `CronList` can still see it ([`record-cron-id.md`](record-cron-id.md)), so teardown can delete the cron by id after a session continue / compaction blinds `CronList` to it. Reconcile ([`reconcile.md`](reconcile.md)) sweeps crons whose PR is terminal or whose slot is gone, and re-arms an open slot whose watcher stopped silently — monitor-first, never with a recurring cron.
|
|
22
|
+
|
|
23
|
+
**Session death.** Monitors and crons are both session-bound — a session that ends or hits its usage limit takes every watch with it. That is by design: nothing polls out of session, because a review is addressed only inside a session that carries the context to address it, never by a headless process replying context-blind. The recovery point is the **next session start** — the `reconcile-stale-watchers.sh` hook nudges [`reconcile`](reconcile.md#triggers), which re-arms every open slot whose watcher is dead. A review landing while no session runs waits until then.
|
|
22
24
|
|
|
23
25
|
## Routing
|
|
24
26
|
|
|
@@ -27,14 +29,19 @@ The skill recognizes its mode by inspecting `$ARGUMENTS` and falling back to on-
|
|
|
27
29
|
| Input | On-disk check | Mode |
|
|
28
30
|
| :---- | :------------ | :--- |
|
|
29
31
|
| First arg matches `https?://github\.com/[^/]+/[^/]+/pull/\d+` | — | **bootstrap** → [`bootstrap.md`](bootstrap.md) |
|
|
30
|
-
| `<slug> <pr-number>` | session dir for `<slug>` exists | **tick** → [`contract.md`](contract.md) |
|
|
32
|
+
| `<slug> <pr-number>` | session dir for `<slug>` exists; slot's `watch.pid` is dead, or `--wake` passed | **tick** → [`contract.md`](contract.md) |
|
|
33
|
+
| `<slug> <pr-number>` | session dir for `<slug>` exists; `watch.pid` names a live process and no `--wake` | **watch-status** — report the slot from on-disk state (`prs.json` state + head, newest `followup.log` tick line, monitor liveness, watermark floors). **Zero provider calls:** a live monitor owns the cadence, and the session never polls unwoken. |
|
|
34
|
+
| `<slug> <pr-number>` | session dir missing, `<slug>.stopped` exists (or the global kill file `~/.muggle-ai/muggle-do/polling.disabled`) | **absorb** — one line, nothing else ([`contract.md`](contract.md) Step 0) |
|
|
31
35
|
| `<slug> <pr-number>` | session dir missing | **error:** "no session at `<path>`; pass a PR URL to start one" |
|
|
32
|
-
| `<
|
|
36
|
+
| `stop` (optional `<slug>`) | — | **stop** → [`stop.md`](stop.md) — tear down monitor + cron, mark slot(s) `.stopped`; no slug stops everything and writes the kill file |
|
|
37
|
+
| `<pr-number>` alone | exactly one existing session contains it | **tick** or **watch-status** for that PR, by the same `watch.pid` gate |
|
|
33
38
|
| `<pr-number>` alone | zero or multiple matches | **error:** ambiguous; list candidates and exit |
|
|
34
39
|
| empty | — | **auto-track** → [`auto-track.md`](auto-track.md) |
|
|
35
40
|
| `help` / `?` | — | **help:** list active loops per [`output-templates/help.md`](output-templates/help.md) |
|
|
36
41
|
| `reconcile` / `sweep` (optional `<slug>`) | — | **reconcile** → [`reconcile.md`](reconcile.md) |
|
|
37
42
|
|
|
43
|
+
**`--wake=<event>`** is passed only by an event wake's dispatch ([`arm-watcher.md`](arm-watcher.md) step 5) — it asserts the monitor already saw something new, so the tick's poll is justified. Manual invocations never pass it; with a live watcher they get watch-status, because a poll that nothing prompted is a main-session poll wasted. Recovery fires need no flag — a recoverable slot's watcher is dead by definition, so the gate falls through to tick. A stale cron firing against a re-armed slot hits the live-`watch.pid` branch and is absorbed as a status line, no provider calls.
|
|
44
|
+
|
|
38
45
|
Auto-track runs **reconcile** first, so a no-arg invocation also finalizes any slot whose PR merged or closed while its watcher was down (expired cron, ended session) and re-arms any open slot whose watcher stopped silently (a dropped respawn). Reconcile recovers a watcher that was already running; it never seeds a first watcher for a PR — that is auto-track's / bootstrap's job.
|
|
39
46
|
|
|
40
47
|
**Reconcile also runs at session start** — a `SessionStart` hook ([`../../hooks/README.md`](../../hooks/README.md)) surfaces the sweep when open slots exist, catching a watcher that died with its session (end, or 7-day `/loop` expiry) before a manual sweep would. See [`reconcile.md`](reconcile.md#triggers).
|
|
@@ -2,11 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
How an orchestrating session starts the watch on one PR. Every arming point runs this same sequence: [`bootstrap.md`](bootstrap.md) Step 8, [`auto-track.md`](auto-track.md) Step 6, and the executor's post-cycle settle.
|
|
4
4
|
|
|
5
|
-
1. **Drain.** Run one tick per [`contract.md`](contract.md). It acts on everything already outstanding — actionable threads, body-only reviews past the watermark, a stale branch, red CI — and finalizes a terminal PR. If the tick dispatched a cycle, stop here: the cycle's exit path settles the watch when it finishes.
|
|
6
|
-
2. **Seed the watermark.** Write the slot's watch watermark ([`state-schemas.md`](state-schemas.md#watch-watermarkenv))
|
|
7
|
-
3. **
|
|
8
|
-
4. **On event.** The monitor's line wakes the session: run the tick (step 1), which derives everything from live provider state; a terminal PR finalizes there while the monitor exits on its own. Persistent fetch failure → surface the reason; [`reconcile.md`](reconcile.md) re-arms the slot on its next sweep.
|
|
5
|
+
1. **Drain.** Run one tick per [`contract.md`](contract.md). It acts on everything already outstanding — actionable threads (`gitlab`: discussions), body-only reviews past the watermark (GitHub-only — GitLab has no review envelope), a stale branch, red CI — and finalizes a terminal PR. If the tick dispatched a cycle, stop here: the cycle's exit path settles the watch when it finishes.
|
|
6
|
+
2. **Seed the watermark.** Resolve the provider once per [`../_shared/vcs/detect-vcs.md`](../_shared/vcs/detect-vcs.md) — every fetch in this sequence uses that provider's recipes. Write the slot's watch watermark ([`state-schemas.md`](state-schemas.md#watch-watermarkenv)) to the ids the **drain itself read** — the max review-id and comment-id observed at the drain's own fetch (Step 1), snapshotted at that read. Never let the loop capture its own baseline — the arming session writes it; and **never** from a fresh fetch taken after the drain, which would include a comment that arrived after the drain read the wave and mark it seen unread. Seeded to the drain's floor, anything landing after that read stays above the watermark and the monitor's first iteration surfaces it. Seed the CI floor (`CIRED`) from the same drain read: set it to the head SHA when the checks have **already settled red** at that read (no check pending, one or more in the `fail` bucket per [`../_shared/vcs/common/ci-rollup.md`](../_shared/vcs/common/ci-rollup.md)) — that red is what the drain just handled — and empty otherwise, so an escalated red head the drain already saw does not re-fire on the loop's first iteration.
|
|
7
|
+
3. **Dedup, then watch.** First read `<slot>/watch.pid` ([`state-schemas.md`](state-schemas.md#watchpid)): if it names a live process (`kill -0 "$pid"` succeeds), a watcher already owns this slot — **skip arming, do not start a second**. This is what stops orphaned watchers from accumulating: the in-session monitor dying does not stop the OS loop it launched (on Windows a detached Git Bash loop keeps running and polling `gh` forever after the session ends), so checking a live task list is not enough — the PID lease is.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
Otherwise write the loop as `watch.sh` in the slot and start it as a **persistent background monitor** in the orchestrating session — the command is the one-line script path, and the label is `PR #<n> — <title>`. Both matter: some task surfaces show the label, others show the command, and a slot-path command keeps the watch identifiable everywhere a raw script blob would not. One monitor per PR, alive from arm to terminal: it is the watch's visible handle, showing as a running task the entire time the PR is polled. Its loop checks about every 60 seconds, re-reading the watermark and touching the slot's `watch-heartbeat` file each iteration — the liveness beacon that tells [`reconcile.md`](reconcile.md) a quiet watch is still alive; on a newer submitted review, a newer thread comment (`gitlab`: a newer discussion note), a thread newly unresolved (`gitlab`: discussion), or **the head SHA's checks settling red** — no check pending and one or more in the `fail` bucket per [`../_shared/vcs/common/ci-rollup.md`](../_shared/vcs/common/ci-rollup.md) — it prints one line and **keeps watching**, advancing its in-memory floor so each event fires the tick exactly once. The review and thread floors are monotonic ids; the CI floor is the **head SHA**, because the check rollup is non-monotonic — it flips green↔red and resets on every push — so recording the red head SHA fires CI once per red head, and a later push re-arms it on the new SHA. A head whose checks are still **pending** is never a wake: pending checks may yet go green, and the tick would idle on them (Step 6) regardless. Quiet iterations print nothing and cost nothing — no model tokens are spent while the watch is quiet.
|
|
10
|
+
|
|
11
|
+
The loop is **self-terminating**, so it never becomes one of those orphans. It begins by sourcing the shipped guard library and claiming the slot, with the guard path resolved to an **absolute** path at arm time (from `${CLAUDE_PLUGIN_ROOT}/scripts/pr-watch-guards.sh`) so it still resolves after the arming session is gone:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
GUARDS="<abs>/scripts/pr-watch-guards.sh"
|
|
15
|
+
[ -f "$GUARDS" ] && . "$GUARDS" || exit 0 # guards gone (plugin moved/upgraded) → a newer version's watcher owns this now
|
|
16
|
+
echo "$$" > "<slot>/watch.pid"
|
|
17
|
+
started=$(date +%s)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
and the top of every iteration, before any `gh` call, checks the two guards from [`../../scripts/pr-watch-guards.sh`](../../scripts/pr-watch-guards.sh):
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
watcher_superseded "<slot>" "$$" && exit 0 # a newer arm took the slot's watch.pid → this loop steps down
|
|
24
|
+
watcher_lifetime_exceeded "$started" "$(date +%s)" && exit 0 # 6h cap → any orphan dies on its own; reconcile re-arms an open PR in a live session
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
So it exits when the PR goes terminal, after five consecutive failed fetches, when a newer arm supersedes it, or once it passes the lifetime cap — never `while true` unbounded.
|
|
28
|
+
4. **On event.** Polling never enters the session: the cadence lives entirely in the detached monitor loop (step 3), and the only thing the session ever receives is the monitor's one event line. That line wakes the session: the wake turn runs the tick (step 1) with `--wake=<event>` — the flag that tells routing's live-watcher gate this poll was prompted, not idle curiosity ([`SKILL.md`](SKILL.md#routing)) — and whatever cycle the tick produces runs **inline in the owning session** — never in a subagent. The session's full history is part of the cycle's context (decisions, review nuance, owner phrasing); a subagent only knows its briefing plus disk/provider state, and anything the briefing omits is silently missing from the cycle. The token cost of inline cycles is accepted — context beats cost. The tick still derives everything from live provider state; a terminal PR finalizes there while the monitor exits on its own. Persistent fetch failure → surface the reason; [`reconcile.md`](reconcile.md) re-arms the slot at the next session start.
|
|
29
|
+
|
|
30
|
+
**After a cycle** — advance the watermark to the **handled wave's snapshot**, in the same dispatched context that ran the cycle. Capture the max review-id and comment-id **at the start of handling** — the ids the cycle actually read when it derived the wave, before any reply is posted — and advance the watermark to exactly those. **Never** re-derive it from a live-max fetched after the replies land: a reviewer comment that arrives during the handling window — between reading the wave and posting the replies — lands below that post-reply max and is marked seen without ever being read. Snapshotting at the start-of-handling read keeps such a comment above the watermark, so the next tick surfaces it. That advance is also what stops the loop from reporting its own reply; a cycle that skips it leaves the watermark stale, and the next event is an echo.
|
|
31
|
+
|
|
32
|
+
**Scan before advancing — a hard invariant.** Before writing the advanced watermark, confirm every unresolved review thread in the handled wave has been addressed: a reply posted, or an explicit deferral recorded. If any unresolved thread is still unaddressed, do **not** advance past it — leave its newest-comment id above the `COM` floor and its thread id out of the known-unresolved `THREADS` set, so the monitor's next iteration wakes on it and the next tick re-surfaces it. The watermark only ever moves over threads that were actually handled.
|
|
33
|
+
|
|
34
|
+
Arm a fresh monitor only when none is alive — judged by the `<slot>/watch.pid` lease (a live PID means a watcher already owns the slot), not by the in-session task list, since an orphaned OS loop outlives the session that launched it; the settle path never duplicates a running watch.
|
|
11
35
|
|
|
12
36
|
Drain-then-watch is the invariant: the tick handles everything up to the watermark with the real semantics — marker rule, watermark, attempt budgets — that the watch deliberately lacks, so the monitor only needs to see what arrives after it.
|
|
@@ -25,6 +25,8 @@ Bootstrap seeds watcher state and dispatches the first tick. The watcher itself
|
|
|
25
25
|
|
|
26
26
|
Extract `<owner>`, `<repo>`, `<pr-number>`. On malformed input, exit with the malformed-URL abort from [`output-templates/bootstrap.md`](output-templates/bootstrap.md).
|
|
27
27
|
|
|
28
|
+
If the global kill file `~/.muggle-ai/muggle-do/polling.disabled` exists, delete it and say so in one line — a pasted PR URL is an explicit request to watch ([`stop.md`](stop.md)), and leaving the kill file in place would make every tick of the new watch absorb itself stillborn.
|
|
29
|
+
|
|
28
30
|
### Step 2 — Fetch PR metadata
|
|
29
31
|
|
|
30
32
|
Per [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md).
|
|
@@ -33,7 +35,7 @@ If `state` is `MERGED` or `CLOSED`, exit with the terminal-PR abort. If the `gh`
|
|
|
33
35
|
|
|
34
36
|
### Step 3 — Verify the working tree
|
|
35
37
|
|
|
36
|
-
Per [`../_shared/vcs/
|
|
38
|
+
Per [`../_shared/vcs/common/verify-working-tree.md`](../_shared/vcs/common/verify-working-tree.md). On any check failure, exit with the wrong-working-tree abort.
|
|
37
39
|
|
|
38
40
|
### Step 4 — Resolve the slug
|
|
39
41
|
|