@muggleai/works 5.10.0 → 5.11.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-OCLMXAVJ.js → chunk-MSPMXBO5.js} +418 -326
- package/dist/{chunk-7NC2DJTK.js → chunk-WUMAJOBT.js} +9 -4
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- 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 +3 -1
- package/dist/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/dist/plugin/commands/mtest.md +1 -1
- package/dist/plugin/hooks/README.md +15 -3
- package/dist/plugin/hooks/hooks.json +79 -0
- package/dist/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/dist/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/dist/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/dist/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/dist/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/dist/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/dist/plugin/scripts/guardrail-report-format.sh +9 -5
- package/dist/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/dist/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/dist/plugin/scripts/guardrails.mjs +428 -35
- package/dist/plugin/scripts/pr-watch-events.sh +116 -0
- package/dist/plugin/scripts/pr-watch-guards.sh +8 -2
- package/dist/plugin/scripts/pr-watch-loop.sh +254 -0
- package/dist/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/dist/plugin/scripts/sign-body.sh +88 -0
- package/dist/plugin/skills/CLAUDE.md +16 -0
- package/dist/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/dist/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/dist/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/dist/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/dist/plugin/skills/do/open-prs/forward.md +2 -2
- package/dist/plugin/skills/do/per-comment-replies.md +2 -15
- package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/dist/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/dist/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/dist/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/dist/plugin/skills/muggle-test/SKILL.md +22 -2
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +3 -0
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +19 -12
- package/dist/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +13 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +3 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/dist/release-manifest.json +4 -4
- package/dist/src-B5OTAZKM.js +1 -0
- 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 +3 -1
- package/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/plugin/commands/mtest.md +1 -1
- package/plugin/hooks/README.md +15 -3
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/plugin/scripts/guardrail-report-format.sh +9 -5
- package/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/plugin/scripts/guardrails.mjs +428 -35
- package/plugin/scripts/pr-watch-events.sh +116 -0
- package/plugin/scripts/pr-watch-guards.sh +8 -2
- package/plugin/scripts/pr-watch-loop.sh +254 -0
- package/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/plugin/scripts/sign-body.sh +88 -0
- package/plugin/skills/CLAUDE.md +16 -0
- package/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/plugin/skills/do/open-prs/forward.md +2 -2
- package/plugin/skills/do/per-comment-replies.md +2 -15
- package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/plugin/skills/muggle-test/SKILL.md +22 -2
- package/plugin/skills/muggle-test-feature-local/SKILL.md +3 -0
- package/plugin/skills/muggle-test-prepare/SKILL.md +19 -12
- package/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +13 -2
- package/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +3 -1
- package/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/dist/src-AWUORQZZ.js +0 -1
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
# Reply routing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reply endpoints are not uniform across comment types. Route by parent type, then run the resolved provider's recipe for that route — resolve the provider once per [`../vcs/detect-vcs.md`](../vcs/detect-vcs.md). The routing decision below is provider-agnostic; the commands live with their tool.
|
|
4
4
|
|
|
5
5
|
## Line-level review comment (most common)
|
|
6
6
|
|
|
7
|
-
A comment attached to a specific file:line that belongs to a review thread.
|
|
7
|
+
A comment attached to a specific file:line that belongs to a review thread. Reply in the same thread, so the reply keeps its context:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--method POST \
|
|
12
|
-
-H "Accept: application/vnd.github+json" \
|
|
13
|
-
/repos/<owner>/<repo>/pulls/<number>/comments/<comment_id>/replies \
|
|
14
|
-
-f body="Done in $(git rev-parse --short HEAD) — renamed \`fooBar\` to \`foo_bar\`."
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The reply lands in the same review thread with `in_reply_to_id = <comment_id>`.
|
|
9
|
+
- `github` — [`../vcs/github/reply-line-comment.md`](../vcs/github/reply-line-comment.md). The reply lands in the thread with `in_reply_to_id = <comment_id>`.
|
|
10
|
+
- `gitlab` — [`../vcs/gitlab/reply-discussion.md`](../vcs/gitlab/reply-discussion.md). The reply is a new note on the discussion.
|
|
18
11
|
|
|
19
|
-
## Review body
|
|
12
|
+
## Review body with no inline comments
|
|
20
13
|
|
|
21
|
-
A reviewer left a summary review with
|
|
14
|
+
A reviewer left a summary review with a body but **no** inline comments. There is no thread to reply into, so answer with a top-level comment that names the review:
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```
|
|
16
|
+
- `github` — [`../vcs/github/top-level-comment.md`](../vcs/github/top-level-comment.md). GitHub has no reply-to-review-body endpoint, which is why this route exists at all.
|
|
17
|
+
- `gitlab` — does not arise. GitLab has no review envelope; every note belongs to a discussion, so it routes as a line-level reply above.
|
|
26
18
|
|
|
27
19
|
## Failing CI check
|
|
28
20
|
|
|
29
|
-
No reply. The fix commit IS the response. Include the failing check name in the commit subject:
|
|
21
|
+
No reply on either provider. The fix commit IS the response. Include the failing check name in the commit subject:
|
|
30
22
|
|
|
31
23
|
```
|
|
32
24
|
fix(ci): typecheck — narrow type of foo
|
|
@@ -36,5 +28,5 @@ fix(ci): lint — remove unused import
|
|
|
36
28
|
## Never
|
|
37
29
|
|
|
38
30
|
- Never post a top-level comment in reply to a line-level comment. It loses thread context.
|
|
39
|
-
- Never `gh pr review --comment`
|
|
31
|
+
- Never open a *new* review to carry a reply (`github`: `gh pr review --comment`) — those endpoints are for new reviews, not replies.
|
|
40
32
|
- Never reply twice to the same comment. The loop marker on each posted reply is the re-entry guard — a thread whose newest comment is loop-marked is no longer actionable, so the next round won't re-reply.
|
|
@@ -5,6 +5,7 @@ Provider-agnostic seam for the watcher and dev-cycle skills. A caller resolves o
|
|
|
5
5
|
## Index
|
|
6
6
|
|
|
7
7
|
- [`detect-vcs.md`](detect-vcs.md) — resolve `github` | `gitlab` from a URL argument or the origin remote.
|
|
8
|
+
- [`post-signature.md`](post-signature.md) — sign every posted body through `scripts/sign-body.sh`; provider-agnostic, required by every recipe below that writes a body.
|
|
8
9
|
- [`github.md`](github.md) — `gh` / `git` recipe TOC; per-recipe files in [`github/`](github/).
|
|
9
10
|
- [`gitlab.md`](gitlab.md) — `glab` / `git` recipe TOC; per-recipe files in [`gitlab/`](gitlab/).
|
|
10
11
|
- `common/` — provider-agnostic entry points (`ci-rollup`, `branch-standing`, `push-to-branch`, `verify-working-tree`), indexed by both provider TOCs.
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
For `open-prs/update.md` when E2E state flips (passing↔failing) or validation strategy changes.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-do --mode editable < <draft-file> > <signed-file>
|
|
6
7
|
gh pr edit <pr-number> --repo <owner>/<repo> --title "<new-title>"
|
|
7
|
-
gh pr edit <pr-number> --repo <owner>/<repo> --body-file <file>
|
|
8
|
+
gh pr edit <pr-number> --repo <owner>/<repo> --body-file <signed-file>
|
|
8
9
|
```
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Sign with `--mode editable` per [`../post-signature.md`](../post-signature.md) — a description is re-posted on every refresh, and that mode cuts the previous signature before appending so they never stack.
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
Used by `/muggle-do` per-comment inline replies.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
body="$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-do --mode loop < <draft-file>)"
|
|
6
7
|
gh api --method POST \
|
|
7
8
|
-H "Accept: application/vnd.github+json" \
|
|
8
9
|
repos/<owner>/<repo>/pulls/<n>/comments/<comment-id>/replies \
|
|
9
|
-
-f body="
|
|
10
|
+
-f body="$body"
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Sign with `--mode loop` per [`../post-signature.md`](../post-signature.md) — the `<!-- muggle-do:bot -->` marker it prefixes is what keeps the loop from re-triggering on its own reply.
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
For the resolve-reminder stage and any non-threaded notice.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
6
|
+
body="$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command <command> --mode <mode> < <draft-file>)"
|
|
7
|
+
gh pr comment <pr-number> --repo <owner>/<repo> --body "$body"
|
|
7
8
|
```
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Sign per [`../post-signature.md`](../post-signature.md). A resolve-reminder takes `--mode loop`, so the loop recognises the reminder as its own on a later tick; a one-shot notice nothing needs to detect takes `--mode plain`.
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
For `open-prs`. Push the branch first (see [`../common/push-to-branch.md`](../common/push-to-branch.md)), then open the MR.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-do --mode editable < <draft-file> > <signed-file>
|
|
6
7
|
glab mr create -R <group>/<project> \
|
|
7
8
|
--source-branch <branch> --target-branch <base> \
|
|
8
|
-
--title "<title>" --description "$(cat <file>)"
|
|
9
|
+
--title "<title>" --description "$(cat <signed-file>)"
|
|
9
10
|
```
|
|
10
11
|
|
|
12
|
+
Sign with `--mode editable` per [`../post-signature.md`](../post-signature.md) — the description this opens the MR with is the same body `mr-edit.md` refreshes later, so it carries the dedup marker from the start.
|
|
13
|
+
|
|
11
14
|
`glab` prints the created MR's URL on success — capture stdout and store the URL for handoff (the watcher seeds from it, the user gets the link).
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
For `open-prs/update.md` when E2E state flips (passing↔failing) or validation strategy changes. GitLab calls the body the **description**.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-do --mode editable < <draft-file> > <signed-file>
|
|
6
7
|
glab mr update <iid> -R <group>/<project> --title "<new-title>"
|
|
7
|
-
glab mr update <iid> -R <group>/<project> --description "$(cat <file>)"
|
|
8
|
+
glab mr update <iid> -R <group>/<project> --description "$(cat <signed-file>)"
|
|
8
9
|
```
|
|
10
|
+
|
|
11
|
+
Sign with `--mode editable` per [`../post-signature.md`](../post-signature.md) — a description is re-posted on every refresh, and that mode cuts the previous signature before appending so they never stack.
|
|
@@ -3,5 +3,8 @@
|
|
|
3
3
|
For the resolve-reminder stage and any non-threaded notice.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
6
|
+
body="$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command <command> --mode <mode> < <draft-file>)"
|
|
7
|
+
glab mr note <iid> -R <group>/<project> -m "$body"
|
|
7
8
|
```
|
|
9
|
+
|
|
10
|
+
Sign per [`../post-signature.md`](../post-signature.md). A resolve-reminder takes `--mode loop`, so the loop recognises the reminder as its own on a later tick; a one-shot notice nothing needs to detect takes `--mode plain`.
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Used by `/muggle-do` per-comment inline replies. A threaded reply is a new note appended to an existing discussion.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
body="$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-do --mode loop < <draft-file>)"
|
|
6
7
|
glab api --method POST \
|
|
7
8
|
projects/:id/merge_requests/<iid>/discussions/<discussion-id>/notes \
|
|
8
|
-
-f body="
|
|
9
|
+
-f body="$body"
|
|
9
10
|
```
|
|
11
|
+
|
|
12
|
+
Sign with `--mode loop` per [`../post-signature.md`](../post-signature.md) — the `<!-- muggle-do:bot -->` marker it prefixes is what keeps the loop from re-triggering on its own note.
|
|
@@ -2,14 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
Every pull-request / merge-request body, comment, and review-thread reply that muggle works posts ends with a signature line. Under a single-account workflow the automation posts as the repo owner, so the signature is what tells a reader — and a reviewer — that the post came from Muggle Works and which command produced it.
|
|
4
4
|
|
|
5
|
+
## Signing a body
|
|
6
|
+
|
|
7
|
+
Pipe the body through the shipped script. Never retype the line from this doc: a signature derived from prose is dropped silently, and a post that lost it still succeeds, still looks right to the poster, and is indistinguishable from a human comment forever after.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
body="$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command <command> --mode <mode> < <draft-file>)"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`--mode` picks the marker that precedes the visible line:
|
|
14
|
+
|
|
15
|
+
| Mode | Marker | Use for |
|
|
16
|
+
|------|--------|---------|
|
|
17
|
+
| `loop` | `<!-- muggle-do:bot -->` | Thread replies and resolve-reminders — anything the loop must later recognise as its own. |
|
|
18
|
+
| `editable` | `<!-- muggle-works:signature -->` | PR/MR descriptions. The script cuts the previous signature before appending, so a description re-posted on every refresh keeps exactly one. |
|
|
19
|
+
| `plain` | none | One-shot comments that nothing needs to detect later. |
|
|
20
|
+
|
|
21
|
+
Signing is idempotent — an already-signed body is re-signed, not double-signed — so a body may be passed through on a refresh path without checking whether it carries a signature already.
|
|
22
|
+
|
|
5
23
|
## The line
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
What the script emits, recorded here so a reader can recognise it. The script is the definition:
|
|
8
26
|
|
|
9
27
|
```
|
|
10
28
|
🤖 _Posted by `<command>` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
11
29
|
```
|
|
12
30
|
|
|
31
|
+
## Naming the command
|
|
32
|
+
|
|
13
33
|
`<command>` is the slash-command of the skill whose `gh` / `glab` call posts the body:
|
|
14
34
|
|
|
15
35
|
- `/muggle-do` — PR/MR descriptions, per-comment thread replies, top-level reference replies, and resolve-reminders.
|
|
@@ -17,15 +37,6 @@ Append this as the last line of the posted body:
|
|
|
17
37
|
|
|
18
38
|
Name the command that owns the post, not the one that generated the content. When the walkthrough hands its rendered block back for embedding (Mode B/C), the caller owns the post, so the caller's command is what the signature names.
|
|
19
39
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
A description is re-posted whenever state changes, so its signature must not stack. Precede the line with a hidden marker and treat the pair as one unit:
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
<!-- muggle-works:signature -->
|
|
26
|
-
🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Before writing an edited body, delete everything from the `<!-- muggle-works:signature -->` marker to the end of the body, then append the block fresh. This keeps exactly one signature no matter how many times the description is refreshed.
|
|
40
|
+
## Enforcement
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
`scripts/check-post-signatures.mjs` fails any recipe that posts a body without signing it, on every PR via the `post-signatures` CI job and locally via `pnpm run verify:signatures`.
|
|
@@ -31,10 +31,10 @@ Forward pipeline's Stage 7. Invoked by `/muggle-do` after stages 1–6 of a fres
|
|
|
31
31
|
- `## Changes` — summary of what changed in this repo.
|
|
32
32
|
- `## Validation` — one line: link to E2E report, `unit-only`, or `skip — <reason>`.
|
|
33
33
|
- **Walkthrough block** — only when an E2E report exists. Fire [`postPRVisualWalkthrough`](../../muggle-preferences/preference-gates/postPRVisualWalkthrough.md); on skip, omit this block. Otherwise invoke [`../../muggle-pr-visual-walkthrough/SKILL.md`](../../muggle-pr-visual-walkthrough/SKILL.md) Mode B and embed the returned `body` verbatim. No report → skip the block.
|
|
34
|
-
- **Signature** —
|
|
34
|
+
- **Signature** — write the assembled body to a file and sign it with `--command /muggle-do --mode editable` per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md). The signature lands last, after the walkthrough block; `editable` is the mode a description carries so later refreshes replace it instead of stacking.
|
|
35
35
|
|
|
36
36
|
4. **Create:** resolve the provider per [`../../_shared/vcs/detect-vcs.md`](../../_shared/vcs/detect-vcs.md).
|
|
37
|
-
- `github` → `gh pr create --title "..." --body
|
|
37
|
+
- `github` → `gh pr create --title "..." --body-file <signed-file> --head <branch>`, passing the file signed in Step 3. Capture the PR URL and number.
|
|
38
38
|
- `gitlab` → open the change via [`../../_shared/vcs/gitlab/mr-create.md`](../../_shared/vcs/gitlab/mr-create.md): `glab mr create --source-branch <branch> --target-branch <base> --title "..." --description "..."`. Capture the MR URL and iid.
|
|
39
39
|
|
|
40
40
|
5. **Overflow comment:** if the walkthrough skill returned a non-null `comment`, post it once using the provider resolved in Step 4 — `github` per [`../../_shared/vcs/github/top-level-comment.md`](../../_shared/vcs/github/top-level-comment.md), `gitlab` per [`../../_shared/vcs/gitlab/mr-note.md`](../../_shared/vcs/gitlab/mr-note.md). End the posted body with the signature line (command `/muggle-do`) per [`../../_shared/vcs/post-signature.md`](../../_shared/vcs/post-signature.md). Never post when `comment` is `null`.
|
|
@@ -27,14 +27,7 @@ If a comment has no associated change in either source (e.g. the comment was a q
|
|
|
27
27
|
|
|
28
28
|
For each comment id with a description, post the reply with the resolved provider:
|
|
29
29
|
|
|
30
|
-
- **`github`** — nested reply on the line comment
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
gh api --method POST \
|
|
34
|
-
-H "Accept: application/vnd.github+json" \
|
|
35
|
-
repos/<owner>/<repo>/pulls/<n>/comments/<comment-id>/replies \
|
|
36
|
-
-f body="<reply-body>"
|
|
37
|
-
```
|
|
30
|
+
- **`github`** — nested reply on the line comment, one per actionable comment, per [`../_shared/vcs/github/reply-line-comment.md`](../_shared/vcs/github/reply-line-comment.md).
|
|
38
31
|
|
|
39
32
|
- **`gitlab`** — one threaded note per actionable discussion per [`../_shared/vcs/gitlab/reply-discussion.md`](../_shared/vcs/gitlab/reply-discussion.md) (the discussion id stands in for the comment id), then resolve each fully-addressed thread per [`../_shared/vcs/gitlab/resolve-discussion.md`](../_shared/vcs/gitlab/resolve-discussion.md). The loop-marked reply is the addressed signal; the resolve is GitLab's equivalent of a reviewer closing the thread, which the loop can do directly. Resolving folds the discussion out of the next tick's actionable set, so no separate resolve-reminder nudge is needed for it.
|
|
40
33
|
|
|
@@ -42,12 +35,9 @@ Reply body uses the template in [`../muggle-pr-followup/output-templates/inline-
|
|
|
42
35
|
|
|
43
36
|
```
|
|
44
37
|
Addressed in <short-sha>: <one-line summary of the change made for THIS comment>.
|
|
45
|
-
|
|
46
|
-
<!-- muggle-do:bot -->
|
|
47
|
-
🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
48
38
|
```
|
|
49
39
|
|
|
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.
|
|
40
|
+
`<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. Write the content only — the posting recipe signs it with `--mode loop`, which appends the `<!-- muggle-do:bot -->` marker that identifies the reply as loop-authored (see [`../_shared/pr-followup-helpers/loop-signature.md`](../_shared/pr-followup-helpers/loop-signature.md)).
|
|
51
41
|
|
|
52
42
|
### Step 3 — Handle review-body-only comments (GitHub only)
|
|
53
43
|
|
|
@@ -58,9 +48,6 @@ If an actionable review has a non-empty `body` and **zero** line comments, GitHu
|
|
|
58
48
|
|
|
59
49
|
```
|
|
60
50
|
Re: review #<review_id> — addressed in <short-sha>: <one-line summary>.
|
|
61
|
-
|
|
62
|
-
<!-- muggle-do:bot -->
|
|
63
|
-
🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
|
|
64
51
|
```
|
|
65
52
|
|
|
66
53
|
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,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: muggle-browser-task
|
|
3
|
-
description: "Perform a real action on a website or web app from a plain-English instruction — log in and do the thing: submit a form, create or update a record, place an order, refund a charge, file a ticket, publish a post,
|
|
3
|
+
description: "Perform a real action on a website or web app from a plain-English instruction — log in and do the thing: submit a form, create or update a record, send a message, place an order, refund a charge, file a ticket, publish a post, complete a multi-step flow. Muggle drives a real browser through the site's own UI and supplies the session — managed login profiles and stored credentials — so a named service (Slack, Jira, Stripe, WordPress, a marketplace, a booking site, an admin panel) is a signal to reach for this skill, not to hunt for its API or connector; \"no logged-in session\" is not a reason to decline. Use whenever the user wants something done in a web UI ('log into X and …', 'send the message', 'create the ticket'), rather than declining or doing it by hand; ask for missing specifics inside the flow, not instead of starting it. Boundary: this performs the action (operating a site's own UI), not migrating personal data between consumer apps; to verify a flow works instead, use muggle-test-feature-local."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Muggle Test Task Runner
|
|
@@ -13,7 +13,8 @@ This folder holds the watcher loop that drives one PR toward merge-ready. The wa
|
|
|
13
13
|
- [`finalize.md`](finalize.md) — shared termination sequence for a terminal PR (mark terminal, `result.md`, log/telemetry, unschedule cron, post-merge cleanup handoff). Called by `contract.md` and `reconcile.md`.
|
|
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
|
-
- [`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);
|
|
16
|
+
- [`reconcile.md`](reconcile.md) — sweep that finalizes slots whose PR went terminal while polling lapsed, deletes orphaned crons, and re-arms **this session's own** open slots whose watcher stopped silently (dropped respawn); reports slots owned by other sessions without touching them. Runs on demand, at the top of auto-track, and nudged by a session-start hook.
|
|
17
|
+
- [`adopt.md`](adopt.md) — the user's explicit takeover of a slot owned by a session that is gone: claim `owner.json`, then arm. The only path by which a slot changes hands; nothing automatic calls it.
|
|
17
18
|
- [`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.
|
|
18
19
|
- [`state-schemas.md`](state-schemas.md) — canonical JSON shapes of session state files.
|
|
19
20
|
- [`output-templates.md`](output-templates.md) — TOC of message templates; per-group files in `output-templates/`.
|
|
@@ -20,7 +20,9 @@ A watcher that babysits one open PR toward **merge-ready** — review threads ad
|
|
|
20
20
|
|
|
21
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
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.
|
|
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.
|
|
24
|
+
|
|
25
|
+
**A watcher belongs to the session that armed it.** Each slot records its owning session in `owner.json` ([`state-schemas.md`](state-schemas.md#ownerjson)), and recovery is owner-scoped: [`reconcile`](reconcile.md#triggers) re-arms a dead watcher only inside the session that armed it, and reports the rest as orphans it declined to touch. Starting a session therefore picks up nothing new — the same context argument that rules out a headless daemon rules out inheriting a stranger's PR, which would hand review work to a session that never saw the design. A PR whose owning session is gone stays unwatched until the user adopts it by name ([`adopt.md`](adopt.md)).
|
|
24
26
|
|
|
25
27
|
## Routing
|
|
26
28
|
|
|
@@ -39,12 +41,13 @@ The skill recognizes its mode by inspecting `$ARGUMENTS` and falling back to on-
|
|
|
39
41
|
| empty | — | **auto-track** → [`auto-track.md`](auto-track.md) |
|
|
40
42
|
| `help` / `?` | — | **help:** list active loops per [`output-templates/help.md`](output-templates/help.md) |
|
|
41
43
|
| `reconcile` / `sweep` (optional `<slug>`) | — | **reconcile** → [`reconcile.md`](reconcile.md) |
|
|
44
|
+
| `adopt` (optional `<slug>`) | — | **adopt** → [`adopt.md`](adopt.md) — take over a slot owned by a dead session; no slug lists the adoptable ones |
|
|
42
45
|
|
|
43
46
|
**`--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
47
|
|
|
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
|
|
48
|
+
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 this session's own open slots 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 — and never re-arms one this session does not own. A no-arg invocation is a request to track *your* PRs, so it can only ever end with the session watching PRs it pushed plus watchers it already had.
|
|
46
49
|
|
|
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).
|
|
50
|
+
**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. It reports orphaned slots without acting on them; treat that report as inventory, never as a to-do. See [`reconcile.md`](reconcile.md#triggers).
|
|
48
51
|
|
|
49
52
|
Bootstrap accepts three optional trailing flags:
|
|
50
53
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Adopt Procedure
|
|
2
|
+
|
|
3
|
+
The procedure for the **adopt mode** of `muggle-pr-followup` — the user's deliberate takeover of a watcher slot owned by a session that is gone. Routing is in [`SKILL.md`](SKILL.md#routing).
|
|
4
|
+
|
|
5
|
+
Adoption is the only way a slot changes hands. [`reconcile.md`](reconcile.md) re-arms watchers this session already owns and refuses everything else; this file is the door it refuses through. Both exist because a watcher polls a PR in order to hand review work to `/muggle-do` **inside the owning session** — the session that carries the design decisions, the reviewer's phrasing, and the reasoning behind the branch. A session adopting a PR takes that job on without any of it, so the user has to say so.
|
|
6
|
+
|
|
7
|
+
## Input
|
|
8
|
+
|
|
9
|
+
`$ARGUMENTS` is `adopt`, optionally followed by a `<slug>`.
|
|
10
|
+
|
|
11
|
+
- **`adopt <slug>`** — take that slot.
|
|
12
|
+
- **`adopt`** alone — list the adoptable slots and stop. It is a query, not a prompt: print the list and end the turn, with no `AskUserQuestion` and no arming. The user picks by running the command again with a slug.
|
|
13
|
+
|
|
14
|
+
## Procedure
|
|
15
|
+
|
|
16
|
+
### Step 1 — Resolve the slot
|
|
17
|
+
|
|
18
|
+
Slot path is `~/.muggle-ai/muggle-do/sessions/<slug>/`. Refuse, one line each, when:
|
|
19
|
+
|
|
20
|
+
- **The dir is missing** — `no slot for <slug>; pass a PR URL to start one`.
|
|
21
|
+
- **The name ends in `.stopped`** — `<slug> was stopped by its owner; rename it back to adopt it`. Adoption is not a way around [`stop.md`](stop.md): the rename is the owner's kill switch, and honoring it here keeps that switch meaning one thing everywhere.
|
|
22
|
+
- **`result.md` exists** — `<slug> is finalized (<state>); nothing to watch`.
|
|
23
|
+
- **This session already owns it** — `<slug> is already owned by this session`, then run [`reconcile.md`](reconcile.md) scoped to that slug so a dead poller still gets recovered. Adopting what you own is a no-op, not an error.
|
|
24
|
+
|
|
25
|
+
With no slug, list every adoptable slot — open, not `.stopped`, owned by another session or none — as `<slug> → <owner>/<repo>#<n> (owner: <session-id | none>)`, then stop.
|
|
26
|
+
|
|
27
|
+
### Step 2 — Confirm the PR is still worth watching
|
|
28
|
+
|
|
29
|
+
Fetch the PR per [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md) using `prs.json[0].url`.
|
|
30
|
+
|
|
31
|
+
- `MERGED` / `CLOSED` → do not adopt. Run [`finalize.md`](finalize.md) on the slot instead and say so: the slot needed closing out, not a watcher.
|
|
32
|
+
- `gh` fails → surface the error verbatim and stop. Never claim a slot whose PR cannot be read; the claim would strand it under a session that cannot poll it either.
|
|
33
|
+
- `open` → continue.
|
|
34
|
+
|
|
35
|
+
Refresh `prs.json[0].head_sha` to the current `headRefOid`. The branch has almost certainly moved since the original owner last looked.
|
|
36
|
+
|
|
37
|
+
### Step 3 — Warn about the context gap
|
|
38
|
+
|
|
39
|
+
State plainly, in one line, what adoption does not carry: `adopting <slug> — this session has none of the original session's context for #<n>; cycles will work from the PR and the diff alone.`
|
|
40
|
+
|
|
41
|
+
This is a statement, not a gate — the user asked. It exists because the failure it describes is silent: an adopted watcher looks identical to one this session armed, and the first sign of the gap is usually a reply that misreads why the code is the way it is.
|
|
42
|
+
|
|
43
|
+
### Step 4 — Claim and arm
|
|
44
|
+
|
|
45
|
+
Write `owner.json` ([`state-schemas.md`](state-schemas.md#ownerjson)) with `session_id` from `$CLAUDE_CODE_SESSION_ID` and `claimed_at` now, overwriting any previous owner. If `$CLAUDE_CODE_SESSION_ID` is unset, stop with `cannot adopt: no session id to record` — an unidentifiable owner leaves the slot recoverable by nobody.
|
|
46
|
+
|
|
47
|
+
Then arm per [`arm-watcher.md`](arm-watcher.md): one drain tick, then the persistent monitor. The drain is what makes the adoption honest — it acts on everything outstanding now, rather than resuming from a watermark seeded against a wave the original session read and this one never saw.
|
|
48
|
+
|
|
49
|
+
Append `adopted (from <previous-owner | unowned>)` to the slot's `followup.log`.
|
|
50
|
+
|
|
51
|
+
### Step 5 — Report
|
|
52
|
+
|
|
53
|
+
One line: `adopted <slug> → <owner>/<repo>#<n> — armed`.
|
|
54
|
+
|
|
55
|
+
## Invariants
|
|
56
|
+
|
|
57
|
+
- **Explicit only.** Adoption runs when the user names it. No sweep, hook, auto-track, or tick ever calls this procedure, and nothing offers it unprompted.
|
|
58
|
+
- **One slot per invocation.** No bulk adopt, no `--all`. Adopting every orphan in one command is the behavior the ownership gate exists to prevent, wearing a different name.
|
|
59
|
+
- **Stopped stays stopped.** A `.stopped` slot is unreachable here, as it is from every other recovery path.
|
|
60
|
+
- **Claim before arm.** `owner.json` is written before the monitor starts, so a crash between the two leaves a slot this session owns and can recover — never an armed watcher no session claims.
|
|
@@ -6,28 +6,24 @@ How an orchestrating session starts the watch on one PR. Every arming point runs
|
|
|
6
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. Seed the rebase floor (`REBASED`) the same way, from the drain's branch-standing read per [`../_shared/vcs/common/branch-standing.md`](../_shared/vcs/common/branch-standing.md): set it to the current `rebase_key` (`<head_sha>..<base_tip_sha>`) when the drain found the branch already behind or conflicting — that staleness is what the drain just handled — and empty otherwise, so a branch the drain already rebased or escalated does not re-fire on the loop's first iteration. Seed the blocked-CI floor (`BLOCKED_CIDIGEST`) to the blocked fingerprint's `ci_digest` when arming while `last_seen.blocked` is already set, and empty otherwise — empty is the not-blocked state, in which the loop's blocked-resume probe stays dormant.
|
|
7
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.
|
|
8
8
|
|
|
9
|
-
Otherwise
|
|
9
|
+
Otherwise **claim the slot for this session** before starting anything: write `owner.json` ([`state-schemas.md`](state-schemas.md#ownerjson)) with `session_id` from `$CLAUDE_CODE_SESSION_ID` and `claimed_at` now. Arming is what establishes ownership, so every arming point records it here rather than each caller remembering to. If `$CLAUDE_CODE_SESSION_ID` is unset, write no `owner.json` — an unidentifiable owner is worse than none, since [`reconcile.md`](reconcile.md) would read a bogus id as some other session's claim and could never recover the slot.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Then start the **shipped** loop as a **persistent background monitor** in the orchestrating session, with the script path resolved to an absolute path at arm time (from `${CLAUDE_PLUGIN_ROOT}/scripts/`) so it still resolves after the arming session is gone:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
|
|
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)
|
|
14
|
+
bash "<abs>/scripts/pr-watch-loop.sh" --slot "<slot>" --repo "<owner>/<repo>" --pr <n> --base <base-branch>
|
|
18
15
|
```
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
**Never author a per-slot `watch.sh`.** The loop ships as [`../../scripts/pr-watch-loop.sh`](../../scripts/pr-watch-loop.sh), with its wake conditions in [`../../scripts/pr-watch-events.sh`](../../scripts/pr-watch-events.sh); arming runs it and passes arguments. Writing the loop from this prose was how it drifted — each arm produced an independent derivation, and a derivation that quietly dropped a wake still ran, still heartbeat, still logged, and simply never fired for the signal it lost. Two slots on one machine ended up without the behind-base wake, which left their PRs unmergeable under watchers that looked healthy. The prose below says *why* each wake exists; the shipped files are the only definition of *what* fires. A slot holding a legacy generated `watch.sh` keeps it until re-armed, at which point the supersede guard retires the old loop.
|
|
21
18
|
|
|
22
|
-
and the
|
|
19
|
+
The label is `PR #<n> — <title>`. Label and command both matter: some task surfaces show one, some the other, and a slot-bearing command keeps the watch identifiable everywhere a raw script blob would not. One 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), **the head SHA's checks settling red** (no check pending and one or more in the `fail` bucket per [`../_shared/vcs/common/ci-rollup.md`](../_shared/vcs/common/ci-rollup.md)), **the branch falling behind or conflicting with its base** (`behind_by > 0` or the conflict signal per [`../_shared/vcs/common/branch-standing.md`](../_shared/vcs/common/branch-standing.md)), or — **only while the watch is blocked** (`BLOCKED_CIDIGEST` non-empty) — **the head's CI digest changing in any way** (not just to red) — it prints one line 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 other three are not. The CI-red 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. The rebase floor (`REBASED`) is the **`rebase_key`** — `<head_sha>..<base_tip_sha>` — because staleness is a function of both sides: keying on the pair fires once per newly-due pair and re-arms when either the head or the base moves, where a head-only key would wedge permanently the first time the base advances (the head cannot change while nobody pushes). A head whose checks are still **pending** is never a red wake, and a branch with `behind_by == 0` and `mergeable == UNKNOWN` is never a rebase wake: pending checks may yet go green and conflict state is still computing, and the tick would idle on either (Steps 5–6) regardless. The blocked-CI signal is different in kind — a **resume** probe, live only while the watch is blocked: it wakes on any move of the head's CI digest (the same bucket-plus-sorted-name/conclusion signature the blocked fingerprint records — [`blocked-tick.md`](blocked-tick.md)) away from `BLOCKED_CIDIGEST`, so a block waiting on a green pass, a rerun, or an external deploy check resumes as promptly as one waiting on red. Quiet iterations print nothing and cost nothing — no model tokens are spent while the watch is quiet.
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```
|
|
21
|
+
The loop is **self-terminating**, so it never becomes one of those orphans. It claims the slot's `watch.pid`, then checks the guards from [`../../scripts/pr-watch-guards.sh`](../../scripts/pr-watch-guards.sh) at the top of every iteration before any `gh` call: `watcher_superseded` steps it down when a newer arm takes the slot, and `watcher_lifetime_exceeded` retires an orphan nothing supersedes. A failed fetch is never mistaken for a terminal PR — it counts toward `MUGGLE_PR_WATCH_MAX_FETCH_FAILURES` (60) with `watcher_fetch_backoff` between tries, so hours of outage are ridden out and the error lands in `<slot>/watch-fetch.log`. Terminal exit stays reserved for an observed `MERGED`/`CLOSED`.
|
|
22
|
+
|
|
23
|
+
The lifetime cap comes from the [`watcherLifetime`](../muggle-preferences/preference-gates/watcherLifetime.md) preference — `7d` by default, `1d`, or `never` for unbounded. The loop cannot read preferences, so resolve the value at arm time, convert it to seconds, and export `MUGGLE_PR_WATCH_MAX_LIFETIME` into the monitor's environment; an already-set value wins and is never overwritten. `never` exports 0, which the guard reads as unbounded — leaving `watcher_superseded` as the only thing that can retire an orphan.
|
|
28
24
|
|
|
29
|
-
So it exits when the PR goes terminal, after `MUGGLE_PR_WATCH_MAX_FETCH_FAILURES` consecutive failed fetches
|
|
30
|
-
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
|
|
25
|
+
So it exits when the PR goes terminal, after `MUGGLE_PR_WATCH_MAX_FETCH_FAILURES` consecutive failed fetches, when a newer arm supersedes it, or once it passes the lifetime cap — never `while true` unbounded.
|
|
26
|
+
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 start of the session that owns it.
|
|
31
27
|
|
|
32
28
|
**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.
|
|
33
29
|
|
|
@@ -20,6 +20,8 @@ Auto-track discovers the PRs you pushed or opened during this Claude Code sessio
|
|
|
20
20
|
|
|
21
21
|
Run [`reconcile.md`](reconcile.md) first. A no-arg invocation is the natural moment to finalize any slot whose PR merged or closed while its watcher was down — an expired `/loop` cron or an ended session leaves termination un-run (see reconcile's rationale). Then continue discovering new PRs below.
|
|
22
22
|
|
|
23
|
+
**Reconcile here can only recover, never widen.** It re-arms watchers this session already owns and finalizes terminal slots; a slot owned by another session is reported and left alone. That boundary matters most at this call site: auto-track is invoked with no arguments, so the user named no PR, and a sweep that adopted every open slot on disk would turn "track what I pushed" into "watch everything on this machine". Whatever the sweep reports as orphaned is **not** a candidate for Step 1 — this session did not push those.
|
|
24
|
+
|
|
23
25
|
### Step 1 — Discover candidate PRs from session context
|
|
24
26
|
|
|
25
27
|
A PR counts as **pushed this session** if, earlier in this conversation, you:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"skill_name": "muggle-pr-followup",
|
|
3
|
-
"notes": "These evals test the TICK DECISION behavior plus the recovery/stop substrate. A real tick needs live GitHub state and the Cron tools, so each prompt hands the model a concrete slot + provider state and asks for the step-by-step plan it would execute; assertions check the plan takes the branch the contract mandates. Cases 0–10 cover per-tick dispatch/idle decisions; cases 11–17 cover cron-delivered ticks handing back to the token-free monitor, the owner stop switch, and the stopped-slot / kill-file absorb that makes an unreachable orphaned cron inert; cases 8 and 18–19 cover the monitor's own wake signals (settled-red CI, branch-behind/conflicting, and blocked-resume on any CI-digest move). Run via skill-creator's eval runner — CI does not execute these (ci.yml gates on vitest/check-skill-deps; skill-eval.yml runs gate scenarios sourced from muggle-ai-brain and the routing eval).",
|
|
3
|
+
"notes": "These evals test the TICK DECISION behavior plus the recovery/stop substrate. A real tick needs live GitHub state and the Cron tools, so each prompt hands the model a concrete slot + provider state and asks for the step-by-step plan it would execute; assertions check the plan takes the branch the contract mandates. Cases 0–10 cover per-tick dispatch/idle decisions; cases 11–17 cover cron-delivered ticks handing back to the token-free monitor, the owner stop switch, and the stopped-slot / kill-file absorb that makes an unreachable orphaned cron inert; cases 8 and 18–19 cover the monitor's own wake signals (settled-red CI, branch-behind/conflicting, and blocked-resume on any CI-digest move); cases 20–22 cover session ownership — reconcile re-arms only slots the running session armed, fails closed when it cannot identify itself, and a dead session's slot changes hands only through an explicit single-slug adopt. Run via skill-creator's eval runner — CI does not execute these (ci.yml gates on vitest/check-skill-deps; skill-eval.yml runs gate scenarios sourced from muggle-ai-brain and the routing eval).",
|
|
4
4
|
"evals": [
|
|
5
5
|
{
|
|
6
6
|
"id": 0,
|
|
@@ -243,6 +243,68 @@
|
|
|
243
243
|
{ "name": "digest_matches_fingerprint_definition", "text": "The digest the loop computes is the same bucket-plus-sorted-name/conclusion signature the blocked fingerprint's ci_digest uses, so the loop and the tick's Step 2.5 resume gate agree by construction." },
|
|
244
244
|
{ "name": "resume_is_the_tick_job", "text": "The loop only wakes on the digest move; clearing last_seen.blocked and re-evaluating belongs to the tick's Step 2.5 fingerprint gate, and the blocked-tick flag/resume path is what maintains BLOCKED_CIDIGEST — the loop never writes it." }
|
|
245
245
|
]
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"id": 20,
|
|
249
|
+
"eval_name": "reconcile-never-rearms-another-sessions-slot",
|
|
250
|
+
"prompt": "Run reconcile. $CLAUDE_CODE_SESSION_ID is session-AAA. On disk: muggle-ai-works-pr600 is open, owner.json says session-AAA, heartbeat 3 hours old. muggle-ai-ui-pr601 is open, owner.json says session-BBB, heartbeat 3 hours old. muggle-ai-brain-pr602 is open with no owner.json at all, heartbeat 3 hours old. muggle-ai-works-pr603 has owner.json session-BBB and its PR is now MERGED. Walk me through the plan for this sweep.",
|
|
251
|
+
"files": [],
|
|
252
|
+
"assertions": [
|
|
253
|
+
{ "name": "partitions_by_owner_session_id", "text": "Plan splits the slots by owner.json session_id against $CLAUDE_CODE_SESSION_ID before deciding anything — pr600 owned, pr601/pr602/pr603 foreign." },
|
|
254
|
+
{ "name": "rearms_only_the_owned_slot", "text": "Plan re-arms ONLY muggle-ai-works-pr600. It does not re-arm pr601 or pr602 despite their stale heartbeats." },
|
|
255
|
+
{ "name": "missing_owner_is_foreign_not_adoptable", "text": "Plan treats muggle-ai-brain-pr602's absent owner.json as foreign and never re-arms or backfills an owner for it." },
|
|
256
|
+
{ "name": "finalizes_terminal_regardless_of_owner", "text": "Plan finalizes muggle-ai-works-pr603 even though session-BBB owns it, because finalizing needs no context — it writes result.md and cancels the cron." },
|
|
257
|
+
{ "name": "reports_orphans_without_offering", "text": "Plan reports pr601 and pr602 as orphaned with the adopt command, and does NOT offer to adopt them, prompt about them, or pick one to take." }
|
|
258
|
+
]
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"id": 21,
|
|
262
|
+
"eval_name": "reconcile-fails-closed-without-a-session-id",
|
|
263
|
+
"prompt": "Run reconcile in an environment where $CLAUDE_CODE_SESSION_ID is unset. Three open slots exist, all with stale heartbeats: two carry owner.json with different session ids, one carries none. Walk me through the plan.",
|
|
264
|
+
"files": [],
|
|
265
|
+
"assertions": [
|
|
266
|
+
{ "name": "treats_every_slot_as_foreign", "text": "Plan treats ALL three slots as foreign because the running session cannot identify itself — it does not match the unset id against anything." },
|
|
267
|
+
{ "name": "rearms_nothing", "text": "Plan re-arms zero watchers." },
|
|
268
|
+
{ "name": "still_finalizes_and_reports", "text": "Plan still refreshes live state, finalizes any slot whose PR went terminal, and reports the open ones as orphans — it degrades to finalize-and-report rather than aborting." }
|
|
269
|
+
]
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"id": 22,
|
|
273
|
+
"eval_name": "adopt-is-explicit-and-single-slot",
|
|
274
|
+
"prompt": "The user runs: /muggle:muggle-pr-followup adopt muggle-ai-ui-pr601. That slot is open, its owner.json names a different session, and its PR is still open on GitHub. Walk me through the plan.",
|
|
275
|
+
"files": [],
|
|
276
|
+
"assertions": [
|
|
277
|
+
{ "name": "claims_owner_before_arming", "text": "Plan overwrites owner.json with this session's $CLAUDE_CODE_SESSION_ID and a fresh claimed_at BEFORE arming the monitor." },
|
|
278
|
+
{ "name": "warns_about_the_context_gap", "text": "Plan states that this session lacks the original session's context for the PR, so cycles work from the PR and diff alone." },
|
|
279
|
+
{ "name": "arms_with_a_drain_tick", "text": "Plan arms per arm-watcher — a drain tick first, then the persistent monitor — rather than resuming from the previous owner's watermark alone." },
|
|
280
|
+
{ "name": "adopts_only_the_named_slot", "text": "Plan adopts exactly muggle-ai-ui-pr601 and does not sweep, list-and-take, or adopt any other orphaned slot in the same run." },
|
|
281
|
+
{ "name": "refuses_a_stopped_slot", "text": "Plan would refuse the adoption outright if the slot were named <slug>.stopped, since stop is the owner's kill switch and adoption is not a way around it." }
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"id": 23,
|
|
286
|
+
"eval_name": "arm-runs-the-shipped-loop-never-authors-one",
|
|
287
|
+
"prompt": "Arm the watch for muggle-ai-works-pr600 600 after a clean drain. The slot has no watch.pid. Walk me through exactly how you start the monitor, including the command you run.",
|
|
288
|
+
"files": [],
|
|
289
|
+
"assertions": [
|
|
290
|
+
{ "name": "runs_the_shipped_loop", "text": "Plan starts the shipped scripts/pr-watch-loop.sh, passing --slot, --repo and --pr, with the script path resolved to an absolute path under CLAUDE_PLUGIN_ROOT." },
|
|
291
|
+
{ "name": "does_not_author_a_watch_sh", "text": "Plan does NOT write, generate, or hand-author a per-slot watch.sh, and does NOT copy one from another slot." },
|
|
292
|
+
{ "name": "persistent_labeled_monitor", "text": "Plan starts it as a persistent background monitor labeled 'PR #<n> — <title>', one per PR." },
|
|
293
|
+
{ "name": "claims_owner_first", "text": "Plan writes owner.json with this session's $CLAUDE_CODE_SESSION_ID before starting the monitor." }
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"id": 24,
|
|
298
|
+
"eval_name": "monitor-wakes-on-branch-merely-behind-base",
|
|
299
|
+
"prompt": "A watch is running on muggle-ai-works-pr383 383. Its base branch advances by two commits. The PR reports mergeable=MERGEABLE (no conflict) and mergeStateStatus=BLOCKED because a review is still required. The base requires branches to be up to date before merging. Should the watch loop wake, and how does it decide?",
|
|
300
|
+
"files": [],
|
|
301
|
+
"assertions": [
|
|
302
|
+
{ "name": "wakes_on_behind_not_only_conflict", "text": "The loop DOES wake. A branch that is merely behind reports MERGEABLE, so the conflict signal alone never fires for it — behind_by > 0 is its own wake condition." },
|
|
303
|
+
{ "name": "does_not_rely_on_mergestatestatus", "text": "The decision does NOT come from mergeStateStatus: it is a single enum with precedence, and BLOCKED (review required) masks BEHIND on a PR still awaiting approval." },
|
|
304
|
+
{ "name": "measures_behind_via_compare", "text": "Behind-ness is measured with a compare call (behind_by), not inferred from the PR's mergeable/mergeStateStatus fields." },
|
|
305
|
+
{ "name": "keys_on_the_head_base_pair", "text": "The floor is the rebase_key <head_sha>..<base_tip_sha>, so the wake fires once per newly-due pair and re-arms when the base advances again under an unchanged head." },
|
|
306
|
+
{ "name": "condition_lives_in_shipped_file", "text": "The wake condition is defined in the shipped scripts/pr-watch-events.sh (watch_wake_rebase), not re-derived per arm." }
|
|
307
|
+
]
|
|
246
308
|
}
|
|
247
309
|
]
|
|
248
310
|
}
|
|
@@ -5,14 +5,24 @@ For `/muggle:muggle-pr-followup help` or `?` (no args runs **auto-track** instea
|
|
|
5
5
|
```
|
|
6
6
|
muggle-pr-followup — watcher loop for PR review follow-ups
|
|
7
7
|
|
|
8
|
-
Active loops:
|
|
8
|
+
Active loops (this session):
|
|
9
9
|
<slug> → <owner>/<repo>#<n> (<N> cycles)
|
|
10
10
|
...
|
|
11
11
|
(or "no active loops")
|
|
12
12
|
|
|
13
|
+
Owned by other sessions (not watched here):
|
|
14
|
+
<slug> → <owner>/<repo>#<n>
|
|
15
|
+
...
|
|
16
|
+
(omit this block entirely when there are none)
|
|
17
|
+
|
|
13
18
|
Usage:
|
|
14
19
|
/muggle:muggle-pr-followup → auto-track every PR you pushed this session
|
|
15
20
|
/muggle:muggle-pr-followup <pr-url> → bootstrap a new loop
|
|
16
21
|
/muggle:muggle-pr-followup <slug> <pr-number> → run one tick (called by /loop)
|
|
17
22
|
/muggle:muggle-pr-followup <pr-number> → run one tick (slug inferred from on-disk state)
|
|
23
|
+
/muggle:muggle-pr-followup reconcile → finalize terminal slots, re-arm your own dead watchers
|
|
24
|
+
/muggle:muggle-pr-followup adopt [<slug>] → take over a slot from a dead session (no slug lists them)
|
|
25
|
+
/muggle:muggle-pr-followup stop [<slug>] → tear down a watcher (no slug stops everything)
|
|
18
26
|
```
|
|
27
|
+
|
|
28
|
+
Split the two blocks by `owner.json` against `$CLAUDE_CODE_SESSION_ID` ([`../state-schemas.md`](../state-schemas.md#ownerjson)). Listing a foreign slot under "active loops" would claim this session is watching a PR nothing here polls.
|