@muggleai/works 5.19.0-staging.129 → 5.19.0-staging.130

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 (33) hide show
  1. package/dist/{chunk-JNYP5PWT.js → chunk-73AP7MJR.js} +3 -3
  2. package/dist/{chunk-ESPHO3WB.js → chunk-UHA4WKV4.js} +28 -2
  3. package/dist/cli.js +2 -2
  4. package/dist/index.js +2 -2
  5. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  6. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  7. package/dist/plugin/config/preference-defaults.json +26 -25
  8. package/dist/plugin/skills/do/resolve-conflicts.md +68 -59
  9. package/dist/plugin/skills/muggle-pr-followup/blocked-tick.md +40 -38
  10. package/dist/plugin/skills/muggle-pr-followup/contract.md +8 -2
  11. package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +6 -1
  12. package/dist/plugin/skills/muggle-preferences/SKILL.md +33 -33
  13. package/dist/plugin/skills/muggle-preferences/ops/configure.md +51 -50
  14. package/dist/plugin/skills/muggle-preferences/ops/list.md +11 -10
  15. package/dist/plugin/skills/muggle-preferences/ops/onboard.md +54 -54
  16. package/dist/plugin/skills/muggle-preferences/preference-gates/README.md +69 -63
  17. package/dist/plugin/skills/muggle-preferences/preference-gates/maxCatchUpRebases.md +28 -0
  18. package/dist/release-manifest.json +3 -3
  19. package/dist/{src-ZF6QSZJV.js → src-WNUW6D34.js} +1 -1
  20. package/package.json +1 -1
  21. package/plugin/.claude-plugin/plugin.json +1 -1
  22. package/plugin/.cursor-plugin/plugin.json +1 -1
  23. package/plugin/config/preference-defaults.json +26 -25
  24. package/plugin/skills/do/resolve-conflicts.md +68 -59
  25. package/plugin/skills/muggle-pr-followup/blocked-tick.md +40 -38
  26. package/plugin/skills/muggle-pr-followup/contract.md +8 -2
  27. package/plugin/skills/muggle-pr-followup/state-schemas.md +6 -1
  28. package/plugin/skills/muggle-preferences/SKILL.md +33 -33
  29. package/plugin/skills/muggle-preferences/ops/configure.md +51 -50
  30. package/plugin/skills/muggle-preferences/ops/list.md +11 -10
  31. package/plugin/skills/muggle-preferences/ops/onboard.md +54 -54
  32. package/plugin/skills/muggle-preferences/preference-gates/README.md +69 -63
  33. package/plugin/skills/muggle-preferences/preference-gates/maxCatchUpRebases.md +28 -0
