@muggleai/works 5.7.0 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/{chunk-LTULYOOW.js → chunk-5PYK7DWB.js} +15 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  5. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  6. package/dist/plugin/hooks/README.md +11 -2
  7. package/dist/plugin/hooks/hooks.json +7 -1
  8. package/dist/plugin/scripts/gc-state.sh +66 -0
  9. package/dist/plugin/scripts/guard-run-job-object.ps1 +256 -0
  10. package/dist/plugin/scripts/guard-run.mjs +327 -0
  11. package/dist/plugin/scripts/guardrail-record-tests.sh +5 -4
  12. package/dist/plugin/scripts/guardrails.mjs +6 -3
  13. package/dist/plugin/scripts/pr-watch-guards.sh +46 -0
  14. package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
  15. package/dist/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
  16. package/dist/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
  17. package/dist/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
  18. package/dist/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
  19. package/{plugin/skills/_shared/vcs/github → dist/plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
  20. package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
  21. package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
  22. package/dist/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
  23. package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
  24. package/dist/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
  25. package/dist/plugin/skills/_shared/vcs/github.md +5 -2
  26. package/dist/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
  27. package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
  28. package/dist/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
  29. package/dist/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
  30. package/dist/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
  31. package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
  32. package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
  33. package/dist/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
  34. package/dist/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
  35. package/dist/plugin/skills/_shared/vcs/gitlab.md +6 -3
  36. package/dist/plugin/skills/_shared/vcs/post-signature.md +31 -0
  37. package/dist/plugin/skills/do/build.md +58 -56
  38. package/dist/plugin/skills/do/open-prs/forward.md +85 -84
  39. package/dist/plugin/skills/do/open-prs/update.md +3 -3
  40. package/dist/plugin/skills/do/per-comment-replies.md +2 -2
  41. package/dist/plugin/skills/do/resolve-conflicts.md +59 -58
  42. package/dist/plugin/skills/do/unit-tests.md +1 -1
  43. package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
  44. package/dist/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
  45. package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
  46. package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
  47. package/dist/plugin/skills/muggle-pr-followup/contract.md +171 -158
  48. package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
  49. package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
  50. package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
  51. package/dist/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
  52. package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
  53. package/dist/plugin/skills/muggle-pr-followup/stop.md +31 -0
  54. package/dist/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +14 -5
  55. package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
  56. package/dist/release-manifest.json +4 -4
  57. package/package.json +6 -6
  58. package/plugin/.claude-plugin/plugin.json +1 -1
  59. package/plugin/.cursor-plugin/plugin.json +1 -1
  60. package/plugin/hooks/README.md +11 -2
  61. package/plugin/hooks/hooks.json +7 -1
  62. package/plugin/scripts/gc-state.sh +66 -0
  63. package/plugin/scripts/guard-run-job-object.ps1 +256 -0
  64. package/plugin/scripts/guard-run.mjs +327 -0
  65. package/plugin/scripts/guardrail-record-tests.sh +5 -4
  66. package/plugin/scripts/guardrails.mjs +6 -3
  67. package/plugin/scripts/pr-watch-guards.sh +46 -0
  68. package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
  69. package/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
  70. package/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
  71. package/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
  72. package/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
  73. package/{dist/plugin/skills/_shared/vcs/github → plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
  74. package/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
  75. package/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
  76. package/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
  77. package/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
  78. package/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
  79. package/plugin/skills/_shared/vcs/github.md +5 -2
  80. package/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
  81. package/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
  82. package/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
  83. package/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
  84. package/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
  85. package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
  86. package/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
  87. package/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
  88. package/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
  89. package/plugin/skills/_shared/vcs/gitlab.md +6 -3
  90. package/plugin/skills/_shared/vcs/post-signature.md +31 -0
  91. package/plugin/skills/do/build.md +58 -56
  92. package/plugin/skills/do/open-prs/forward.md +85 -84
  93. package/plugin/skills/do/open-prs/update.md +3 -3
  94. package/plugin/skills/do/per-comment-replies.md +2 -2
  95. package/plugin/skills/do/resolve-conflicts.md +59 -58
  96. package/plugin/skills/do/unit-tests.md +1 -1
  97. package/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
  98. package/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
  99. package/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
  100. package/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
  101. package/plugin/skills/muggle-pr-followup/contract.md +171 -158
  102. package/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
  103. package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
  104. package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
  105. package/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
  106. package/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
  107. package/plugin/skills/muggle-pr-followup/stop.md +31 -0
  108. package/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +14 -5
  109. package/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
  110. package/dist/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
  111. package/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
@@ -45,6 +45,8 @@ var GH_PR_MERGED_LINE = /\b(?:Merged|Squashed and merged|Rebased and merged) pul
45
45
  var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
46
46
  var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
47
47
  var MAX_PR_TERMINAL_BLOCKS = 3;
48
+ var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
49
+ var MUGGLE_TEST_SKILL_NAME = "muggle-test";
48
50
 
49
51
  // src/guardrails/prTerminal.ts
50
52
  function detectPrTerminal(input2) {
@@ -113,7 +115,8 @@ ${input2.tool_response?.stderr ?? ""}`;
113
115
  return !FAIL.test(out);
114
116
  }
115
117
  function isE2ERun(input2) {
116
- return E2E_TOOL.test(input2.tool_name ?? "");
118
+ if (E2E_TOOL.test(input2.tool_name ?? "")) return true;
119
+ return MUGGLE_SKILL_EMIT_TOOL.test(input2.tool_name ?? "") && input2.tool_input?.skillName === MUGGLE_TEST_SKILL_NAME;
117
120
  }
118
121
 
119
122
  // src/guardrails/shouldRunE2E.ts
@@ -124,10 +127,10 @@ function shouldRunE2E(state) {
124
127
  function applyRecordedRun(state, run) {
125
128
  let next = state;
126
129
  if (run.unitTestPassed) {
127
- next = { ...next, unitTestsGreen: true, e2eRun: false, e2eBlockCount: 0 };
130
+ next = { ...next, unitTestsGreen: true, e2eRun: false };
128
131
  }
129
132
  if (run.e2eRan) {
130
- next = { ...next, e2eRun: true };
133
+ next = { ...next, e2eRun: true, e2eBlockCount: 0 };
131
134
  }
132
135
  if (run.e2eSkipped) {
133
136
  next = { ...next, e2eSkipped: true };
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Self-termination guards for the muggle-pr-followup watch loop. Sourced by the
4
+ # per-slot watch.sh that arm-watcher writes, and the single source of truth for
5
+ # the two conditions that stop a watcher from leaking across sessions.
6
+ #
7
+ # A bare `while true` monitor leaks on Windows: the OS does not stop a detached
8
+ # Git Bash loop when the Claude session that launched it ends, so orphaned
9
+ # watchers accumulate and each keeps spawning gh calls forever. Two guards bound
10
+ # that, and together with arm-watcher's pre-arm dedup keep at most one live
11
+ # watcher per PR:
12
+ #
13
+ # watcher_superseded — <slot>/watch.pid holds the PID of the watcher that
14
+ # owns the slot. A loop whose PID no longer matches has
15
+ # been replaced by a newer arm and must exit.
16
+ # watcher_lifetime_exceeded — a loop exits after MUGGLE_PR_WATCH_MAX_LIFETIME
17
+ # regardless, so an orphan nothing supersedes still dies
18
+ # on its own; reconcile re-arms an open PR inside a live
19
+ # session.
20
+
21
+ MUGGLE_PR_WATCH_MAX_LIFETIME="${MUGGLE_PR_WATCH_MAX_LIFETIME:-21600}"
22
+ MUGGLE_PR_WATCH_POLL_INTERVAL="${MUGGLE_PR_WATCH_POLL_INTERVAL:-60}"
23
+
24
+ # True when watch.pid exists and names a PID other than this loop's — a newer arm
25
+ # has taken ownership of the slot. Absent/empty watch.pid is not superseded: a
26
+ # loop that has not yet claimed the slot keeps running.
27
+ watcher_superseded() {
28
+ local slot="$1" mypid="$2" owner
29
+ [ -f "${slot}/watch.pid" ] || return 1
30
+ owner=$(cat "${slot}/watch.pid" 2>/dev/null)
31
+ [ -n "$owner" ] && [ "$owner" != "$mypid" ]
32
+ }
33
+
34
+ watcher_lifetime_exceeded() {
35
+ local started="$1" now="$2" max="${3:-$MUGGLE_PR_WATCH_MAX_LIFETIME}"
36
+ [ $((now - started)) -ge "$max" ]
37
+ }
38
+
39
+ # True when pid names a running process. `kill -0` sends no signal; EPERM means
40
+ # the process exists but is foreign, which still counts as alive. Used by
41
+ # arm-watcher's pre-arm dedup to decide whether a watcher already owns the slot.
42
+ watcher_pid_alive() {
43
+ local pid="$1"
44
+ [ -n "$pid" ] || return 1
45
+ kill -0 "$pid" 2>/dev/null
46
+ }
@@ -8,11 +8,11 @@ Append these two lines as the end of every loop-posted comment body:
8
8
 
9
9
  ```
10
10
  <!-- muggle-do:bot -->
11
- 🤖 _Automated reply from muggle-do._
11
+ 🤖 _Posted by `/muggle-do` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
12
12
  ```
13
13
 
14
- - `<!-- muggle-do:bot -->` — hidden HTML marker; GitHub renders it invisibly and humans never type it. This is the **detection token**.
15
- - The visible line makes the automation clear to a reader.
14
+ - `<!-- muggle-do:bot -->` — hidden HTML marker; GitHub renders it invisibly and humans never type it. This is the **detection token**. It must stay exactly as written — echo-protection and addressed-by-loop classification read this literal string.
15
+ - The visible line is the shared Muggle Works signature ([`../vcs/post-signature.md`](../vcs/post-signature.md)) with `/muggle-do` as the command. It links a reader back to the tool and names the command that posted the comment.
16
16
 
17
17
  ## Detection
18
18
 
@@ -1,9 +1,10 @@
1
1
  # VCS provider recipes
2
2
 
3
- Provider-agnostic seam for the watcher and dev-cycle skills. A caller resolves one provider token via [`detect-vcs.md`](detect-vcs.md) — `github` (`gh`) or `gitlab` (`glab`) — then loads the matching provider's recipe for each agnostic step. The two recipe sets mirror each other call-for-call; callers branch on the token, never fork procedure.
3
+ Provider-agnostic seam for the watcher and dev-cycle skills. A caller resolves one provider token via [`detect-vcs.md`](detect-vcs.md) — `github` (`gh`) or `gitlab` (`glab`) — then loads the matching provider's recipe for each agnostic step. The two recipe sets mirror each other call-for-call; callers branch on the token, never fork procedure. Provider-agnostic entry points live once in `common/`, indexed by both TOCs: a **dispatcher** owns the `github`-vs-`gitlab` branch so a caller links it alone instead of naming both recipes side by side (`ci-rollup`, `branch-standing`, `push-to-branch`), and a recipe with no provider-specific procedure lives there too (`verify-working-tree`).
4
4
 
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
8
  - [`github.md`](github.md) — `gh` / `git` recipe TOC; per-recipe files in [`github/`](github/).
9
9
  - [`gitlab.md`](gitlab.md) — `glab` / `git` recipe TOC; per-recipe files in [`gitlab/`](gitlab/).
10
+ - `common/` — provider-agnostic entry points (`ci-rollup`, `branch-standing`, `push-to-branch`, `verify-working-tree`), indexed by both provider TOCs.
@@ -0,0 +1,19 @@
1
+ # Branch standing vs base (tool-agnostic)
2
+
3
+ The single instruction the rebase trigger references for **conflict** and **out-of-date** detection — sites link here and never name a provider's metadata recipe inline. Both signals read from the metadata snapshot the caller already fetched; this file only says which field each provider reads.
4
+
5
+ Resolve the provider per [`../detect-vcs.md`](../detect-vcs.md).
6
+
7
+ ## Conflict with the base
8
+
9
+ - `github` → `mergeable == CONFLICTING`, corroborated by `mergeStateStatus == DIRTY` ([`../github/pr-metadata.md`](../github/pr-metadata.md)).
10
+ - `gitlab` → `detailed_merge_status` in `{broken_status, conflict}` ([`../gitlab/mr-metadata.md`](../gitlab/mr-metadata.md)).
11
+
12
+ A still-computing state — `github`'s `mergeable == UNKNOWN`, `gitlab`'s `checking` / `unchecked` — counts as **not conflicting** this tick.
13
+
14
+ ## Out of date (behind the base)
15
+
16
+ Read from **commit ancestry**, never from the merge-state field: GitHub masks `BEHIND` behind `DIRTY` / `BLOCKED`, and GitLab reports `need_rebase` only under fast-forward-merge — both hide a stale branch. The compare is exact even while the conflict state is still computing.
17
+
18
+ - `github` → the compare call in [`../github/pr-metadata.md`](../github/pr-metadata.md#behind-by-out-of-date-detection); `behind_by > 0` ⇒ out of date.
19
+ - `gitlab` → the compare call in [`../gitlab/mr-metadata.md`](../gitlab/mr-metadata.md#behind-by-out-of-date-detection); any base commit the head lacks ⇒ out of date.
@@ -0,0 +1,16 @@
1
+ # CI rollup (tool-agnostic)
2
+
3
+ The single instruction every CI-poll site references — sites link here and never name a provider's rollup recipe inline. Fetch the CI state for a PR/MR head and fold it into one of three buckets: **red**, **pending**, **green**.
4
+
5
+ Resolve the provider per [`../detect-vcs.md`](../detect-vcs.md), then run its rollup recipe — the recipe owns the fetch command and the raw per-check/per-job states:
6
+
7
+ - `github` → [`../github/pr-checks.md`](../github/pr-checks.md) — the check-run rollup for the head SHA.
8
+ - `gitlab` → [`../gitlab/mr-pipeline.md`](../gitlab/mr-pipeline.md) — the pipeline-job rollup for the head.
9
+
10
+ Both recipes fold into the same buckets, so a caller branches on the token only to fetch, then reasons on the bucket alone:
11
+
12
+ - **red** — one or more entries in the `fail` bucket (a failed check-run; a `failed` job). Candidate for fix-ci; the dispatch carries the failing names.
13
+ - **pending** — nothing failed but an entry has not settled (`pending` bucket; a `running` / `pending` / `created` job). The result may yet go green.
14
+ - **green** — every entry passed, was skipped, or was cancelled, or there are none at all.
15
+
16
+ The rollup is **non-monotonic** — it flips red↔green and resets on every push. A caller that records a red result keys it on the **head SHA**, not a monotonic id, so the record re-arms on the next push.
@@ -0,0 +1,16 @@
1
+ # Push to branch (tool-agnostic)
2
+
3
+ The single instruction every commit/push site references — sites link here and never embed provider commands inline.
4
+
5
+ ## Signing gate — never push unsigned commits
6
+
7
+ Resolve the provider per [`../detect-vcs.md`](../detect-vcs.md), then run its signed-commits recipe — the recipe owns the preflight and the actual commands:
8
+
9
+ - `github` → [`../github/signed-commits.md`](../github/signed-commits.md) — local signing working: commit and push normally; broken: create the commits server-signed (`createCommitOnBranch`) and skip the push (the remote already has them). Rebase/force-push follows the same recipe's replay path.
10
+ - `gitlab` → [`../gitlab/signed-commits.md`](../gitlab/signed-commits.md) — local signing working: commit and push normally; broken: stop and escalate (no server-side signing).
11
+
12
+ Never disable signing to make a push go through.
13
+
14
+ ## After the push
15
+
16
+ Capture the new head SHA — from the local branch, or from the mutation response on the server-signed path — and append it to `last_seen.pushed_shas[]` so resolve-reminder can recognize it.
@@ -8,10 +8,10 @@ git remote get-url origin # remote matches <owner>/<repo>
8
8
  git rev-parse --abbrev-ref HEAD # current branch matches PR's headRefName
9
9
  ```
10
10
 
11
- Accept any remote URL form for `<owner>/<repo>` (with or without trailing `.git`):
11
+ Accept any remote URL form for `<owner>/<repo>`, with or without trailing `.git`, where `<host>` is the provider host (`github.com`, or the GitLab instance host):
12
12
 
13
- - `https://github.com/<owner>/<repo>`
14
- - `git@github.com:<owner>/<repo>`
15
- - `ssh://git@github.com/<owner>/<repo>`
13
+ - `https://<host>/<owner>/<repo>`
14
+ - `git@<host>:<owner>/<repo>`
15
+ - `ssh://git@<host>/<owner>/<repo>`
16
16
 
17
17
  Any mismatch → abort; the calling skill reports the wrong checkout to the user with its own message.
@@ -6,3 +6,5 @@ For `open-prs/update.md` when E2E state flips (passing↔failing) or validation
6
6
  gh pr edit <pr-number> --repo <owner>/<repo> --title "<new-title>"
7
7
  gh pr edit <pr-number> --repo <owner>/<repo> --body-file <file>
8
8
  ```
9
+
10
+ The `<file>` body must end with the Muggle Works signature. Because the description is re-posted on each refresh, strip the old signature (from the `<!-- muggle-works:signature -->` marker to the end) before re-appending it — see [`../post-signature.md`](../post-signature.md).
@@ -8,3 +8,5 @@ gh api --method POST \
8
8
  repos/<owner>/<repo>/pulls/<n>/comments/<comment-id>/replies \
9
9
  -f body="<reply-text>"
10
10
  ```
11
+
12
+ The `<reply-text>` must end with the loop signature block — the `<!-- muggle-do:bot -->` detection marker above the Muggle Works line. See [`../post-signature.md`](../post-signature.md).
@@ -0,0 +1,28 @@
1
+ # Signed commits without local signing
2
+
3
+ **Never push unsigned commits.** When the machine has no working signing setup, do not fall back to unsigned `git commit` + `git push` — create the commits **remotely** with GraphQL `createCommitOnBranch`, which GitHub signs server-side. And never "fix" a signing failure by disabling it: no `--no-gpg-sign`, no `-c commit.gpgsign=false` — a broken signing setup is fixed or routed around via the remote path, not turned off.
4
+
5
+ ## Preflight
6
+
7
+ ```bash
8
+ git -C <repo-path> log --format='%G?' origin/<branch>..HEAD # unpushed branch: <base>..HEAD
9
+ ```
10
+
11
+ Any `N` (no signature) among the commits about to leave the machine → the push is blocked; use the remote path below. `G`/`E`/`U` are signed commits (locally unverifiable is fine — GitHub-signed commits show `E` without GitHub's key in the local keyring). Signing configured and working → commit and push normally.
12
+
13
+ ## Remote signed commit
14
+
15
+ 1. The branch must exist on the remote; create it at the base if absent: `gh api repos/<owner>/<repo>/git/refs -f ref=refs/heads/<branch> -f sha=<base-sha>`.
16
+ 2. Stage the change (`git add <files>`), then take each file's canonical bytes from the **index** — `git cat-file blob :<path>` — and base64 them. Index blobs, never working-copy reads: the index holds git's normalized content (line endings, filters), so the remote tree matches what a local commit would have produced.
17
+ 3. One `createCommitOnBranch` mutation per commit: `branch {repositoryNameWithOwner, branchName}`, `message {headline, body}`, `expectedHeadOid` = the branch's current remote head (a concurrency lease — a concurrent push 409s instead of being clobbered), `fileChanges.additions` (path + base64 contents; removals via `fileChanges.deletions`).
18
+ 4. Sync the local branch to the result: `git fetch origin <branch>`, verify `git diff --cached origin/<branch>` is **empty** (remote tree identical to what was staged), then `git reset --hard origin/<branch>`. A non-empty diff means the payload missed a file — fix and re-commit; never leave local and remote diverged.
19
+
20
+ ## Rebase / force-push
21
+
22
+ A local rebase mints new local commits — unsigned on this machine — so the `--force-with-lease` path is equally blocked. Replay instead:
23
+
24
+ 1. Create a temp ref at the new base tip.
25
+ 2. Re-create each branch commit on it via `createCommitOnBranch` (same per-commit file snapshots from `git cat-file blob <commit>:<path>`; conflicted files carry the resolved content).
26
+ 3. Move the branch ref with a lease: verify the branch's current remote head is still the expected pre-rebase SHA, then `gh api -X PATCH repos/<owner>/<repo>/git/refs/heads/<branch> -f sha=<new-head> -F force=true`. Delete the temp ref.
27
+
28
+ The replayed commits are GitHub-signed; the guarded ref move is the only force operation.
@@ -5,3 +5,5 @@ For the resolve-reminder stage and any non-threaded notice.
5
5
  ```bash
6
6
  gh pr comment <pr-number> --repo <owner>/<repo> --body "<text>"
7
7
  ```
8
+
9
+ The `<text>` you post must end with the Muggle Works signature — see [`../post-signature.md`](../post-signature.md).
@@ -1,39 +1,39 @@
1
- # Unresolved comment threads
2
-
3
- For the watcher's dispatch trigger and the resolve-reminder stage. GraphQL only — REST does not expose `isResolved`/`isOutdated`.
4
-
5
- ```bash
6
- gh api graphql -F owner=<owner> -F name=<repo> -F number=<n> -f query='
7
- query($owner: String!, $name: String!, $number: Int!) {
8
- repository(owner: $owner, name: $name) {
9
- pullRequest(number: $number) {
10
- reviewThreads(first: 100) {
11
- nodes {
12
- id
13
- isResolved
14
- isOutdated
15
- comments(first: 100) {
16
- nodes {
17
- databaseId
18
- pullRequestReview { databaseId }
19
- author { login }
20
- body
21
- createdAt
22
- }
23
- }
24
- }
25
- }
26
- }
27
- }
28
- }'
29
- ```
30
-
31
- Filter client-side to `isResolved == false`. Walk each thread's comments in `createdAt` order and classify by the loop marker (see [`../../pr-followup-helpers/loop-signature.md`](../../pr-followup-helpers/loop-signature.md)), not by `author.login` — the login is ambiguous under a shared account:
32
-
33
- - **Addressed, awaiting resolve** — the **newest** comment carries the loop marker `<!-- muggle-do:bot -->`. The loop has replied and nothing newer is waiting. → resolve-reminder.
34
- - **Unaddressed human comment** — the newest comment lacks the marker and is newer than the thread's newest loop-marked comment (or the thread has no loop comment yet). → actionable: the round should address it. The **watcher's dispatch trigger** additionally requires `isOutdated == false` — a thread whose anchored line has since changed is skipped, since re-surfacing a stale anchor risks answering a concern the change already mooted. The resolve-reminder stage ignores `isOutdated`.
35
- - **Not addressed** — indeterminate (e.g. no comments).
36
-
37
- Each comment exposes its owning review as `pullRequestReview.databaseId` — the watcher collects this from an actionable thread's newest comment to build its dispatch list.
38
-
39
- A loop comment also cites a `<short-sha>` from `last_seen.pushed_shas[]` in its body, which tells *which* push addressed the thread.
1
+ # Unresolved comment threads
2
+
3
+ For the watcher's dispatch trigger and the resolve-reminder stage. GraphQL only — REST does not expose `isResolved`/`isOutdated`.
4
+
5
+ ```bash
6
+ gh api graphql -F owner=<owner> -F name=<repo> -F number=<n> -f query='
7
+ query($owner: String!, $name: String!, $number: Int!) {
8
+ repository(owner: $owner, name: $name) {
9
+ pullRequest(number: $number) {
10
+ reviewThreads(first: 100) {
11
+ nodes {
12
+ id
13
+ isResolved
14
+ isOutdated
15
+ comments(first: 100) {
16
+ nodes {
17
+ databaseId
18
+ pullRequestReview { databaseId }
19
+ author { login }
20
+ body
21
+ createdAt
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }'
29
+ ```
30
+
31
+ Filter client-side to `isResolved == false`. Walk each thread's comments in `createdAt` order and classify by the loop marker (see [`../../pr-followup-helpers/loop-signature.md`](../../pr-followup-helpers/loop-signature.md)), not by `author.login` — the login is ambiguous under a shared account:
32
+
33
+ - **Addressed, awaiting resolve** — the **newest** comment carries the loop marker `<!-- muggle-do:bot -->`. The loop has replied and nothing newer is waiting. → resolve-reminder.
34
+ - **Unaddressed human comment** — the newest comment lacks the marker and is newer than the thread's newest loop-marked comment (or the thread has no loop comment yet). → actionable: the round should address it. This holds **regardless of `isOutdated`** — a thread whose anchored line has since moved (a rebase or a later push) still carries its unanswered question, and skipping it leaves that question unanswered for good. Whether the moved code already mooted the concern is a judgment for the round to make and reply to, not a reason to skip; the marker rule then stops re-dispatch on the next poll. Both the watcher's dispatch trigger and the resolve-reminder stage ignore `isOutdated`.
35
+ - **Not addressed** — indeterminate (e.g. no comments).
36
+
37
+ Each comment exposes its owning review as `pullRequestReview.databaseId` — the watcher collects this from an actionable thread's newest comment to build its dispatch list.
38
+
39
+ A loop comment also cites a `<short-sha>` from `last_seen.pushed_shas[]` in its body, which tells *which* push addressed the thread.
@@ -11,11 +11,14 @@ Skills assume a working `gh auth status`. Auth errors surface verbatim from `gh`
11
11
  | [`pr-metadata`](github/pr-metadata.md) | Snapshot PR state, head SHA, branch, conflict + `behind_by` out-of-date detection — watcher + bootstrap. |
12
12
  | [`submitted-reviews`](github/submitted-reviews.md) | Fetch a review by id / watcher's body-only-review check. |
13
13
  | [`pr-checks`](github/pr-checks.md) | Check-run rollup for the head SHA — watcher's CI poll. |
14
+ | [`ci-rollup`](common/ci-rollup.md) | Tool-agnostic CI poll — dispatches to `pr-checks` / `mr-pipeline`, folds into red/pending/green. |
15
+ | [`branch-standing`](common/branch-standing.md) | Tool-agnostic conflict + out-of-date detection — dispatches to `pr-metadata` / `mr-metadata`. |
14
16
  | [`line-comments-for-review`](github/line-comments-for-review.md) | Pull a review's line comments — `/muggle-do` per-comment routing. |
15
17
  | [`unresolved-threads`](github/unresolved-threads.md) | GraphQL unresolved-thread state — watcher's dispatch trigger + resolve-reminder. |
16
18
  | [`reply-line-comment`](github/reply-line-comment.md) | POST a threaded reply on a line comment. |
17
19
  | [`top-level-comment`](github/top-level-comment.md) | POST a top-level PR comment — resolve-reminder + overflow. |
18
- | [`push-to-branch`](github/push-to-branch.md) | Push + capture new SHA after address-reviews work. |
19
- | [`verify-working-tree`](github/verify-working-tree.md) | Three checks bootstrap runs before seeding state. |
20
+ | [`push-to-branch`](common/push-to-branch.md) | Signing-gated push + capture new SHA after address-reviews work. |
21
+ | [`signed-commits`](github/signed-commits.md) | Never-push-unsigned rule: `%G?` preflight, server-signed commits via `createCommitOnBranch`, rebase replay. |
22
+ | [`verify-working-tree`](common/verify-working-tree.md) | Three checks bootstrap runs before seeding state. |
20
23
  | [`pr-edit`](github/pr-edit.md) | Refresh title or body when address-reviews mode flips state. |
21
24
  | [`loop-user-identity`](github/loop-user-identity.md) | Resolve the GitHub login of the loop user. |
@@ -3,7 +3,7 @@
3
3
  The GitLab identity that owns the authenticated `glab` token. Resolve-reminder thread classification and reply attribution need this.
4
4
 
5
5
  ```bash
6
- glab api user --jq '.username'
6
+ glab api user | jq -r '.username'
7
7
  ```
8
8
 
9
9
  Cache in `state.md` under `Loop user:`; re-resolve only when missing.
@@ -1,6 +1,6 @@
1
1
  # Open a merge request
2
2
 
3
- For `open-prs`. Push the branch first (see [`../github/push-to-branch.md`](../github/push-to-branch.md)), then open the MR.
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
6
  glab mr create -R <group>/<project> \
@@ -3,7 +3,7 @@
3
3
  Incoming feedback for the watcher's poll and the address-reviews fetch. GitLab has **no review envelope** — there is no submitted-review object grouping a summary body with line comments. Feedback arrives as individual notes, each belonging to a discussion (a thread). This recipe is the `submitted-reviews` analogue.
4
4
 
5
5
  ```bash
6
- glab api projects/:id/merge_requests/:iid/discussions --paginate
6
+ glab api projects/:id/merge_requests/<iid>/discussions --paginate
7
7
  ```
8
8
 
9
9
  Each discussion has `id` and a `notes[]` array; each note has `id`, `author.username`, `body`, `created_at`, `system` (a `true` flag marks GitLab's own activity entries — skip them).
@@ -16,7 +16,7 @@ glab mr view <iid> -R <group>/<project> -F json
16
16
  `detailed_merge_status == "need_rebase"` reports a behind branch only when the project enforces "fast-forward merge"; otherwise it stays `mergeable` while behind. Detect out-of-date straight from commit ancestry instead — independent of merge-method config:
17
17
 
18
18
  ```bash
19
- glab api projects/:id/repository/compare?from=<target_branch>&to=<head_sha> --jq '.commits | length'
19
+ glab api "projects/:id/repository/compare?from=<head_sha>&to=<target_branch>" | jq '.commits | length'
20
20
  ```
21
21
 
22
- GitLab's compare lists only the commits `to` is ahead by, so flip the direction: compare `from=<head_sha>&to=<target_branch>` and a non-empty `.commits` ⇒ the base has commits the head lacks ⇒ out of date. Empty ⇒ current with base.
22
+ Compare lists the commits reachable from `to` but not `from`, so `from=<head_sha>&to=<target_branch>` yields exactly the base commits the head lacks — non-zero ⇒ out of date; zero ⇒ current with base.
@@ -9,7 +9,7 @@ glab ci status -R <group>/<project> -b <source_branch>
9
9
  Or straight from the API for the latest pipeline and its jobs:
10
10
 
11
11
  ```bash
12
- glab api projects/:id/merge_requests/:iid/pipelines --jq '.[0].id'
12
+ glab api projects/:id/merge_requests/<iid>/pipelines | jq -r '.[0].id'
13
13
  glab api projects/:id/pipelines/<pipeline-id>/jobs --paginate
14
14
  ```
15
15
 
@@ -4,6 +4,6 @@ Used by `/muggle-do` per-comment inline replies. A threaded reply is a new note
4
4
 
5
5
  ```bash
6
6
  glab api --method POST \
7
- projects/:id/merge_requests/:iid/discussions/<discussion-id>/notes \
7
+ projects/:id/merge_requests/<iid>/discussions/<discussion-id>/notes \
8
8
  -f body="<reply-text>"
9
9
  ```
@@ -4,7 +4,7 @@ Mark a thread resolved once the loop's reply has addressed it — the resolve-re
4
4
 
5
5
  ```bash
6
6
  glab api --method PUT \
7
- "projects/:id/merge_requests/:iid/discussions/<discussion-id>?resolved=true"
7
+ "projects/:id/merge_requests/<iid>/discussions/<discussion-id>?resolved=true"
8
8
  ```
9
9
 
10
10
  Resolves every resolvable note in the thread at once. Only resolvable (diff/line) threads accept this; a non-resolvable discussion returns an error.
@@ -0,0 +1,15 @@
1
+ # Signed commits without local signing
2
+
3
+ **Never push unsigned commits.** GitLab has no server-side signing analogue — commits created through its commits API are unsigned, so a broken local signing setup has no remote route around it. And never "fix" a signing failure by disabling it: no `--no-gpg-sign`, no `-c commit.gpgsign=false`.
4
+
5
+ ## Preflight
6
+
7
+ ```bash
8
+ git -C <repo-path> log --format='%G?' origin/<branch>..HEAD # unpushed branch: <base>..HEAD
9
+ ```
10
+
11
+ Any `N` (no signature) among the commits about to leave the machine → the push is blocked. `G`/`E`/`U` are signed commits (locally unverifiable is fine). Signing configured and working → commit and push normally.
12
+
13
+ ## Blocked
14
+
15
+ Unsigned commits bound for an MR branch → stop and escalate to the owner to configure local signing. A local rebase mints new unsigned commits, so the force-push path is equally blocked. Never push unsigned.
@@ -3,8 +3,8 @@
3
3
  For the watcher's dispatch trigger and the resolve-reminder stage. REST exposes resolution directly — no GraphQL needed, unlike GitHub.
4
4
 
5
5
  ```bash
6
- glab api projects/:id/merge_requests/:iid/discussions --paginate \
7
- --jq '[.[] | select(.notes[0].resolvable == true) | select(any(.notes[]; .resolved == false))]'
6
+ glab api projects/:id/merge_requests/<iid>/discussions --paginate \
7
+ | jq '[.[] | select(.notes[0].resolvable == true) | select(any(.notes[]; .resolved == false))]'
8
8
  ```
9
9
 
10
10
  A discussion is resolvable when its notes carry `resolvable == true` (diff/line threads are; the MR description and system notes are not). A thread is **unresolved** when any of its notes has `resolved == false`.
@@ -4,7 +4,7 @@ Reusable `glab` / `git` snippets used by `muggle-pr-followup` (watcher + bootstr
4
4
 
5
5
  Skills assume a working `glab auth status`. Auth errors surface verbatim from `glab`.
6
6
 
7
- The project ref `:id` is `<group>/<project>` — URL-encode it for `glab api` (`mygroup/myproj` → `mygroup%2Fmyproj`). `:iid` is the MR's per-project internal id (the `!123` number), not the global id.
7
+ `glab api` fills `:id` itself with the URL-encoded path of the current directory's project — run recipes from the MR worktree and leave `:id` as written (elsewhere, substitute the encoded path yourself: `mygroup/myproj` → `mygroup%2Fmyproj`). `<iid>` is the MR's per-project internal id (the `!123` number), not the global id; glab has no placeholder for it — substitute it. `glab api` has no `--jq` flag — pipe to `jq`.
8
8
 
9
9
  ## Index
10
10
 
@@ -12,6 +12,8 @@ The project ref `:id` is `<group>/<project>` — URL-encode it for `glab api` (`
12
12
  | :----- | :------- |
13
13
  | [`mr-metadata`](gitlab/mr-metadata.md) | Snapshot MR state, head SHA, branch, conflict + out-of-date detection — watcher + bootstrap. |
14
14
  | [`mr-pipeline`](gitlab/mr-pipeline.md) | Pipeline-job rollup for the head SHA — watcher's CI poll. |
15
+ | [`ci-rollup`](common/ci-rollup.md) | Tool-agnostic CI poll — dispatches to `pr-checks` / `mr-pipeline`, folds into red/pending/green. |
16
+ | [`branch-standing`](common/branch-standing.md) | Tool-agnostic conflict + out-of-date detection — dispatches to `pr-metadata` / `mr-metadata`. |
15
17
  | [`mr-discussions`](gitlab/mr-discussions.md) | Fetch incoming notes/discussions — watcher's feedback poll. |
16
18
  | [`unresolved-discussions`](gitlab/unresolved-discussions.md) | Unresolved-discussion state — watcher's dispatch trigger + resolve-reminder. |
17
19
  | [`reply-discussion`](gitlab/reply-discussion.md) | POST a threaded reply on a discussion. |
@@ -20,5 +22,6 @@ The project ref `:id` is `<group>/<project>` — URL-encode it for `glab api` (`
20
22
  | [`mr-edit`](gitlab/mr-edit.md) | Refresh title or description when address-reviews mode flips state. |
21
23
  | [`mr-create`](gitlab/mr-create.md) | Open an MR + capture its URL for handoff. |
22
24
  | [`loop-user-identity`](gitlab/loop-user-identity.md) | Resolve the GitLab username of the loop user. |
23
- | [`push-to-branch`](github/push-to-branch.md) | Push + capture new SHA after address-reviews work (provider-agnostic). |
24
- | [`verify-working-tree`](github/verify-working-tree.md) | Three checks bootstrap runs before seeding state (provider-agnostic). |
25
+ | [`push-to-branch`](common/push-to-branch.md) | Signing-gated push + capture new SHA after address-reviews work. |
26
+ | [`signed-commits`](gitlab/signed-commits.md) | Never-push-unsigned rule: `%G?` preflight; no server-side signing analogue → stop and escalate. |
27
+ | [`verify-working-tree`](common/verify-working-tree.md) | Three checks bootstrap runs before seeding state. |
@@ -0,0 +1,31 @@
1
+ # PR post signature
2
+
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
+
5
+ ## The line
6
+
7
+ Append this as the last line of the posted body:
8
+
9
+ ```
10
+ 🤖 _Posted by `<command>` · [Muggle Works](https://github.com/multiplex-ai/muggle-ai-works)_
11
+ ```
12
+
13
+ `<command>` is the slash-command of the skill whose `gh` / `glab` call posts the body:
14
+
15
+ - `/muggle-do` — PR/MR descriptions, per-comment thread replies, top-level reference replies, and resolve-reminders.
16
+ - `/muggle-pr-visual-walkthrough` — a walkthrough comment the walkthrough skill posts itself (Mode A).
17
+
18
+ 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
+
20
+ ## Editable bodies (PR / MR description)
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.
30
+
31
+ A comment or reply is posted once and never edited, so it needs no dedup marker — append the line alone. A loop-authored reply already carries the `<!-- muggle-do:bot -->` detection marker (defined in loop-signature.md) directly above this line; that marker stays, and this line replaces the old visible text.