@muggleai/works 5.7.0 → 5.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) 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/agents/test-prepare-runner.md +47 -0
  7. package/dist/plugin/agents/visual-walkthrough-builder.md +58 -0
  8. package/dist/plugin/hooks/README.md +11 -2
  9. package/dist/plugin/hooks/hooks.json +7 -1
  10. package/dist/plugin/scripts/gc-state.sh +66 -0
  11. package/dist/plugin/scripts/guard-run-job-object.ps1 +256 -0
  12. package/dist/plugin/scripts/guard-run.mjs +327 -0
  13. package/dist/plugin/scripts/guardrail-record-tests.sh +5 -4
  14. package/dist/plugin/scripts/guardrails.mjs +6 -3
  15. package/dist/plugin/scripts/pr-watch-guards.sh +46 -0
  16. package/dist/plugin/skills/CLAUDE.md +39 -36
  17. package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
  18. package/dist/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
  19. package/dist/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
  20. package/dist/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
  21. package/dist/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
  22. package/{plugin/skills/_shared/vcs/github → dist/plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
  23. package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
  24. package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
  25. package/dist/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
  26. package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
  27. package/dist/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
  28. package/dist/plugin/skills/_shared/vcs/github.md +5 -2
  29. package/dist/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
  30. package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
  31. package/dist/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
  32. package/dist/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
  33. package/dist/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
  34. package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
  35. package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
  36. package/dist/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
  37. package/dist/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
  38. package/dist/plugin/skills/_shared/vcs/gitlab.md +6 -3
  39. package/dist/plugin/skills/_shared/vcs/post-signature.md +31 -0
  40. package/dist/plugin/skills/do/build.md +58 -56
  41. package/dist/plugin/skills/do/open-prs/forward.md +85 -84
  42. package/dist/plugin/skills/do/open-prs/update.md +3 -3
  43. package/dist/plugin/skills/do/per-comment-replies.md +2 -2
  44. package/dist/plugin/skills/do/resolve-conflicts.md +59 -58
  45. package/dist/plugin/skills/do/unit-tests.md +1 -1
  46. package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
  47. package/dist/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
  48. package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
  49. package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
  50. package/dist/plugin/skills/muggle-pr-followup/contract.md +171 -158
  51. package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
  52. package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
  53. package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
  54. package/dist/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
  55. package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
  56. package/dist/plugin/skills/muggle-pr-followup/stop.md +31 -0
  57. package/dist/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +41 -227
  58. package/dist/plugin/skills/muggle-test-prepare/SKILL.md +130 -129
  59. package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
  60. package/dist/release-manifest.json +4 -4
  61. package/package.json +7 -6
  62. package/plugin/.claude-plugin/plugin.json +1 -1
  63. package/plugin/.cursor-plugin/plugin.json +1 -1
  64. package/plugin/agents/test-prepare-runner.md +47 -0
  65. package/plugin/agents/visual-walkthrough-builder.md +58 -0
  66. package/plugin/hooks/README.md +11 -2
  67. package/plugin/hooks/hooks.json +7 -1
  68. package/plugin/scripts/gc-state.sh +66 -0
  69. package/plugin/scripts/guard-run-job-object.ps1 +256 -0
  70. package/plugin/scripts/guard-run.mjs +327 -0
  71. package/plugin/scripts/guardrail-record-tests.sh +5 -4
  72. package/plugin/scripts/guardrails.mjs +6 -3
  73. package/plugin/scripts/pr-watch-guards.sh +46 -0
  74. package/plugin/skills/CLAUDE.md +39 -36
  75. package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +3 -3
  76. package/plugin/skills/_shared/vcs/CLAUDE.md +2 -1
  77. package/plugin/skills/_shared/vcs/common/branch-standing.md +19 -0
  78. package/plugin/skills/_shared/vcs/common/ci-rollup.md +16 -0
  79. package/plugin/skills/_shared/vcs/common/push-to-branch.md +16 -0
  80. package/{dist/plugin/skills/_shared/vcs/github → plugin/skills/_shared/vcs/common}/verify-working-tree.md +4 -4
  81. package/plugin/skills/_shared/vcs/github/pr-edit.md +2 -0
  82. package/plugin/skills/_shared/vcs/github/reply-line-comment.md +2 -0
  83. package/plugin/skills/_shared/vcs/github/signed-commits.md +28 -0
  84. package/plugin/skills/_shared/vcs/github/top-level-comment.md +2 -0
  85. package/plugin/skills/_shared/vcs/github/unresolved-threads.md +39 -39
  86. package/plugin/skills/_shared/vcs/github.md +5 -2
  87. package/plugin/skills/_shared/vcs/gitlab/loop-user-identity.md +1 -1
  88. package/plugin/skills/_shared/vcs/gitlab/mr-create.md +1 -1
  89. package/plugin/skills/_shared/vcs/gitlab/mr-discussions.md +1 -1
  90. package/plugin/skills/_shared/vcs/gitlab/mr-metadata.md +2 -2
  91. package/plugin/skills/_shared/vcs/gitlab/mr-pipeline.md +1 -1
  92. package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +1 -1
  93. package/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +1 -1
  94. package/plugin/skills/_shared/vcs/gitlab/signed-commits.md +15 -0
  95. package/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +2 -2
  96. package/plugin/skills/_shared/vcs/gitlab.md +6 -3
  97. package/plugin/skills/_shared/vcs/post-signature.md +31 -0
  98. package/plugin/skills/do/build.md +58 -56
  99. package/plugin/skills/do/open-prs/forward.md +85 -84
  100. package/plugin/skills/do/open-prs/update.md +3 -3
  101. package/plugin/skills/do/per-comment-replies.md +2 -2
  102. package/plugin/skills/do/resolve-conflicts.md +59 -58
  103. package/plugin/skills/do/unit-tests.md +1 -1
  104. package/plugin/skills/muggle-pr-followup/CLAUDE.md +1 -0
  105. package/plugin/skills/muggle-pr-followup/SKILL.md +11 -4
  106. package/plugin/skills/muggle-pr-followup/arm-watcher.md +29 -5
  107. package/plugin/skills/muggle-pr-followup/bootstrap.md +3 -1
  108. package/plugin/skills/muggle-pr-followup/contract.md +171 -158
  109. package/plugin/skills/muggle-pr-followup/evals/evals.json +221 -101
  110. package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +2 -2
  111. package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -1
  112. package/plugin/skills/muggle-pr-followup/reconcile.md +9 -5
  113. package/plugin/skills/muggle-pr-followup/state-schemas.md +191 -175
  114. package/plugin/skills/muggle-pr-followup/stop.md +31 -0
  115. package/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +41 -227
  116. package/plugin/skills/muggle-test-prepare/SKILL.md +130 -129
  117. package/plugin/skills/muggle-test-prepare/steps/start-services.md +2 -2
  118. package/dist/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
  119. package/plugin/skills/_shared/vcs/github/push-to-branch.md +0 -10
@@ -0,0 +1,327 @@
1
+ import { spawn, spawnSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { extname, join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ // src/guard-run/cli.ts
7
+
8
+ // src/guard-run/buildLaunchPlan.ts
9
+ function quoteWindowsArgument(argument) {
10
+ if (argument !== "" && !/[ \t"]/.test(argument)) return argument;
11
+ let quoted = '"';
12
+ let backslashCount = 0;
13
+ for (const character of argument) {
14
+ if (character === "\\") {
15
+ backslashCount += 1;
16
+ continue;
17
+ }
18
+ if (character === '"') {
19
+ quoted += "\\".repeat(backslashCount * 2 + 1) + '"';
20
+ backslashCount = 0;
21
+ continue;
22
+ }
23
+ quoted += "\\".repeat(backslashCount) + character;
24
+ backslashCount = 0;
25
+ }
26
+ return quoted + "\\".repeat(backslashCount * 2) + '"';
27
+ }
28
+ function buildWindowsCommandLine(command) {
29
+ return command.map(quoteWindowsArgument).join(" ");
30
+ }
31
+ function buildWindowsJobObjectPlan(input) {
32
+ const bareCommandLine = buildWindowsCommandLine(input.options.command);
33
+ const commandLine = input.commandNeedsCmdShell ? `cmd.exe /d /s /c "${bareCommandLine}"` : bareCommandLine;
34
+ return {
35
+ executable: "powershell.exe",
36
+ args: [
37
+ "-NoProfile",
38
+ "-NonInteractive",
39
+ "-ExecutionPolicy",
40
+ "Bypass",
41
+ "-File",
42
+ input.jobObjectShimPath,
43
+ String(input.options.activeProcessLimit),
44
+ input.options.isServiceMode ? "persist" : "kill",
45
+ String(input.launcherPid),
46
+ // Base64 survives the node→powershell→CreateProcessW arg relays that
47
+ // would otherwise re-interpret quotes.
48
+ Buffer.from(commandLine, "utf8").toString("base64")
49
+ ],
50
+ spawnDetached: false,
51
+ killsProcessGroupOnExit: false,
52
+ stopsScopeUnitOnExit: null
53
+ };
54
+ }
55
+ function buildLinuxSystemdScopePlan(args) {
56
+ return {
57
+ // pids.max (cgroup v2): the excess fork fails with EAGAIN in the kernel.
58
+ executable: "systemd-run",
59
+ args: [
60
+ "--user",
61
+ "--scope",
62
+ "--quiet",
63
+ "--collect",
64
+ `--unit=${args.scopeUnitName}`,
65
+ "-p",
66
+ `TasksMax=${args.options.activeProcessLimit}`,
67
+ "--",
68
+ ...args.options.command
69
+ ],
70
+ spawnDetached: false,
71
+ killsProcessGroupOnExit: false,
72
+ // Catchable launcher death stops the scope; after an untrappable SIGKILL
73
+ // the scope lives on but stays TasksMax-bounded, so the cap always holds.
74
+ stopsScopeUnitOnExit: args.options.isServiceMode ? null : args.scopeUnitName
75
+ };
76
+ }
77
+ function buildLinuxPrlimitProcessGroupPlan(args) {
78
+ return {
79
+ executable: "prlimit",
80
+ args: [`--nproc=${args.nprocCeiling}`, "--", ...args.options.command],
81
+ spawnDetached: true,
82
+ killsProcessGroupOnExit: !args.options.isServiceMode,
83
+ stopsScopeUnitOnExit: null
84
+ };
85
+ }
86
+ function buildDarwinUlimitProcessGroupPlan(args) {
87
+ return {
88
+ executable: "/bin/sh",
89
+ args: ["-c", `ulimit -u ${args.nprocCeiling}; exec "$@"`, "guard-run", ...args.options.command],
90
+ spawnDetached: true,
91
+ killsProcessGroupOnExit: !args.options.isServiceMode,
92
+ stopsScopeUnitOnExit: null
93
+ };
94
+ }
95
+ function buildBackendLaunchPlan(args) {
96
+ if (args.backend === "windows-job-object" /* WindowsJobObject */) {
97
+ return buildWindowsJobObjectPlan({
98
+ options: args.options,
99
+ jobObjectShimPath: args.jobObjectShimPath ?? "",
100
+ launcherPid: args.launcherPid ?? 0,
101
+ commandNeedsCmdShell: args.commandNeedsCmdShell ?? false
102
+ });
103
+ }
104
+ if (args.backend === "linux-systemd-scope" /* LinuxSystemdScope */) {
105
+ return buildLinuxSystemdScopePlan({
106
+ options: args.options,
107
+ scopeUnitName: args.scopeUnitName ?? ""
108
+ });
109
+ }
110
+ if (args.backend === "linux-prlimit-process-group" /* LinuxPrlimitProcessGroup */) {
111
+ return buildLinuxPrlimitProcessGroupPlan({
112
+ options: args.options,
113
+ nprocCeiling: args.nprocCeiling ?? 0
114
+ });
115
+ }
116
+ return buildDarwinUlimitProcessGroupPlan({
117
+ options: args.options,
118
+ nprocCeiling: args.nprocCeiling ?? 0
119
+ });
120
+ }
121
+
122
+ // src/guard-run/headroom.ts
123
+ function countProcessListLines(psOutput) {
124
+ return psOutput.split("\n").filter((line) => line.trim() !== "").length;
125
+ }
126
+ function computeNprocCeiling(args) {
127
+ return args.currentUserProcessCount + args.activeProcessLimit;
128
+ }
129
+
130
+ // src/guard-run/constants.ts
131
+ var GUARD_RUN_DEFAULT_ACTIVE_PROCESS_LIMIT = 64;
132
+ var GUARD_RUN_USAGE = "usage: guard-run [--limit N] [--service] -- <command...>\n Runs <command> with its whole process tree kernel-capped at N processes\n (default 64). Default mode also ties the tree to the launcher: killing\n guard-run reaps every descendant. --service keeps the cap but lets the\n tree outlive the launcher (for dev servers).";
133
+
134
+ // src/guard-run/parseArguments.ts
135
+ function parseError(errorMessage) {
136
+ return { options: null, errorMessage: `${errorMessage}
137
+ ${GUARD_RUN_USAGE}` };
138
+ }
139
+ function parsePositiveInteger(rawValue) {
140
+ if (rawValue === void 0 || !/^\d+$/.test(rawValue)) return null;
141
+ const parsed = Number(rawValue);
142
+ return parsed >= 1 ? parsed : null;
143
+ }
144
+ function parseGuardRunArguments(argv) {
145
+ let activeProcessLimit = GUARD_RUN_DEFAULT_ACTIVE_PROCESS_LIMIT;
146
+ let isServiceMode = false;
147
+ let index = 0;
148
+ while (index < argv.length) {
149
+ const token = argv[index];
150
+ if (token === "--") {
151
+ index += 1;
152
+ break;
153
+ }
154
+ if (token === "--service") {
155
+ isServiceMode = true;
156
+ index += 1;
157
+ continue;
158
+ }
159
+ if (token === "--limit" || token.startsWith("--limit=")) {
160
+ const rawLimit = token === "--limit" ? argv[index + 1] : token.slice("--limit=".length);
161
+ const parsedLimit = parsePositiveInteger(rawLimit);
162
+ if (parsedLimit === null) return parseError("--limit requires a positive integer");
163
+ activeProcessLimit = parsedLimit;
164
+ index += token === "--limit" ? 2 : 1;
165
+ continue;
166
+ }
167
+ if (token.startsWith("--")) return parseError(`unknown flag: ${token}`);
168
+ break;
169
+ }
170
+ const command = argv.slice(index);
171
+ if (command.length === 0) return parseError("no command given");
172
+ return {
173
+ options: {
174
+ activeProcessLimit,
175
+ isServiceMode,
176
+ command
177
+ },
178
+ errorMessage: null
179
+ };
180
+ }
181
+
182
+ // src/guard-run/selectBackend.ts
183
+ var SYSTEMD_USABLE_MANAGER_STATES = /* @__PURE__ */ new Set(["running", "degraded"]);
184
+ function selectGuardRunBackend(probe) {
185
+ if (probe.platform === "win32") {
186
+ return { backend: "windows-job-object" /* WindowsJobObject */, errorMessage: null };
187
+ }
188
+ if (probe.platform === "linux") {
189
+ const managerState = (probe.systemdUserManagerState ?? "").trim();
190
+ if (probe.hasSystemdRun && SYSTEMD_USABLE_MANAGER_STATES.has(managerState)) {
191
+ return { backend: "linux-systemd-scope" /* LinuxSystemdScope */, errorMessage: null };
192
+ }
193
+ if (probe.hasPrlimit) {
194
+ return { backend: "linux-prlimit-process-group" /* LinuxPrlimitProcessGroup */, errorMessage: null };
195
+ }
196
+ return {
197
+ backend: null,
198
+ errorMessage: "no process-cap backend available (need a systemd user manager or prlimit); refusing to run the command unguarded"
199
+ };
200
+ }
201
+ if (probe.platform === "darwin") {
202
+ return { backend: "darwin-ulimit-process-group" /* DarwinUlimitProcessGroup */, errorMessage: null };
203
+ }
204
+ return {
205
+ backend: null,
206
+ errorMessage: `unsupported platform "${probe.platform}"; refusing to run the command unguarded`
207
+ };
208
+ }
209
+
210
+ // src/guard-run/cli.ts
211
+ var JOB_OBJECT_SHIM_FILENAME = "guard-run-job-object.ps1";
212
+ function fail(message) {
213
+ process.stderr.write(`guard-run: ${message}
214
+ `);
215
+ process.exit(2);
216
+ }
217
+ function commandSucceeds(executable, args) {
218
+ const probe = spawnSync(executable, args, { stdio: "ignore", timeout: 1e4 });
219
+ return probe.error === void 0 && probe.status === 0;
220
+ }
221
+ function probedSystemdUserManagerState() {
222
+ const probe = spawnSync("systemctl", ["--user", "is-system-running"], {
223
+ encoding: "utf-8",
224
+ timeout: 1e4
225
+ });
226
+ if (probe.error !== void 0 || typeof probe.stdout !== "string") return null;
227
+ return probe.stdout.trim();
228
+ }
229
+ function resolveJobObjectShimPath() {
230
+ const bundledSibling = join(dirname(fileURLToPath(import.meta.url)), JOB_OBJECT_SHIM_FILENAME);
231
+ if (existsSync(bundledSibling)) return bundledSibling;
232
+ const pluginTreePath = join(
233
+ dirname(fileURLToPath(import.meta.url)),
234
+ "..",
235
+ "..",
236
+ "plugin",
237
+ "scripts",
238
+ JOB_OBJECT_SHIM_FILENAME
239
+ );
240
+ if (existsSync(pluginTreePath)) return pluginTreePath;
241
+ return fail(`cannot locate ${JOB_OBJECT_SHIM_FILENAME}; refusing to run the command unguarded`);
242
+ }
243
+ function windowsCommandNeedsCmdShell(firstCommandToken) {
244
+ const knownExtension = extname(firstCommandToken).toLowerCase();
245
+ if (knownExtension === ".cmd" || knownExtension === ".bat") return true;
246
+ if (knownExtension !== "") return false;
247
+ const lookup = spawnSync("where.exe", [firstCommandToken], {
248
+ encoding: "utf-8",
249
+ timeout: 1e4
250
+ });
251
+ if (lookup.error !== void 0 || lookup.status !== 0) return false;
252
+ const resolvedExtension = extname(lookup.stdout.split(/\r?\n/)[0] ?? "").toLowerCase();
253
+ return resolvedExtension === ".cmd" || resolvedExtension === ".bat";
254
+ }
255
+ function currentUserProcessCount() {
256
+ const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
257
+ const listing = spawnSync("ps", ["-u", uid, "-o", "pid="], {
258
+ encoding: "utf-8",
259
+ timeout: 1e4
260
+ });
261
+ if (listing.error !== void 0 || listing.status !== 0) {
262
+ return fail("cannot count current user processes for the nproc ceiling");
263
+ }
264
+ return countProcessListLines(listing.stdout);
265
+ }
266
+ function registerTreeCleanup(plan2, childPid) {
267
+ if (!plan2.killsProcessGroupOnExit && plan2.stopsScopeUnitOnExit === null) return;
268
+ let cleanedUp = false;
269
+ const cleanup = () => {
270
+ if (cleanedUp) return;
271
+ cleanedUp = true;
272
+ if (plan2.killsProcessGroupOnExit && childPid !== void 0) {
273
+ try {
274
+ process.kill(-childPid, "SIGKILL");
275
+ } catch {
276
+ }
277
+ }
278
+ if (plan2.stopsScopeUnitOnExit !== null) {
279
+ spawnSync("systemctl", ["--user", "stop", plan2.stopsScopeUnitOnExit], {
280
+ stdio: "ignore",
281
+ timeout: 1e4
282
+ });
283
+ }
284
+ };
285
+ process.on("exit", cleanup);
286
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
287
+ process.on(signal, () => {
288
+ cleanup();
289
+ process.exit(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
290
+ });
291
+ }
292
+ }
293
+ var parseResult = parseGuardRunArguments(process.argv.slice(2));
294
+ if (parseResult.options === null) fail(parseResult.errorMessage ?? "invalid arguments");
295
+ var options = parseResult.options;
296
+ var isLinux = process.platform === "linux";
297
+ var selection = selectGuardRunBackend({
298
+ platform: process.platform,
299
+ hasSystemdRun: isLinux && commandSucceeds("systemd-run", ["--version"]),
300
+ systemdUserManagerState: isLinux ? probedSystemdUserManagerState() : null,
301
+ hasPrlimit: isLinux && commandSucceeds("prlimit", ["--version"])
302
+ });
303
+ if (selection.backend === null) fail(selection.errorMessage ?? "no backend");
304
+ var backend = selection.backend;
305
+ var needsNprocCeiling = backend === "linux-prlimit-process-group" /* LinuxPrlimitProcessGroup */ || backend === "darwin-ulimit-process-group" /* DarwinUlimitProcessGroup */;
306
+ var plan = buildBackendLaunchPlan({
307
+ backend,
308
+ options,
309
+ jobObjectShimPath: backend === "windows-job-object" /* WindowsJobObject */ ? resolveJobObjectShimPath() : void 0,
310
+ launcherPid: process.pid,
311
+ commandNeedsCmdShell: backend === "windows-job-object" /* WindowsJobObject */ ? windowsCommandNeedsCmdShell(options.command[0]) : false,
312
+ scopeUnitName: `muggle-guard-${process.pid}-${Date.now()}.scope`,
313
+ nprocCeiling: needsNprocCeiling ? computeNprocCeiling({
314
+ currentUserProcessCount: currentUserProcessCount(),
315
+ activeProcessLimit: options.activeProcessLimit
316
+ }) : void 0
317
+ });
318
+ var child = spawn(plan.executable, plan.args, {
319
+ stdio: "inherit",
320
+ detached: plan.spawnDetached,
321
+ windowsHide: true
322
+ });
323
+ child.on("error", (error) => fail(`failed to launch backend: ${String(error)}`));
324
+ registerTreeCleanup(plan, child.pid);
325
+ child.on("exit", (code, signal) => {
326
+ process.exitCode = code ?? (signal === "SIGKILL" ? 137 : signal === "SIGTERM" ? 143 : 1);
327
+ });
@@ -9,12 +9,13 @@ set -uo pipefail
9
9
  # Fires after every Bash call and every muggle execute/replay, so a keyword
10
10
  # pre-filter for test runners and the muggle E2E tool names keeps Node off the
11
11
  # hot path. Only a `test` command (npm/pnpm/yarn/jest/vitest/pytest/go/cargo),
12
- # a muggle execute/replay/test-generation event, or an E2E skip marker reaches
13
- # guardrails.mjs, which then inspects the output for pass/fail and updates
14
- # state. Degrades to {}.
12
+ # a muggle execute/replay/test-generation event, a muggle-test skill telemetry
13
+ # emit (registers a clean-SKIP verdict as an E2E run), or an E2E skip marker
14
+ # reaches guardrails.mjs, which then inspects the output for pass/fail and
15
+ # updates state. Degrades to {}.
15
16
  payload="$(cat)"
16
17
 
17
- if ! grep -Eiq '(pnpm|npm|yarn)[[:space:]]+(run[[:space:]]+)?test|jest|vitest|pytest|go[[:space:]]+test|cargo[[:space:]]+test|muggle.*(execute|test-generation|replay)|MUGGLE_E2E_SKIP' <<<"$payload"; then
18
+ if ! grep -Eiq '(pnpm|npm|yarn)[[:space:]]+(run[[:space:]]+)?test|jest|vitest|pytest|go[[:space:]]+test|cargo[[:space:]]+test|muggle.*(execute|test-generation|replay)|muggle-local-telemetry-skill-emit|MUGGLE_E2E_SKIP' <<<"$payload"; then
18
19
  printf '{}'
19
20
  exit 0
20
21
  fi
@@ -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
+ }
@@ -1,36 +1,39 @@
1
- # Skill authoring conventions
2
-
3
- Rules for every skill under `plugin/skills/`. Read before adding or editing one.
4
-
5
- ## One-way dependencies — no reverse references
6
-
7
- Skill cross-references form a one-way graph. If any file in skill **A** references skill **B** — a markdown link to B's files, or a documented dependency on B's internals — then **no file in B may reference A back**. Reference *downward*, toward the more general / lower-level skill you depend on; pass anything the other direction needs as input, not as a link.
8
-
9
- A reverse reference (A → B and B → A) couples the depended-on skill to its caller, creates a cycle no one can reason about in isolation, and makes every edit ripple both ways. The lower-level skill must stay reusable by callers it has never heard of.
10
-
11
- **Runtime dispatch is not a doc reference.** A dumb-pipe skill may *fire* another skill's slash command at runtime (hand off and forget) — that is an action, not a dependency. What the rule forbids is a procedure file **linking to** or **encoding the internals of** the skill it hands off to.
12
-
13
- **Worked example.** `muggle-pr-followup` (the dumb-pipe watcher) is lower-level than `muggle-do` (the executor that orchestrates it). `muggle-do` references `muggle-pr-followup`; `muggle-pr-followup`'s files must not link back to `muggle-do`. A watcher tick still dispatches `/muggle-do …` at runtime — allowed — but no watcher file links a `do/` file or restates its steps, and shared primitives like `muggle-pr-followup/finalize.md` stay dispatch-free so any caller can reuse them.
14
-
15
- When you feel the urge to link "up" to a caller, that is the smell — restructure so the caller passes what is needed in.
16
-
17
- ### Enforcement
18
-
19
- `scripts/check-skill-deps.mjs` derives the cross-skill link graph and fails on any cycle. A "reference" is a markdown file-link into another skill's directory — runtime slash-command dispatch is not a link and is not counted. It runs three ways: the `skill-deps` CI job on every PR, a `PreToolUse` hook (`.claude/settings.json`) that blocks the write mid-session with the offending link named, and `pnpm run verify:skill-deps` locally.
20
-
21
- `plugin/skills/skill-deps.config.json` declares support dirs grouped into their owning skill (`do/` → `muggle-do`), shared namespaces exploded to per-file nodes (`_shared`), and `knownReverseDeps` — pre-existing violations grandfathered so CI stays green. That list is debt: fix each link and delete its entry. A new reverse dependency is blocked whether or not it is on the list.
22
-
23
- ## Model tiers
24
-
25
- Each skill sets a `model:` in its `SKILL.md` frontmatter sized to its cognitive load. `model:` is a native Claude Code field — the override applies while the skill is active and reverts to the session model when it exits. Cheaper, faster models run the mechanical skills; the default (Opus) is reserved for the ones that actually reason. Cost and latency scale with the model, and these skills run often (the watcher fires every minute), so the tier is a real lever, not cosmetics.
26
-
27
- | Model | Skills | Why this tier |
28
- |-------|--------|---------------|
29
- | `haiku` | `muggle`, `muggle-status`, `muggle-repair`, `muggle-upgrade`, `muggle-preferences`, `muggle-feedback`, `muggle-pr-followup` | Routers and dumb pipes. They follow an explicit procedure with no open-ended reasoning: route intent to a downstream skill, run a fixed CLI sequence, CRUD a config file, format a status report, or poll provider state and branch on conditions. `muggle-pr-followup` is the canonical case — a watcher that reads GitHub state and dispatches; all judgment lives in the `muggle-do` it hands off to. |
30
- | `sonnet` | `muggle-pr-visual-walkthrough`, `muggle-test-regenerate-missing` | Multi-step orchestration with light judgment, short of deep reasoning: assemble run data and build a PR section with fit-vs-overflow handling; scan, filter, bulk-dispatch, and classify per-item failures into buckets. More moving parts than a router, but each step is well-defined. |
31
- | `opus` (explicit pin) | `muggle-test-prepare` | Pinned for **reliability**, not raw reasoning load: it's flaky on smaller models, and since other skills gate on the environment it readies, a wrong call is expensive. Pin explicitly rather than leaving `model:` unset so it stays on Opus even when the user's session runs a cheaper model. |
32
- | default (Opus) — no `model:` set | `muggle-do`, `muggle-test`, `muggle-test-feature-local`, `muggle-browser-task`, `muggle-test-import` | Reasoning-heavy. Authoring code to a PR, mapping a code diff to affected user flows and interpreting E2E results, reasoning about an arbitrary website's flow to drive a browser, translating Playwright/Cypress/PRD artifacts into Muggle test cases. Leave `model:` unset so the skill inherits the session model. |
33
-
34
- **Choosing a tier for a new skill.** Ask what the skill actually does. Pure routing / fixed procedure / CRUD / reporting → `haiku`. Several well-defined steps with some judgment or classification → `sonnet`. Open-ended reasoning, code authoring, or interpreting ambiguous real-world state → leave `model:` unset (Opus). When unsure between two tiers, pick the cheaper one and watch for misbehavior — the likeliest to need a bump is anything doing AI-based classification. If a skill proves flaky on its tier and reliability matters more than cost (other skills depend on it, or a wrong call is expensive), pin it up explicitly — `model: opus` — rather than leaving it unset, so the floor holds regardless of the user's session model.
35
-
36
- **Never set `model:` on aliases or commands.** The alias skills (`m`, `mstatus`, …) and `plugin/commands/*.md` are thin routers that re-invoke the canonical skill via the `Skill` tool. The canonical `SKILL.md`'s `model:` takes effect once it loads, so a model on the alias would only apply to the negligible one-line hand-off — and risks drifting from the canonical value.
1
+ # Skill authoring conventions
2
+
3
+ Rules for every skill under `plugin/skills/`. Read before adding or editing one.
4
+
5
+ ## One-way dependencies — no reverse references
6
+
7
+ Skill cross-references form a one-way graph. If any file in skill **A** references skill **B** — a markdown link to B's files, or a documented dependency on B's internals — then **no file in B may reference A back**. Reference *downward*, toward the more general / lower-level skill you depend on; pass anything the other direction needs as input, not as a link.
8
+
9
+ A reverse reference (A → B and B → A) couples the depended-on skill to its caller, creates a cycle no one can reason about in isolation, and makes every edit ripple both ways. The lower-level skill must stay reusable by callers it has never heard of.
10
+
11
+ **Runtime dispatch is not a doc reference.** A dumb-pipe skill may *fire* another skill's slash command at runtime (hand off and forget) — that is an action, not a dependency. What the rule forbids is a procedure file **linking to** or **encoding the internals of** the skill it hands off to.
12
+
13
+ **Worked example.** `muggle-pr-followup` (the dumb-pipe watcher) is lower-level than `muggle-do` (the executor that orchestrates it). `muggle-do` references `muggle-pr-followup`; `muggle-pr-followup`'s files must not link back to `muggle-do`. A watcher tick still dispatches `/muggle-do …` at runtime — allowed — but no watcher file links a `do/` file or restates its steps, and shared primitives like `muggle-pr-followup/finalize.md` stay dispatch-free so any caller can reuse them.
14
+
15
+ When you feel the urge to link "up" to a caller, that is the smell — restructure so the caller passes what is needed in.
16
+
17
+ ### Enforcement
18
+
19
+ `scripts/check-skill-deps.mjs` derives the cross-skill link graph and fails on any cycle. A "reference" is a markdown file-link into another skill's directory — runtime slash-command dispatch is not a link and is not counted. It runs three ways: the `skill-deps` CI job on every PR, a `PreToolUse` hook (`.claude/settings.json`) that blocks the write mid-session with the offending link named, and `pnpm run verify:skill-deps` locally.
20
+
21
+ `plugin/skills/skill-deps.config.json` declares support dirs grouped into their owning skill (`do/` → `muggle-do`), shared namespaces exploded to per-file nodes (`_shared`), and `knownReverseDeps` — pre-existing violations grandfathered so CI stays green. That list is debt: fix each link and delete its entry. A new reverse dependency is blocked whether or not it is on the list.
22
+
23
+ ## Model tiers
24
+
25
+ Each skill sets a `model:` in its `SKILL.md` frontmatter sized to its cognitive load. `model:` is a native Claude Code field — the override applies while the skill is active and reverts to the session model when it exits. Cheaper, faster models run the mechanical skills; the default (Opus) is reserved for the ones that actually reason. Cost and latency scale with the model, and these skills run often (the watcher fires every minute), so the tier is a real lever, not cosmetics.
26
+
27
+ | Model | Skills | Why this tier |
28
+ |-------|--------|---------------|
29
+ | `haiku` | `muggle`, `muggle-status`, `muggle-repair`, `muggle-upgrade`, `muggle-preferences`, `muggle-feedback`, `muggle-pr-followup` | Routers and dumb pipes. They follow an explicit procedure with no open-ended reasoning: route intent to a downstream skill, run a fixed CLI sequence, CRUD a config file, format a status report, or poll provider state and branch on conditions. `muggle-pr-followup` is the canonical case — a watcher that reads GitHub state and dispatches; all judgment lives in the `muggle-do` it hands off to. |
30
+ | `sonnet` | `muggle-pr-visual-walkthrough` (executes via the `visual-walkthrough-builder` agent), `muggle-test-regenerate-missing` | Multi-step orchestration with light judgment, short of deep reasoning: assemble run data and build a PR section with fit-vs-overflow handling; scan, filter, bulk-dispatch, and classify per-item failures into buckets. More moving parts than a router, but each step is well-defined. |
31
+ | `opus` (explicit pin) | `muggle-test-prepare` (executes via the `test-prepare-runner` agent) | Pinned for **reliability**, not raw reasoning load: it's flaky on smaller models, and since other skills gate on the environment it readies, a wrong call is expensive. Pin explicitly rather than leaving `model:` unset so it stays on Opus even when the user's session runs a cheaper model. |
32
+
33
+ | default (Opus) — no `model:` set | `muggle-do`, `muggle-test`, `muggle-test-feature-local`, `muggle-browser-task`, `muggle-test-import` | Reasoning-heavy. Authoring code to a PR, mapping a code diff to affected user flows and interpreting E2E results, reasoning about an arbitrary website's flow to drive a browser, translating Playwright/Cypress/PRD artifacts into Muggle test cases. Leave `model:` unset so the skill inherits the session model. |
34
+
35
+ **A `SKILL.md` `model:` only bites when the skill starts a fresh session; invoked mid-session the session model keeps running.** A skill whose pin must hold regardless resolves its user interaction in the `SKILL.md`, then dispatches a `plugin/agents/*` agent that carries the pin — the harness applies an agent's `model:` on every dispatch. That is why `muggle-pr-visual-walkthrough`, `muggle-test-prepare`, and `muggle-pr-followup` execute through agents. Agents have no user channel; an unresolved decision comes back as `needs-input:` for the skill to resolve.
36
+
37
+ **Choosing a tier for a new skill.** Ask what the skill actually does. Pure routing / fixed procedure / CRUD / reporting → `haiku`. Several well-defined steps with some judgment or classification → `sonnet`. Open-ended reasoning, code authoring, or interpreting ambiguous real-world state → leave `model:` unset (Opus). When unsure between two tiers, pick the cheaper one and watch for misbehavior — the likeliest to need a bump is anything doing AI-based classification. If a skill proves flaky on its tier and reliability matters more than cost (other skills depend on it, or a wrong call is expensive), pin it up explicitly — `model: opus` — rather than leaving it unset, so the floor holds regardless of the user's session model.
38
+
39
+ **Never set `model:` on aliases or commands.** The alias skills (`m`, `mstatus`, …) and `plugin/commands/*.md` are thin routers that re-invoke the canonical skill via the `Skill` tool. The canonical `SKILL.md`'s `model:` takes effect once it loads, so a model on the alias would only apply to the negligible one-line hand-off — and risks drifting from the canonical value.
@@ -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).