@@ -1,4 +1,4 @@
1
- import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require, getDisclosureCopy, buildOnboardingPlan, applyOnboardingAnswers, needsOnboarding, OnboardingBlanketChoice, PreferenceKey } from './chunk-ESPHO3WB.js';
1
+ import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require, getDisclosureCopy, buildOnboardingPlan, applyOnboardingAnswers, needsOnboarding, OnboardingBlanketChoice, PreferenceKey } from './chunk-UHA4WKV4.js';
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { v4 } from 'uuid';
@@ -754,7 +754,7 @@ async function resolveGsScreenshotUrls(report, opts) {
754
754
  if (gsUrls.length === 0) {
755
755
  return report;
756
756
  }
757
- const mcps = await import('./src-ZF6QSZJV.js');
757
+ const mcps = await import('./src-WNUW6D34.js');
758
758
  const credentials = await mcps.getCallerCredentialsAsync();
759
759
  if (!credentials.bearerToken && !credentials.apiKey) {
760
760
  stderrWrite(
@@ -807,7 +807,7 @@ var withSentinel = (s) => s ? `${REPORT_SECTION_SENTINEL}
807
807
  ${s}` : s;
808
808
  async function resolveDashboardBaseUrl(stderrWrite) {
809
809
  try {
810
- const mcps = await import('./src-ZF6QSZJV.js');
810
+ const mcps = await import('./src-WNUW6D34.js');
811
811
  return mcps.resolveActiveProfile().dashboardBaseUrl;
812
812
  } catch (err) {
813
813
  stderrWrite(
@@ -2230,6 +2230,7 @@ var PreferenceKey = /* @__PURE__ */ ((PreferenceKey2) => {
2230
2230
  PreferenceKey2["AutoRouteBuildToMuggleDo"] = "autoRouteBuildToMuggleDo";
2231
2231
  PreferenceKey2["AutoWatchPR"] = "autoWatchPR";
2232
2232
  PreferenceKey2["WatcherLifetime"] = "watcherLifetime";
2233
+ PreferenceKey2["MaxCatchUpRebases"] = "maxCatchUpRebases";
2233
2234
  PreferenceKey2["ReusePreparePlan"] = "reusePreparePlan";
2234
2235
  return PreferenceKey2;
2235
2236
  })(PreferenceKey || {});
@@ -2241,6 +2242,9 @@ var PreferenceValue = /* @__PURE__ */ ((PreferenceValue2) => {
2241
2242
  PreferenceValue2["Remote"] = "remote";
2242
2243
  PreferenceValue2["OneDay"] = "1d";
2243
2244
  PreferenceValue2["SevenDays"] = "7d";
2245
+ PreferenceValue2["TenRebases"] = "10";
2246
+ PreferenceValue2["TwentyRebases"] = "20";
2247
+ PreferenceValue2["FiftyRebases"] = "50";
2244
2248
  return PreferenceValue2;
2245
2249
  })(PreferenceValue || {});
2246
2250
 
@@ -2271,7 +2275,8 @@ var DEFAULT_PREFERENCES = {
2271
2275
  ["autoRouteBuildToMuggleDo" /* AutoRouteBuildToMuggleDo */]: "always" /* Always */,
2272
2276
  ["autoWatchPR" /* AutoWatchPR */]: "always" /* Always */,
2273
2277
  ["reusePreparePlan" /* ReusePreparePlan */]: "always" /* Always */,
2274
- ["watcherLifetime" /* WatcherLifetime */]: "7d" /* SevenDays */
2278
+ ["watcherLifetime" /* WatcherLifetime */]: "7d" /* SevenDays */,
2279
+ ["maxCatchUpRebases" /* MaxCatchUpRebases */]: "20" /* TwentyRebases */
2275
2280
  };
2276
2281
  var ALWAYS_ASK_NEVER = [
2277
2282
  "always" /* Always */,
@@ -2292,6 +2297,12 @@ var WATCHER_LIFETIMES = [
2292
2297
  "7d" /* SevenDays */,
2293
2298
  "never" /* Never */
2294
2299
  ];
2300
+ var CATCH_UP_REBASE_BUDGETS = [
2301
+ "10" /* TenRebases */,
2302
+ "20" /* TwentyRebases */,
2303
+ "50" /* FiftyRebases */,
2304
+ "never" /* Never */
2305
+ ];
2295
2306
  var PREFERENCE_ALLOWED_VALUES = {
2296
2307
  ["autoLogin" /* AutoLogin */]: ALWAYS_ASK_NEVER,
2297
2308
  ["autoSelectProject" /* AutoSelectProject */]: ALWAYS_ASK_NEVER,
@@ -2315,7 +2326,8 @@ var PREFERENCE_ALLOWED_VALUES = {
2315
2326
  ["autoRouteBuildToMuggleDo" /* AutoRouteBuildToMuggleDo */]: ALWAYS_ASK_NEVER,
2316
2327
  ["autoWatchPR" /* AutoWatchPR */]: ALWAYS_ASK_NEVER,
2317
2328
  ["reusePreparePlan" /* ReusePreparePlan */]: ALWAYS_ASK_NEVER,
2318
- ["watcherLifetime" /* WatcherLifetime */]: WATCHER_LIFETIMES
2329
+ ["watcherLifetime" /* WatcherLifetime */]: WATCHER_LIFETIMES,
2330
+ ["maxCatchUpRebases" /* MaxCatchUpRebases */]: CATCH_UP_REBASE_BUDGETS
2319
2331
  };
2320
2332
  var PREFERENCES_SCHEMA = {
2321
2333
  ["autoLogin" /* AutoLogin */]: {
@@ -2386,6 +2398,9 @@ var PREFERENCES_SCHEMA = {
2386
2398
  },
2387
2399
  ["watcherLifetime" /* WatcherLifetime */]: {
2388
2400
  description: "How long a muggle-pr-followup watch loop polls before retiring itself \u2014 1d, 7d, or never (unbounded, which leaves no time-based reaper for an orphaned loop)"
2401
+ },
2402
+ ["maxCatchUpRebases" /* MaxCatchUpRebases */]: {
2403
+ description: "How many catch-up rebases muggle-pr-followup runs on one PR before it stops and reports the branch back to its owner \u2014 10, 20, 50, or never (unbounded, which lets an active base rebase a PR for as long as it stays open)"
2389
2404
  }
2390
2405
  };
2391
2406
 
@@ -8121,6 +8136,17 @@ var ONBOARDING_CHOICE_GROUPS = [
8121
8136
  { value: "7d" /* SevenDays */, label: "Retire after 7 days" },
8122
8137
  { value: "never" /* Never */, label: "Never retire \u2014 no time-based reaper for an orphaned loop" }
8123
8138
  ]
8139
+ },
8140
+ {
8141
+ header: "Rebase budget",
8142
+ key: "maxCatchUpRebases" /* MaxCatchUpRebases */,
8143
+ prompt: "How many times should a PR be rebased onto a moving base before you are asked to step in?",
8144
+ optionLabels: [
8145
+ { value: "10" /* TenRebases */, label: "Stop after 10" },
8146
+ { value: "20" /* TwentyRebases */, label: "Stop after 20" },
8147
+ { value: "50" /* FiftyRebases */, label: "Stop after 50" },
8148
+ { value: "never" /* Never */, label: "Never stop \u2014 an active base can rebase a PR for as long as it stays open" }
8149
+ ]
8124
8150
  }
8125
8151
  ];
8126
8152
  var ONBOARDING_BLANKET_OPTIONS = [
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-JNYP5PWT.js';
3
- import './chunk-ESPHO3WB.js';
2
+ import { runCli } from './chunk-73AP7MJR.js';
3
+ import './chunk-UHA4WKV4.js';
4
4
 
5
5
  // src/cli/main.ts
6
6
  runCli().catch((error) => {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-JNYP5PWT.js';
2
- export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-ESPHO3WB.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-73AP7MJR.js';
2
+ export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-UHA4WKV4.js';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
4
- "version": "5.19.0-staging.129",
4
+ "version": "5.19.0-staging.130",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
5
- "version": "5.19.0-staging.129",
5
+ "version": "5.19.0-staging.130",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -1,25 +1,26 @@
1
- {
2
- "autoLogin": "always",
3
- "autoSelectProject": "always",
4
- "autoSelectLocalHost": "always",
5
- "showElectronBrowser": "always",
6
- "openTestResultsAfterRun": "always",
7
- "defaultExecutionMode": "local",
8
- "suggestRelatedUseCases": "always",
9
- "suggestRelatedTestCases": "always",
10
- "autoDetectChanges": "always",
11
- "postPRVisualWalkthrough": "always",
12
- "autoCreatePR": "always",
13
- "checkForUpdates": "always",
14
- "verboseOutput": "never",
15
- "autoUseWorktree": "always",
16
- "autoRebase": "always",
17
- "autoCleanup": "always",
18
- "autoE2ETest": "always",
19
- "autoResolveConflicts": "always",
20
- "autoReuseValidationContext": "always",
21
- "autoRouteBuildToMuggleDo": "always",
22
- "autoWatchPR": "always",
23
- "reusePreparePlan": "always",
24
- "watcherLifetime": "7d"
25
- }
1
+ {
2
+ "autoLogin": "always",
3
+ "autoSelectProject": "always",
4
+ "autoSelectLocalHost": "always",
5
+ "showElectronBrowser": "always",
6
+ "openTestResultsAfterRun": "always",
7
+ "defaultExecutionMode": "local",
8
+ "suggestRelatedUseCases": "always",
9
+ "suggestRelatedTestCases": "always",
10
+ "autoDetectChanges": "always",
11
+ "postPRVisualWalkthrough": "always",
12
+ "autoCreatePR": "always",
13
+ "checkForUpdates": "always",
14
+ "verboseOutput": "never",
15
+ "autoUseWorktree": "always",
16
+ "autoRebase": "always",
17
+ "autoCleanup": "always",
18
+ "autoE2ETest": "always",
19
+ "autoResolveConflicts": "always",
20
+ "autoReuseValidationContext": "always",
21
+ "autoRouteBuildToMuggleDo": "always",
22
+ "autoWatchPR": "always",
23
+ "reusePreparePlan": "always",
24
+ "watcherLifetime": "7d",
25
+ "maxCatchUpRebases": "20"
26
+ }
@@ -1,59 +1,68 @@
1
- # Rebase (watcher-dispatched)
2
-
3
- Rebase a PR's branch onto its base — whether it's merely **behind** (out of date, no conflict) or actually **conflicting** — behind a verify-or-rollback gate, then force-push, so a PR doesn't sit stale or un-mergeable forever. A dumb-pipe dispatch like fix-ci: the watcher detects the branch is behind or conflicting and hands off; the executor owns the rebase (and any conflict resolution), never the decision to dispatch.
4
-
5
- ## Turn preamble
6
-
7
- ```
8
- **/muggle-do rebase** — rebasing <owner>/<repo>#<n> onto <base> to bring the branch up to date.
9
- ```
10
-
11
- ## Input
12
-
13
- `$ARGUMENTS` carries a `github.com/.../pull/<n>` URL, `slug=<slug>`, and a `rebase` directive (no review ids, no failing check names). Parse all three.
14
-
15
- ## Inputs from disk
16
-
17
- From `~/.muggle-ai/muggle-do/sessions/<slug>/`: `prs.json` (PR + branch + `head_sha`), `last_seen.json` (`conflict_resolve_attempts`, `conflict_escalated_keys`, `pushed_shas`), `state.md` (worktree path, validation strategy, base branch).
18
-
19
- ## Procedure
20
-
21
- ### Step 1 — Re-attach
22
-
23
- Materialize the PR branch in its worktree per [`../_shared/pr-branch-worktree.md`](../_shared/pr-branch-worktree.md) (or use `state.md`'s `worktreePath`). Capture `rebase_sha = prs.json[0].head_sha` and the base branch (`baseRefName` from [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md)). Capture the base tip too — `.base_commit.sha` from that recipe's `compare` call — and form `rebase_key = "<rebase_sha>..<base_tip_sha>"`. Both budget fields below are keyed on that pair, matching the watcher's Step 5; writing a bare SHA instead leaves an entry the watcher ignores, and the rebase re-dispatches forever.
24
-
25
- ### Step 2 — Rebase onto base (resolve conflicts if any)
26
-
27
- Run the rebase from [`../_shared/sync-branch-with-base.md`](../_shared/sync-branch-with-base.md) against the base branch, taking its `always` path unconditionally — this programmatic mode never asks, so skip the `autoRebase` prompt (the watcher already decided a rebase is due).
28
-
29
- - **Clean replay** — a behind-only branch (and any rebase that hits no conflicts) replays without intervention. Proceed to Step 3.
30
- - **Conflicts** — handle per [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
31
- - `never` → abort and escalate per Step 5 (`kind: "rebase-conflict"`). The watcher keeps polling; the user resolves on GitHub.
32
- - `always` → resolve via [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md); Steps 3 and 5 below are this mode's instance of the [`../_shared/verify-or-rollback-gate.md`](../_shared/verify-or-rollback-gate.md).
33
-
34
- ### Step 3 — Verify the resolution
35
-
36
- Build (typecheck + lint on the changed surface) + unit suite must pass. Run E2E per [`e2e-acceptance.md`](e2e-acceptance.md) when app logic changed and the session carries validation context. A rebase that does not verify is rolled back → escalate per Step 5. **Never push an unverified rebase.**
37
-
38
- ### Step 4 — Force-push + respawn
39
-
40
- Force-push per [`../_shared/vcs/common/push-to-branch.md`](../_shared/vcs/common/push-to-branch.md) (tool-agnostic instruction; handles the signing gate for rebased commits and directs to provider-specific force-push paths). Use `git push --force-with-lease` for the force operation. Append the new SHA to `last_seen.pushed_shas`; increment `last_seen.conflict_resolve_attempts[rebase_key]` — both whole-file rewrites (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md), never the Edit tool. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md). Its next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
41
-
42
- ### Step 5 — Escalate (can't resolve / budget spent)
43
-
44
- When `autoResolveConflicts=never`, the resolution failed verification, or `conflict_resolve_attempts[rebase_key]` has reached 2:
45
-
46
- 1. Add `rebase_key` to `last_seen.conflict_escalated_keys` so the watcher does not re-dispatch this head against this base. If the base later moves, the key changes and the branch re-arms on its own — a conflict the user resolved upstream stops being this watcher's dead end.
47
- 2. Emit one terminal escalation naming the PR and the conflicting files (or the failing verification, for a behind-only rebase that didn't verify).
48
- 3. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md) — it keeps polling for the user's manual resolution or any new reviews.
49
-
50
- ### Step 6 — Telemetry
51
-
52
- Emit one `muggle-do:cycle` event ([`../_shared/telemetry-events/muggle-do-cycle.md`](../_shared/telemetry-events/muggle-do-cycle.md)) with `outcome: "rebased"` (a verified rebase pushed — behind-only or conflicts resolved) or `"rebase-escalated"`.
53
-
54
- ## Guardrails
55
-
56
- - Max 2 rebase attempts per SHA; then escalate rather than churn.
57
- - Never push an unverified rebase — verify-or-rollback always.
58
- - Never push unsigned commits — every push or force-push goes through the signing gate in [`../_shared/vcs/common/push-to-branch.md`](../_shared/vcs/common/push-to-branch.md).
59
- - Resolve `autoResolveConflicts` from the configured preference (per the gate contract — don't assume a default): `always` resolves conflicts behind the verify-or-rollback gate, `never` escalates to the user. A clean behind-only rebase needs neither.
1
+ # Rebase (watcher-dispatched)
2
+
3
+ Rebase a PR's branch onto its base — whether it's merely **behind** (out of date, no conflict) or actually **conflicting** — behind a verify-or-rollback gate, then force-push, so a PR doesn't sit stale or un-mergeable forever. A dumb-pipe dispatch like fix-ci: the watcher detects the branch is behind or conflicting and hands off; the executor owns the rebase (and any conflict resolution), never the decision to dispatch.
4
+
5
+ ## Turn preamble
6
+
7
+ ```
8
+ **/muggle-do rebase** — rebasing <owner>/<repo>#<n> onto <base> to bring the branch up to date.
9
+ ```
10
+
11
+ ## Input
12
+
13
+ `$ARGUMENTS` carries a `github.com/.../pull/<n>` URL, `slug=<slug>`, and a `rebase` directive (no review ids, no failing check names). Parse all three.
14
+
15
+ ## Inputs from disk
16
+
17
+ From `~/.muggle-ai/muggle-do/sessions/<slug>/`: `prs.json` (PR + branch + `head_sha`), `last_seen.json` (`conflict_resolve_attempts`, `conflict_escalated_keys`, `catch_up_rebases`, `pushed_shas`), `state.md` (worktree path, validation strategy, base branch).
18
+
19
+ ## Procedure
20
+
21
+ ### Step 1 — Re-attach
22
+
23
+ Materialize the PR branch in its worktree per [`../_shared/pr-branch-worktree.md`](../_shared/pr-branch-worktree.md) (or use `state.md`'s `worktreePath`). Capture `rebase_sha = prs.json[0].head_sha` and the base branch (`baseRefName` from [`../_shared/vcs/github/pr-metadata.md`](../_shared/vcs/github/pr-metadata.md)). Capture the base tip too — `.base_commit.sha` from that recipe's `compare` call — and form `rebase_key = "<rebase_sha>..<base_tip_sha>"`. Both budget fields below are keyed on that pair, matching the watcher's Step 5; writing a bare SHA instead leaves an entry the watcher ignores, and the rebase re-dispatches forever.
24
+
25
+ ### Step 2 — Rebase onto base (resolve conflicts if any)
26
+
27
+ Run the rebase from [`../_shared/sync-branch-with-base.md`](../_shared/sync-branch-with-base.md) against the base branch, taking its `always` path unconditionally — this programmatic mode never asks, so skip the `autoRebase` prompt (the watcher already decided a rebase is due).
28
+
29
+ - **Clean replay** — a behind-only branch (and any rebase that hits no conflicts) replays without intervention. Proceed to Step 3.
30
+ - **Conflicts** — handle per [`autoResolveConflicts`](../muggle-preferences/preference-gates/autoResolveConflicts.md):
31
+ - `never` → abort and escalate per Step 5 (`kind: "rebase-conflict"`). The watcher keeps polling; the user resolves on GitHub.
32
+ - `always` → resolve via [`../_shared/resolve-rebase-conflicts.md`](../_shared/resolve-rebase-conflicts.md); Steps 3 and 5 below are this mode's instance of the [`../_shared/verify-or-rollback-gate.md`](../_shared/verify-or-rollback-gate.md).
33
+
34
+ ### Step 3 — Verify the resolution
35
+
36
+ Build (typecheck + lint on the changed surface) + unit suite must pass. Run E2E per [`e2e-acceptance.md`](e2e-acceptance.md) when app logic changed and the session carries validation context. A rebase that does not verify is rolled back → escalate per Step 5. **Never push an unverified rebase.**
37
+
38
+ ### Step 4 — Force-push + respawn
39
+
40
+ Force-push per [`../_shared/vcs/common/push-to-branch.md`](../_shared/vcs/common/push-to-branch.md) (tool-agnostic instruction; handles the signing gate for rebased commits and directs to provider-specific force-push paths). Use `git push --force-with-lease` for the force operation. Append the new SHA to `last_seen.pushed_shas`; increment `last_seen.conflict_resolve_attempts[rebase_key]` **and** `last_seen.catch_up_rebases` — all whole-file rewrites (Read → change field → Write) per [`../_shared/session-state-writes.md`](../_shared/session-state-writes.md), never the Edit tool. The second counter is unkeyed on purpose: `rebase_key` carries the base tip, so it mints a fresh budget every time the base moves and cannot bound a PR on an active base. Increment it on every push here, including one that later escalates, or a branch that fails the same way twenty times reads as having spent nothing. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md). Its next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
41
+
42
+ ### Step 5 — Escalate (can't resolve / budget spent)
43
+
44
+ When `autoResolveConflicts=never`, the resolution failed verification, or `conflict_resolve_attempts[rebase_key]` has reached 2:
45
+
46
+ 1. Add `rebase_key` to `last_seen.conflict_escalated_keys` so the watcher does not re-dispatch this head against this base. If the base later moves, the key changes and the branch re-arms on its own — a conflict the user resolved upstream stops being this watcher's dead end.
47
+ 2. Emit one terminal escalation naming the PR and the conflicting files (or the failing verification, for a behind-only rebase that didn't verify).
48
+ 3. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md) — it keeps polling for the user's manual resolution or any new reviews.
49
+
50
+ ### Step 5a — Escalate (per-PR catch-up budget spent)
51
+
52
+ When `catch_up_rebases` has reached [`maxCatchUpRebases`](../muggle-preferences/preference-gates/maxCatchUpRebases.md) (20 unless the user changed it), the PR is out of catch-up budget for good — a different escalation from Step 5, which retires one `rebase_key` and re-arms the moment the base moves.
53
+
54
+ 1. Set `last_seen.rebase_budget_exhausted` to `true`. The watcher stops dispatching rebases for this PR outright, so no base advance re-arms it.
55
+ 2. Emit one terminal escalation, and post it on the PR, naming the count, the cap, and `maxCatchUpRebases` as the way to raise it. Say what the owner has to decide — rebase onto a different base, split the branch, or close it — because the harness has stopped trying and silence here reads as a healthy watcher.
56
+ 3. Respawn the watcher per [`respawn-watcher.md`](respawn-watcher.md); it keeps polling reviews and CI, and blocks with reason `rebase_budget_exhausted`.
57
+
58
+ ### Step 6 — Telemetry
59
+
60
+ Emit one `muggle-do:cycle` event ([`../_shared/telemetry-events/muggle-do-cycle.md`](../_shared/telemetry-events/muggle-do-cycle.md)) with `outcome: "rebased"` (a verified rebase pushed — behind-only or conflicts resolved), `"rebase-escalated"`, or `"rebase-budget-exhausted"` (Step 5a).
61
+
62
+ ## Guardrails
63
+
64
+ - Max 2 rebase attempts per `rebase_key`, and `maxCatchUpRebases` (20 by default) across the PR's whole life; then escalate rather than churn. The per-key cap alone bounds nothing on an active base, since a base advance mints a new key.
65
+ - Never let an exhausted budget pass as silence — Step 5a reports on the PR. A watcher that stops without saying so is indistinguishable from one that has nothing to do.
66
+ - Never push an unverified rebase — verify-or-rollback always.
67
+ - Never push unsigned commits — every push or force-push goes through the signing gate in [`../_shared/vcs/common/push-to-branch.md`](../_shared/vcs/common/push-to-branch.md).
68
+ - Resolve `autoResolveConflicts` from the configured preference (per the gate contract — don't assume a default): `always` resolves conflicts behind the verify-or-rollback gate, `never` escalates to the user. A clean behind-only rebase needs neither.
@@ -1,38 +1,40 @@
1
- # Blocked-tick procedure
2
-
3
- The watcher's conditional path for a PR **blocked pending a human** — a durable block only the user can clear (an escalated rebase or CI budget spent, or an ambiguous review awaiting direction). Entered from [`contract.md`](contract.md) Step 7 (flag), then driven on each subsequent tick by Step 2.5 (resume gate). None of this runs on a normal tick: when `last_seen.blocked` is absent, the watcher skips straight through.
4
-
5
- **Governing rule — one reminder per block, and the watch never stops.** The tick that flags the block reminds the owner once; every later blocked tick is silent. The watch keeps standing — the monitor stays visible, and ticks still run at their normal cadence (historically `1m`) whenever a wake or recovery cron fires — with fingerprint-based auto-resume clearing the block the moment external state moves. No repeat nagging, no cadence swap, no separate `reminded` flag: `blocked` present means the reminder has already been sent.
6
-
7
- ## The fingerprint
8
-
9
- The external state a block is waiting on. Recompute from live state each blocked tick:
10
-
11
- - `head_sha` — from the tick's [`contract.md`](contract.md) Step 1 refresh.
12
- - `latest_review_id` — `max(id)` over submitted reviews per [`../_shared/vcs/github/submitted-reviews.md`](../_shared/vcs/github/submitted-reviews.md) (`0` if none).
13
- - `ci_digest` — the CI rollup digest for `head_sha` per [`../_shared/vcs/github/pr-checks.md`](../_shared/vcs/github/pr-checks.md): the bucket plus each check's name and conclusion, sorted into one stable string.
14
-
15
- Any component moving means the block may have cleared — a new push (`head_sha`, which also clears the per-SHA escalation sets), a new review (`latest_review_id`), or a CI/deploy state change (`ci_digest`, which is how an external staging deploy is caught).
16
-
17
- ## Flag the block (from Step 7)
18
-
19
- When an idle tick is a durable human-block and `last_seen.blocked` is not already set:
20
-
21
- 1. Increment `last_seen.idle_tick_count`.
22
- 2. Write `last_seen.blocked = { reason, since: <now>, fingerprint }` (reuse the `latest_review_id` / `ci_digest` already fetched this tick). Mirror the fingerprint's `ci_digest` into the watch-watermark's `BLOCKED_CIDIGEST` ([`state-schemas.md`](state-schemas.md#watch-watermarkenv)) — a whole-file rewrite of `watch-watermark.env` that leaves the other floors untouched. That non-empty value is what arms the monitor's blocked-resume probe, so a block resumes on any CI move ([`arm-watcher.md`](arm-watcher.md)), not just red — without it the probe stays dormant.
23
- 3. **Remind the owner** — emit the one-line reminder per [`output-templates/blocked-reminder.md`](output-templates/blocked-reminder.md): the pending act plus a reference back to the decision context. This is the block's **only** reminder.
24
- 4. Append a `blocked reason=<reason>` line to `followup.log` per [`output-templates/watcher-log.md`](output-templates/watcher-log.md); emit a `tick` event with `idle: true`, `blocked: true`, and the same other fields as a transient idle. Exit.
25
-
26
- ## Remind or resume (the Step 2.5 gate)
27
-
28
- Every subsequent tick while `last_seen.blocked` is present: recompute the fingerprint and compare to `last_seen.blocked.fingerprint`.
29
-
30
- - **Unchanged** → still blocked. Stay **silent** — the reminder went out when the block was flagged. Increment `last_seen.idle_tick_count`, append a `blocked reason=<reason>` line to `followup.log`, emit a `tick` event with `idle: true`, `blocked: true`. Exit.
31
- - **Changed** → clear `last_seen.blocked`, clear the watch-watermark's `BLOCKED_CIDIGEST` to empty (whole-file rewrite — disarms the monitor's blocked-resume probe now that the watch is unblocked), and **fall through to [`contract.md`](contract.md) Step 3** to re-evaluate against the moved state this same tick: a dispatch hands the PR to the cycle (its exit settles the watch); a transient idle changes nothing; idling back into a block re-flags per Step 7 — a new block, which sends its own single reminder (and re-arms `BLOCKED_CIDIGEST` afresh).
32
-
33
- ## Invariants
34
-
35
- - One reminder per block — sent when flagged, never repeated while the same block holds. A re-flag after a resume is a new block and sends its own single reminder.
36
- - The watch never stops — a blocked PR stays visibly watched at the normal `1m` cadence; only a terminal PR or an explicit teardown ends it.
37
- - The block clears the instant any fingerprint component moves, caught at the next wake or tick.
38
- - The blocked path never posts to the PR.
1
+ # Blocked-tick procedure
2
+
3
+ The watcher's conditional path for a PR **blocked pending a human** — a durable block only the user can clear (an escalated rebase or CI budget spent, or an ambiguous review awaiting direction). Entered from [`contract.md`](contract.md) Step 7 (flag), then driven on each subsequent tick by Step 2.5 (resume gate). None of this runs on a normal tick: when `last_seen.blocked` is absent, the watcher skips straight through.
4
+
5
+ **Governing rule — one reminder per block, and the watch never stops.** The tick that flags the block reminds the owner once; every later blocked tick is silent. The watch keeps standing — the monitor stays visible, and ticks still run at their normal cadence (historically `1m`) whenever a wake or recovery cron fires — with fingerprint-based auto-resume clearing the block the moment external state moves. No repeat nagging, no cadence swap, no separate `reminded` flag: `blocked` present means the reminder has already been sent.
6
+
7
+ ## The fingerprint
8
+
9
+ The external state a block is waiting on. Recompute from live state each blocked tick:
10
+
11
+ - `head_sha` — from the tick's [`contract.md`](contract.md) Step 1 refresh.
12
+ - `latest_review_id` — `max(id)` over submitted reviews per [`../_shared/vcs/github/submitted-reviews.md`](../_shared/vcs/github/submitted-reviews.md) (`0` if none).
13
+ - `ci_digest` — the CI rollup digest for `head_sha` per [`../_shared/vcs/github/pr-checks.md`](../_shared/vcs/github/pr-checks.md): the bucket plus each check's name and conclusion, sorted into one stable string.
14
+
15
+ Any component moving means the block may have cleared — a new push (`head_sha`, which also clears the per-SHA escalation sets), a new review (`latest_review_id`), or a CI/deploy state change (`ci_digest`, which is how an external staging deploy is caught).
16
+
17
+ ## Flag the block (from Step 7)
18
+
19
+ When an idle tick is a durable human-block and `last_seen.blocked` is not already set:
20
+
21
+ 1. Increment `last_seen.idle_tick_count`.
22
+ 2. Write `last_seen.blocked = { reason, since: <now>, fingerprint }` (reuse the `latest_review_id` / `ci_digest` already fetched this tick). Mirror the fingerprint's `ci_digest` into the watch-watermark's `BLOCKED_CIDIGEST` ([`state-schemas.md`](state-schemas.md#watch-watermarkenv)) — a whole-file rewrite of `watch-watermark.env` that leaves the other floors untouched. That non-empty value is what arms the monitor's blocked-resume probe, so a block resumes on any CI move ([`arm-watcher.md`](arm-watcher.md)), not just red — without it the probe stays dormant.
23
+ 3. **Remind the owner** — emit the one-line reminder per [`output-templates/blocked-reminder.md`](output-templates/blocked-reminder.md): the pending act plus a reference back to the decision context. This is the block's **only** reminder.
24
+ 4. Append a `blocked reason=<reason>` line to `followup.log` per [`output-templates/watcher-log.md`](output-templates/watcher-log.md); emit a `tick` event with `idle: true`, `blocked: true`, and the same other fields as a transient idle. Exit.
25
+
26
+ ## Remind or resume (the Step 2.5 gate)
27
+
28
+ Every subsequent tick while `last_seen.blocked` is present: recompute the fingerprint and compare to `last_seen.blocked.fingerprint`.
29
+
30
+ - **Unchanged** → still blocked. Stay **silent** — the reminder went out when the block was flagged. Increment `last_seen.idle_tick_count`, append a `blocked reason=<reason>` line to `followup.log`, emit a `tick` event with `idle: true`, `blocked: true`. Exit.
31
+ `reason: "rebase_budget_exhausted"` is the one exception to the gate: treat it as **Unchanged** whatever the fingerprint says. The budget was spent by the base advancing, so resuming on a fingerprint move would hand the PR straight back into the unbounded rebase loop the cap exists to end. It clears only when the owner raises [`maxCatchUpRebases`](../muggle-preferences/preference-gates/maxCatchUpRebases.md) or the PR goes terminal.
32
+
33
+ - **Changed** → clear `last_seen.blocked`, clear the watch-watermark's `BLOCKED_CIDIGEST` to empty (whole-file rewrite — disarms the monitor's blocked-resume probe now that the watch is unblocked), and **fall through to [`contract.md`](contract.md) Step 3** to re-evaluate against the moved state this same tick: a dispatch hands the PR to the cycle (its exit settles the watch); a transient idle changes nothing; idling back into a block re-flags per Step 7 — a new block, which sends its own single reminder (and re-arms `BLOCKED_CIDIGEST` afresh).
34
+
35
+ ## Invariants
36
+
37
+ - One reminder per block — sent when flagged, never repeated while the same block holds. A re-flag after a resume is a new block and sends its own single reminder.
38
+ - The watch never stops — a blocked PR stays visibly watched at the normal `1m` cadence; only a terminal PR or an explicit teardown ends it.
39
+ - The block clears the instant any fingerprint component moves, caught at the next wake or tick — except `rebase_budget_exhausted`, which only the owner clears.
40
+ - The blocked path never posts to the PR.
@@ -116,7 +116,9 @@ Rebase dedup is keyed on the **pair** `rebase_key = "<head_sha>..<base_tip_sha>"
116
116
 
117
117
  Entries written by an older watcher are bare head SHAs with no `..` — ignore them when reading `conflict_escalated_keys`, which re-arms any slot a head-only key had wedged.
118
118
 
119
- If a rebase is due **and** `conflict_resolve_attempts[rebase_key] < 2` **and** `rebase_key` ∉ `conflict_escalated_keys` → dispatch and exit:
119
+ `conflict_resolve_attempts` bounds one key, never the PR. Because `rebase_key` carries the base tip, every advance of the base mints a fresh key and hands the branch a fresh pair of attempts, so on an active base that budget can never stop anything. The per-PR budget is the one that binds: `last_seen.catch_up_rebases` counts every rebase this PR has been dispatched for, no key resets it, and [`../muggle-preferences/preference-gates/maxCatchUpRebases.md`](../muggle-preferences/preference-gates/maxCatchUpRebases.md) caps it at 20 by default.
120
+
121
+ If a rebase is due **and** `catch_up_rebases < maxCatchUpRebases` **and** `conflict_resolve_attempts[rebase_key] < 2` **and** `rebase_key` ∉ `conflict_escalated_keys` → dispatch and exit:
120
122
 
121
123
  1. Reset `last_seen.idle_tick_count` to 0.
122
124
  2. **Stop this watcher (single-thread):** cancel its cron exactly as in Step 4 — `/muggle-do`'s rebase respawns it when the cycle is done.
@@ -128,7 +130,11 @@ If a rebase is due **and** `conflict_resolve_attempts[rebase_key] < 2` **and** `
128
130
  4. Append a dispatching line to `followup.log`; emit a `tick` event with `rebase_needed: true`, `dispatched_rebase: true`.
129
131
  5. Exit. The dev cycle owns the PR; its respawn restarts the watcher, whose next tick re-checks the branch against its base on the new head — the rebase is its own verify loop, bounded by the per-SHA attempt budget.
130
132
 
131
- Otherwise — `behind_by == 0` and not conflicting (`mergeable == UNKNOWN` is fine here: `behind_by` is exact while GitHub is still computing conflict state, so a stale branch still triggers), or budget spent (`conflict_resolve_attempts[rebase_key] >= 2` or `rebase_key` ∈ `conflict_escalated_keys`) → fall through to CI.
133
+ If a rebase is due **and** `catch_up_rebases >= maxCatchUpRebases` → the PR has spent its catch-up budget. Do **not** dispatch, and do **not** fall through quietly: set `last_seen.rebase_budget_exhausted` to `true`, block the watch with reason `rebase_budget_exhausted` (Step 7), and report the count, the cap and the preference key on the PR. Twenty rebases without converging is a branch its owner has to decide about — rebase it onto a different base, split it, or close it — and a watcher that merely stops leaves them to discover it days later.
134
+
135
+ That block is the one reason a fingerprint move does not clear (Step 2.5): base movement is precisely what spent the budget, so resuming on it would restore the loop the cap exists to end. It clears when the owner raises `maxCatchUpRebases` or the PR goes terminal.
136
+
137
+ Otherwise — `behind_by == 0` and not conflicting (`mergeable == UNKNOWN` is fine here: `behind_by` is exact while GitHub is still computing conflict state, so a stale branch still triggers), or the per-key budget is spent (`conflict_resolve_attempts[rebase_key] >= 2` or `rebase_key` ∈ `conflict_escalated_keys`) → fall through to CI.
132
138
 
133
139
  ### Step 6 — No actionable feedback, branch current → poll CI for the head SHA
134
140
 
@@ -82,6 +82,8 @@ Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
82
82
  "ci_escalated_shas": ["<sha>", ...],
83
83
  "conflict_resolve_attempts": { "<head-sha>..<base-tip-sha>": <int> },
84
84
  "conflict_escalated_keys": ["<head-sha>..<base-tip-sha>", ...],
85
+ "catch_up_rebases": <int>,
86
+ "rebase_budget_exhausted": <bool>,
85
87
  "blocked": {
86
88
  "reason": "conflict_escalated" | "ci_escalated" | "reviews_escalated",
87
89
  "since": "<ISO-8601>",
@@ -105,12 +107,15 @@ Keyed by `"<owner>/<repo>#<n>"`. One key per PR in the slot.
105
107
  - `ci_escalated_shas`: head SHAs whose CI the fix-ci stage gave up on (attempts exhausted or only out-of-scope checks). The watcher excludes these from CI dispatch so a hopeless SHA is never re-fixed.
106
108
  - `conflict_resolve_attempts`: count of rebase cycles `/muggle-do` has run (behind-only or conflicting — both rebase onto the base). The watcher stops dispatching once a key's count reaches 2. Keyed by `rebase_key` — `"<head_sha>..<base_tip_sha>"`, the head paired with the base branch tip it was measured against.
107
109
  - `conflict_escalated_keys`: `rebase_key`s whose rebase `/muggle-do` gave up on (attempts exhausted, or a conflict under `autoResolveConflicts=never`). The watcher excludes these from rebase dispatch so a hopeless pairing is never re-attempted.
110
+ - `catch_up_rebases`: how many rebases this PR has been dispatched for across its whole life. A plain counter, deliberately unkeyed: the two fields above key on `rebase_key`, which changes on every base advance and so hands an active base an unlimited supply of fresh budgets. Nothing resets this one, which makes it the only cap that binds. Capped by [`../muggle-preferences/preference-gates/maxCatchUpRebases.md`](../muggle-preferences/preference-gates/maxCatchUpRebases.md) — 20 by default, `never` for unbounded. Incremented by the rebase cycle on every push, whatever the outcome.
111
+ - `rebase_budget_exhausted`: set once `catch_up_rebases` reaches the cap. The watcher then stops dispatching rebases for the PR entirely and blocks with reason `rebase_budget_exhausted` rather than idling, so the owner is told the branch is waiting on them. Cleared by raising the preference.
108
112
 
109
113
  Both are keyed on the pair, not the head alone, because whether a branch conflicts depends on both sides. Under a head-only key, a base that moves produces a genuinely new conflict against an unchanged head — and the stale entry suppresses it permanently, because nothing can change the head while the branch sits blocked. Pairing re-arms the budget whenever either side moves. Legacy entries written before this change are bare SHAs with no `..`; readers ignore them, which un-wedges any slot they had blocked.
110
114
 
111
115
  Unlike these, `ci_fix_attempts` / `ci_escalated_shas` stay keyed on the head SHA alone — a CI result is a function of the head only, so base movement must not re-arm them.
112
116
  - `blocked`: present only while the watcher is **awaiting the owner** on a PR that cannot progress without a human ([`contract.md`](contract.md) Step 7). Absent ⇒ the watcher is in its normal dispatch flow. When present, the watcher **keeps the normal `1m` cadence** and each tick is a reminder-or-resume check ([`contract.md`](contract.md) Step 2.5): it re-emits a one-line reminder to the owner, recomputes the `fingerprint`, and clears the block the moment any component moves. Its value is the reason-specific reminder plus fingerprint auto-resume.
113
- - `reason`: which durable block is being awaited — `conflict_escalated` (`rebase_key` ∈ `conflict_escalated_keys`), `ci_escalated` (`head_sha` ∈ `ci_escalated_shas`), or `reviews_escalated` (a review sits in `escalated_review_ids` awaiting the user, actionable set empty). Selects the reminder wording; the resume decision is fingerprint-driven, not reason-driven.
117
+ - `reason`: which durable block is being awaited — `conflict_escalated` (`rebase_key` ∈ `conflict_escalated_keys`), `ci_escalated` (`head_sha` ∈ `ci_escalated_shas`), `reviews_escalated` (a review sits in `escalated_review_ids` awaiting the user, actionable set empty), or `rebase_budget_exhausted` (`catch_up_rebases` reached its cap). Selects the reminder wording; the resume decision is fingerprint-driven for every reason but the last.
118
+ - `rebase_budget_exhausted` is the one reason a fingerprint move does not clear. Base movement is what spent the budget, so resuming on it would restore the unbounded rebase loop the cap exists to end; it clears when the owner raises `maxCatchUpRebases` or the PR goes terminal.
114
119
  - `since`: when the block was first flagged — lets the reminder state how long the owner has been the blocker.
115
120
  - `fingerprint`: the external state the block is waiting on. `head_sha` moves on a new push (which also clears the per-SHA escalation sets, keyed by SHA); `latest_review_id` is `max(id)` over submitted reviews and moves when a reviewer submits anything new; `ci_digest` is a stable digest of the head SHA's CI rollup (bucket + each check's name/conclusion, sorted) and moves when a check flips, a rerun lands, or an external check such as a staging deploy posts. Any change clears the block and resumes evaluation.
116
121
 
@@ -1,33 +1,33 @@
1
- ---
2
- name: muggle-preferences
3
- model: haiku
4
- description: >-
5
- View, set, or reset Muggle AI preferences that control testing behavior.
6
- Use when user asks to see preferences, change a setting, configure Muggle Test
7
- defaults, or manage muggle config. Triggers on: 'muggle preferences',
8
- 'show muggle settings', 'change muggle preference', 'set autoLogin to
9
- always', 'muggle config', 'reset muggle preferences', 'show my muggle
10
- settings', 'configure muggle', 'muggle setup'.
11
- ---
12
-
13
- # Muggle Test Preferences
14
-
15
- > Telemetry first step: see [`_shared/telemetry-emit.md`](../_shared/telemetry-emit.md). Use `skillName: "muggle-preferences"`.
16
-
17
- Pick the operation, then read its op file for the procedure.
18
-
19
- | Intent | Op file |
20
- |---|---|
21
- | `/muggle-preferences <key>` or "change my `<key>` preference" | `ops/change-one.md` |
22
- | User names key + value (e.g. "set autoLogin to always") | `ops/set.md` |
23
- | "show preferences" / no args | `ops/list.md` |
24
- | "configure muggle" / "muggle setup" / change without naming a key | `ops/configure.md` |
25
- | "reset preferences" | `ops/reset.md` |
26
- | Session context says first-run setup has not been run | `ops/onboard.md` |
27
-
28
- ## Shared context (all ops)
29
-
30
- - **Current values**: session-context line `Muggle Test Preferences key=value …`, already resolved against the shipped defaults in `${CLAUDE_PLUGIN_ROOT}/config/preference-defaults.json`.
31
- - **Per-key files**: `preference-gates/<key>.md`. Key list = `ls preference-gates/*.md` minus `README.md`.
32
- - **Allowed values**: `always`/`never`/`ask`, except `defaultExecutionMode` (`local`/`remote`/`ask`), `autoE2ETest` (`always`/`ask`), and `watcherLifetime` (`1d`/`7d`/`never`).
33
- - **Scope**: preferences are user-level. Every write lands in `~/.muggle-ai/preferences.json` and applies to every repo. If the user asks for a per-project setting, say it isn't supported and confirm before setting it everywhere.
1
+ ---
2
+ name: muggle-preferences
3
+ model: haiku
4
+ description: >-
5
+ View, set, or reset Muggle AI preferences that control testing behavior.
6
+ Use when user asks to see preferences, change a setting, configure Muggle Test
7
+ defaults, or manage muggle config. Triggers on: 'muggle preferences',
8
+ 'show muggle settings', 'change muggle preference', 'set autoLogin to
9
+ always', 'muggle config', 'reset muggle preferences', 'show my muggle
10
+ settings', 'configure muggle', 'muggle setup'.
11
+ ---
12
+
13
+ # Muggle Test Preferences
14
+
15
+ > Telemetry first step: see [`_shared/telemetry-emit.md`](../_shared/telemetry-emit.md). Use `skillName: "muggle-preferences"`.
16
+
17
+ Pick the operation, then read its op file for the procedure.
18
+
19
+ | Intent | Op file |
20
+ |---|---|
21
+ | `/muggle-preferences <key>` or "change my `<key>` preference" | `ops/change-one.md` |
22
+ | User names key + value (e.g. "set autoLogin to always") | `ops/set.md` |
23
+ | "show preferences" / no args | `ops/list.md` |
24
+ | "configure muggle" / "muggle setup" / change without naming a key | `ops/configure.md` |
25
+ | "reset preferences" | `ops/reset.md` |
26
+ | Session context says first-run setup has not been run | `ops/onboard.md` |
27
+
28
+ ## Shared context (all ops)
29
+
30
+ - **Current values**: session-context line `Muggle Test Preferences key=value …`, already resolved against the shipped defaults in `${CLAUDE_PLUGIN_ROOT}/config/preference-defaults.json`.
31
+ - **Per-key files**: `preference-gates/<key>.md`. Key list = `ls preference-gates/*.md` minus `README.md`.
32
+ - **Allowed values**: `always`/`never`/`ask`, except `defaultExecutionMode` (`local`/`remote`/`ask`), `autoE2ETest` (`always`/`ask`), `watcherLifetime` (`1d`/`7d`/`never`), and `maxCatchUpRebases` (a positive integer, or `never`).
33
+ - **Scope**: preferences are user-level. Every write lands in `~/.muggle-ai/preferences.json` and applies to every repo. If the user asks for a per-project setting, say it isn't supported and confirm before setting it everywhere.