@muggleai/works 5.9.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +8 -0
  2. package/dist/{chunk-5PYK7DWB.js → chunk-7NC2DJTK.js} +10 -4
  3. package/dist/{chunk-LVUJ4E62.js → chunk-OCLMXAVJ.js} +245 -167
  4. package/dist/cli.js +2 -2
  5. package/dist/index.js +2 -2
  6. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  7. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  8. package/dist/plugin/README.md +19 -13
  9. package/dist/plugin/agents/test-prepare-runner.md +1 -0
  10. package/dist/plugin/scripts/ensure-electron-app.sh +62 -47
  11. package/dist/plugin/scripts/guardrails.mjs +47 -46
  12. package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
  13. package/dist/plugin/skills/muggle/SKILL.md +1 -1
  14. package/dist/plugin/skills/muggle-preferences/SKILL.md +1 -1
  15. package/dist/plugin/skills/muggle-preferences/evals/evals.json +45 -0
  16. package/dist/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
  17. package/dist/plugin/skills/muggle-preferences/ops/configure.md +4 -5
  18. package/dist/plugin/skills/muggle-preferences/ops/list.md +1 -1
  19. package/dist/plugin/skills/muggle-preferences/ops/set.md +3 -4
  20. package/dist/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
  21. package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
  22. package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
  23. package/dist/plugin/skills/muggle-test/SKILL.md +1 -1
  24. package/dist/plugin/skills/muggle-test/execute-local.md +2 -0
  25. package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +3 -1
  26. package/dist/plugin/skills/muggle-test-import/SKILL.md +1 -1
  27. package/dist/plugin/skills/muggle-test-prepare/SKILL.md +10 -4
  28. package/dist/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
  29. package/dist/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +100 -0
  30. package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +9 -4
  31. package/dist/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +5 -4
  32. package/dist/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
  33. package/dist/release-manifest.json +4 -4
  34. package/dist/src-AWUORQZZ.js +1 -0
  35. package/package.json +6 -6
  36. package/plugin/.claude-plugin/plugin.json +1 -1
  37. package/plugin/.cursor-plugin/plugin.json +1 -1
  38. package/plugin/README.md +19 -13
  39. package/plugin/agents/test-prepare-runner.md +1 -0
  40. package/plugin/scripts/ensure-electron-app.sh +62 -47
  41. package/plugin/scripts/guardrails.mjs +47 -46
  42. package/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
  43. package/plugin/skills/muggle/SKILL.md +1 -1
  44. package/plugin/skills/muggle-preferences/SKILL.md +1 -1
  45. package/plugin/skills/muggle-preferences/evals/evals.json +45 -0
  46. package/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
  47. package/plugin/skills/muggle-preferences/ops/configure.md +4 -5
  48. package/plugin/skills/muggle-preferences/ops/list.md +1 -1
  49. package/plugin/skills/muggle-preferences/ops/set.md +3 -4
  50. package/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
  51. package/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
  52. package/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
  53. package/plugin/skills/muggle-test/SKILL.md +1 -1
  54. package/plugin/skills/muggle-test/execute-local.md +2 -0
  55. package/plugin/skills/muggle-test-feature-local/SKILL.md +3 -1
  56. package/plugin/skills/muggle-test-import/SKILL.md +1 -1
  57. package/plugin/skills/muggle-test-prepare/SKILL.md +10 -4
  58. package/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
  59. package/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +100 -0
  60. package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +9 -4
  61. package/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +5 -4
  62. package/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
  63. package/dist/src-FDK2KQSZ.js +0 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-5PYK7DWB.js';
3
- import './chunk-LVUJ4E62.js';
2
+ import { runCli } from './chunk-7NC2DJTK.js';
3
+ import './chunk-OCLMXAVJ.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-5PYK7DWB.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-LVUJ4E62.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-7NC2DJTK.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-OCLMXAVJ.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.9.0",
4
+ "version": "5.10.0",
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.9.0",
5
+ "version": "5.10.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -21,19 +21,25 @@ This updates the CLI, configures Cursor MCP (`~/.cursor/mcp.json`), and syncs `m
21
21
 
22
22
  Type `muggle` to discover the full command family.
23
23
 
24
- | Skill | What it does |
25
- |:---|:---|
26
- | `/muggle:muggle` | Router and menu for all Muggle Test commands. |
27
- | `/muggle:muggle-do` | Autonomous dev pipeline: requirements, code, unit tests, E2E acceptance tests, PR. |
28
- | `/muggle:muggle-test` | Change-driven E2E acceptance router: detects code changes, maps to use cases, runs test generation locally or remotely, publishes to dashboard, opens in browser, posts E2E acceptance results to PR. |
29
- | `/muggle:muggle-test-feature-local` | Test a feature on localhost with AI-driven browser automation. Offers publish to cloud after each run. |
30
- | `/muggle:muggle-test-import` | Import existing tests into Muggle Test — from Playwright/Cypress specs, PRDs, Gherkin feature files, test plan docs, or any test artifact. |
31
- | `/muggle:muggle-test-regenerate-missing` | Bulk-regenerate test scripts for every test case in a project that doesn't currently have an active script. Scans DRAFT + GENERATION_PENDING, confirms the list with the user, and dispatches remote generation workflows for each. |
32
- | `/muggle:muggle-status` | Health check for Electron browser test runner, MCP server, and authentication. |
33
- | `/muggle:muggle-repair` | Diagnose and fix broken installation automatically. |
34
- | `/muggle:muggle-upgrade` | Update Electron browser test runner and MCP server to latest version. |
35
-
36
- Each skill above also ships a short alias to save typing — `m` (router), `mtest`, `mdo`, `mpr`, `mprefs`, `mstatus`, `mrepair`, `mupgrade`, `mfeedback`, `mimport`, `mtestlocal`, `mtestprep`, `mregen`. Type `/m` (Claude Code) or `m` (Cursor) to open the menu, or jump straight to one (e.g. `/mtest`).
24
+ | Skill | Shorthand | What it does |
25
+ |:---|:---|:---|
26
+ | `/muggle:muggle` | `/m` | Router and menu for all Muggle Test commands. |
27
+ | `/muggle:muggle-do` | `/mdo` | Autonomous dev pipeline: requirements, code, unit tests, E2E acceptance tests, PR. |
28
+ | `/muggle:muggle-test` | `/mtest` | Change-driven E2E acceptance router: detects code changes, maps to use cases, runs test generation locally or remotely, publishes to dashboard, opens in browser, posts E2E acceptance results to PR. |
29
+ | `/muggle:muggle-test-feature-local` | `/mtestlocal` | Test a feature on localhost with AI-driven browser automation. Offers publish to cloud after each run. |
30
+ | `/muggle:muggle-test-prepare` | `/mtestprep` | Verify the dev servers and sibling services a test run needs, and start whatever is missing. |
31
+ | `/muggle:muggle-test-import` | `/mimport` | Import existing tests into Muggle Test — from Playwright/Cypress specs, PRDs, Gherkin feature files, test plan docs, or any test artifact. |
32
+ | `/muggle:muggle-test-regenerate-missing` | `/mregen` | Bulk-regenerate test scripts for every test case in a project that doesn't currently have an active script. Scans DRAFT + GENERATION_PENDING, confirms the list with the user, and dispatches remote generation workflows for each. |
33
+ | `/muggle:muggle-browser-task` | `/mbt` | Perform a real action on a website from plain English — log in and submit the form, create the ticket, refund the charge. |
34
+ | `/muggle:muggle-pr-visual-walkthrough` | `/mpr` | Post per-test-case dashboard links, step-by-step screenshots, and a pass/fail summary to a PR. |
35
+ | `/muggle:muggle-pr-followup` | `/mprfollowup` | Watch one PR's review thread and dispatch the work to address incoming feedback. |
36
+ | `/muggle:muggle-feedback` | `/mfeedback` | Flag a generated action script, or one step in it, as wrong so Muggle can analyze and regenerate affected scripts. |
37
+ | `/muggle:muggle-preferences` | `/mprefs` | View, set, or reset the preferences that gate Muggle Test behavior. |
38
+ | `/muggle:muggle-status` | `/mstatus` | Health check for Electron browser test runner, MCP server, and authentication. |
39
+ | `/muggle:muggle-repair` | `/mrepair` | Diagnose and fix broken installation automatically. |
40
+ | `/muggle:muggle-upgrade` | `/mupgrade` | Update Electron browser test runner and MCP server to latest version. |
41
+
42
+ Every skill ships the short alias in the table above. Type `/m` (Claude Code) or `m` (Cursor) to open the menu, or jump straight to one (e.g. `/mtest`). Claude Code namespaces plugin commands, so the aliases resolve as `/muggle:mtest`; mirror them into `~/.claude/commands/` to type `/mtest` bare.
37
43
 
38
44
  ## MCP Tools
39
45
 
@@ -16,6 +16,7 @@ The dispatch prompt carries the resolved prepare plan:
16
16
  - `testingScope` and `excludedServices[]` (with reasons).
17
17
  - The recorded dev-server URL (from the `autoSelectLocalHost` resolution) — never invent or default a host/port; a framework default like `:3000` is not a fallback.
18
18
  - Resolved gate values the stages read (`autoRebase` outcome already applied or explicitly skipped upstream).
19
+ - The E2E run instructions — startup order, manual steps, local gotchas — already captured from the user. Honour the recorded startup order when starting services, and write the instructions out per the readiness-report stage. Absent means the skill had nothing to record; never invent them.
19
20
 
20
21
  ## Stages
21
22
 
@@ -110,6 +110,10 @@ version_check() {
110
110
  version_check || true
111
111
 
112
112
  # --- Preferences injection ---
113
+ # Preferences are user-level: defaults overlaid by the global file only. A legacy
114
+ # <cwd>/.muggle-ai/preferences.json is left on disk but no longer read, so the
115
+ # keys it can no longer apply are named once — the stamp file suppresses the
116
+ # repeat until that key set changes.
113
117
  prefs_global_file="${HOME}/.muggle-ai/preferences.json"
114
118
  prefs_line=""
115
119
  prefs_file_note=""
@@ -119,8 +123,13 @@ if [ -f "$prefs_global_file" ]; then
119
123
  # Uses node for reliable JSON parsing (already required for muggle).
120
124
  prefs_line=$(node -e "
121
125
  const fs = require('fs');
126
+ const os = require('os');
127
+ const path = require('path');
122
128
  try {
123
- const g = JSON.parse(fs.readFileSync('${prefs_global_file}', 'utf-8')).preferences || {};
129
+ // Resolved through node, not the shell's \$HOME: under Git Bash the shell
130
+ // reports a POSIX path that Windows node cannot open.
131
+ const globalFile = path.join(os.homedir(), '.muggle-ai', 'preferences.json');
132
+ const g = JSON.parse(fs.readFileSync(globalFile, 'utf-8')).preferences || {};
124
133
  const defaults = {
125
134
  autoLogin:'ask', autoSelectProject:'ask', autoSelectLocalHost:'ask',
126
135
  showElectronBrowser:'ask', openTestResultsAfterRun:'ask',
@@ -131,15 +140,30 @@ if [ -f "$prefs_global_file" ]; then
131
140
  autoUseWorktree:'ask', autoRebase:'ask', autoCleanup:'ask',
132
141
  autoE2ETest:'always', autoRouteBuildToMuggleDo:'ask'
133
142
  };
143
+ const resolved = { ...defaults, ...g };
144
+ const line = Object.entries(resolved).map(([k,v]) => k+'='+v).join(' ');
145
+ const blocks = ['Muggle Test Preferences (~/.muggle-ai/preferences.json):\\\\n' + line];
146
+
134
147
  const cwd = process.env.CLAUDE_CWD || process.env.CURSOR_CWD || process.cwd();
135
- const pPath = require('path').join(cwd, '.muggle-ai', 'preferences.json');
148
+ const pPath = path.join(cwd, '.muggle-ai', 'preferences.json');
136
149
  let p = {};
137
150
  try { p = JSON.parse(fs.readFileSync(pPath, 'utf-8')).preferences || {}; } catch {}
138
- const merged = { ...defaults, ...g, ...p };
139
- const hasProject = Object.keys(p).length > 0;
140
- const note = hasProject ? ', project overrides active' : '';
141
- const line = Object.entries(merged).map(([k,v]) => k+'='+v).join(' ');
142
- console.log('Muggle Test Preferences (~/.muggle-ai/preferences.json' + note + '):\\\\n' + line);
151
+ const inertKeys = Object.keys(p).filter((k) => p[k] !== resolved[k]).sort();
152
+ if (inertKeys.length > 0) {
153
+ const stampDir = path.join(os.homedir(), '.cache', 'muggle');
154
+ const stampFile = path.join(stampDir, 'project-prefs-inert');
155
+ const stamp = cwd + '|' + inertKeys.join(',');
156
+ let lastStamp = '';
157
+ try { lastStamp = fs.readFileSync(stampFile, 'utf-8'); } catch {}
158
+ if (lastStamp !== stamp) {
159
+ try { fs.mkdirSync(stampDir, { recursive: true }); fs.writeFileSync(stampFile, stamp); } catch {}
160
+ blocks.push(
161
+ 'Muggle Test: per-project preferences were removed — ' + pPath + ' is no longer read, so these keys no longer take effect: ' + inertKeys.join(', ') + '.\\\\n' +
162
+ 'Tell the user to re-apply any they want everywhere with \`/muggle-preferences\`; the file is safe to delete.'
163
+ );
164
+ }
165
+ }
166
+ console.log(blocks.join('\\\\n\\\\n'));
143
167
  } catch { console.log(''); }
144
168
  " 2>/dev/null || true)
145
169
  if [ -n "$prefs_line" ]; then
@@ -149,53 +173,44 @@ else
149
173
  prefs_file_note="\\n\\nMuggle Test Preferences: not configured. Run \\\`muggle setup\\\` or tell the agent to set preferences."
150
174
  fi
151
175
 
152
- # --- Last-project cache injection ---
153
- # Per-repo "last used Muggle Test project" cache. Lives at <cwd>/.muggle-ai/last-project.json
154
- # and is honored by skills when autoSelectProject = always.
155
- last_project_line=""
156
- last_project_note=""
157
- last_project_line=$(node -e "
158
- const fs = require('fs');
159
- const path = require('path');
160
- try {
161
- const cwd = process.env.CLAUDE_CWD || process.env.CURSOR_CWD || process.cwd();
162
- const lpPath = path.join(cwd, '.muggle-ai', 'last-project.json');
163
- if (!fs.existsSync(lpPath)) { console.log(''); return; }
164
- const raw = JSON.parse(fs.readFileSync(lpPath, 'utf-8'));
165
- const lp = raw && raw.lastProject;
166
- if (!lp || !lp.projectId) { console.log(''); return; }
167
- const safeName = String(lp.projectName || '').replace(/\"/g, '\\\\\"');
168
- console.log('Muggle Test Last Project: id=' + lp.projectId + ' url=' + lp.projectUrl + ' name=\"' + safeName + '\"');
169
- } catch { console.log(''); }
170
- " 2>/dev/null || true)
171
- if [ -n "$last_project_line" ]; then
172
- last_project_note="\\n\\n${last_project_line}"
173
- fi
174
-
175
- # --- Last-host cache injection ---
176
- # Per-repo cache of the local dev server URL the user picked on the previous
177
- # run. Lives at <cwd>/.muggle-ai/last-host.json. Skills silently reuse it
178
- # when the user has set the autoSelectLocalHost preference to "always".
179
- last_host_line=""
180
- last_host_note=""
181
- last_host_line=$(node -e "
176
+ # --- Last-used cache injection ---
177
+ # The "last used Muggle Test project" and "last used local dev server URL"
178
+ # caches live in ~/.muggle-ai/, keyed by working directory, and are honored by
179
+ # skills when autoSelectProject / autoSelectLocalHost = always. A cache written
180
+ # before the move to the home directory still sits in <cwd>/.muggle-ai/ and is
181
+ # read as a fallback, so those sessions keep their context lines.
182
+ last_cache_notes=""
183
+ last_cache_notes=$(node -e "
182
184
  const fs = require('fs');
185
+ const os = require('os');
183
186
  const path = require('path');
184
187
  try {
185
188
  const cwd = process.env.CLAUDE_CWD || process.env.CURSOR_CWD || process.cwd();
186
- const lhPath = path.join(cwd, '.muggle-ai', 'last-host.json');
187
- if (!fs.existsSync(lhPath)) { console.log(''); return; }
188
- const raw = JSON.parse(fs.readFileSync(lhPath, 'utf-8'));
189
- const lh = raw && raw.lastHost;
190
- if (!lh || !lh.host) { console.log(''); return; }
191
- console.log('Muggle Test Last Host: ' + lh.host);
189
+ const parseFile = (filePath) => {
190
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
191
+ };
192
+ const readEntry = (fileName, legacyEntryKey) => {
193
+ const home = parseFile(path.join(os.homedir(), '.muggle-ai', fileName));
194
+ const homeEntry = home && home.entries && home.entries[path.resolve(cwd)];
195
+ if (homeEntry) { return homeEntry; }
196
+ const legacy = parseFile(path.join(cwd, '.muggle-ai', fileName));
197
+ return (legacy && legacy[legacyEntryKey]) || null;
198
+ };
199
+ const lines = [];
200
+ const lastProject = readEntry('last-project.json', 'lastProject');
201
+ if (lastProject && lastProject.projectId) {
202
+ const safeName = String(lastProject.projectName || '').replace(/\"/g, '\\\\\"');
203
+ lines.push('Muggle Test Last Project: id=' + lastProject.projectId + ' url=' + lastProject.projectUrl + ' name=\"' + safeName + '\"');
204
+ }
205
+ const lastHost = readEntry('last-host.json', 'lastHost');
206
+ if (lastHost && lastHost.host) {
207
+ lines.push('Muggle Test Last Host: ' + lastHost.host);
208
+ }
209
+ console.log(lines.map((line) => '\\\\n\\\\n' + line).join(''));
192
210
  } catch { console.log(''); }
193
211
  " 2>/dev/null || true)
194
- if [ -n "$last_host_line" ]; then
195
- last_host_note="\\n\\n${last_host_line}"
196
- fi
197
212
 
198
- context="<EXTREMELY_IMPORTANT>\nYou have access to Muggle AI — a real-browser E2E acceptance testing tool.\n\nWhenever the user asks you to test, validate, verify, or check if their web app works — use the muggle MCP tools. This includes:\n- Testing user flows (signup, login, checkout, forms, dashboards)\n- Verifying UI changes didn't break anything\n- Running regression tests after code changes\n- Validating frontend behavior on localhost or a dev server\n- Checking if a feature works before merging a PR\n\nMuggle Test launches a real Electron browser that clicks buttons, fills forms, navigates pages, and captures screenshots. It generates replayable test scripts that persist across sessions.\n\nDo NOT write test code (Playwright, Cypress, Selenium) or try to test UI manually when muggle tools are available. Use the muggle skill or muggle MCP tools instead — they are faster, capture visual evidence, and produce reusable test scripts.\n\nTrigger phrases: 'test my app', 'check if it works', 'run E2E acceptance tests', 'validate the UI', 'verify the flow', 'regression test', 'make sure it still works', 'test before merging'.\n</EXTREMELY_IMPORTANT>${upgrade_notice}${prefs_file_note}${last_project_note}${last_host_note}"
213
+ context="<EXTREMELY_IMPORTANT>\nYou have access to Muggle AI — a real-browser E2E acceptance testing tool.\n\nWhenever the user asks you to test, validate, verify, or check if their web app works — use the muggle MCP tools. This includes:\n- Testing user flows (signup, login, checkout, forms, dashboards)\n- Verifying UI changes didn't break anything\n- Running regression tests after code changes\n- Validating frontend behavior on localhost or a dev server\n- Checking if a feature works before merging a PR\n\nMuggle Test launches a real Electron browser that clicks buttons, fills forms, navigates pages, and captures screenshots. It generates replayable test scripts that persist across sessions.\n\nDo NOT write test code (Playwright, Cypress, Selenium) or try to test UI manually when muggle tools are available. Use the muggle skill or muggle MCP tools instead — they are faster, capture visual evidence, and produce reusable test scripts.\n\nTrigger phrases: 'test my app', 'check if it works', 'run E2E acceptance tests', 'validate the UI', 'verify the flow', 'regression test', 'make sure it still works', 'test before merging'.\n</EXTREMELY_IMPORTANT>${upgrade_notice}${prefs_file_note}${last_cache_notes}"
199
214
 
200
215
  escaped_context=$(escape_for_json "$context")
201
216
 
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs';
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
2
2
  import { isAbsolute, resolve, join } from 'path';
3
3
  import { homedir } from 'os';
4
4
 
@@ -43,6 +43,7 @@ ${input2.tool_response?.output ?? ""}`;
43
43
  // src/guardrails/constants.ts
44
44
  var GH_PR_MERGED_LINE = /\b(?:Merged|Squashed and merged|Rebased and merged) pull request [\w./-]*#(\d+)/;
45
45
  var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
46
+ var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
46
47
  var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
47
48
  var MAX_PR_TERMINAL_BLOCKS = 3;
48
49
  var MAX_WATCH_BLOCKS = 3;
@@ -71,6 +72,23 @@ function detectPrTerminal(input2) {
71
72
  }
72
73
  return null;
73
74
  }
75
+ function detectPrReopened(input2) {
76
+ if (input2.tool_name !== "Bash") return null;
77
+ const response = input2.tool_response;
78
+ const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
79
+ const reopenedMatch = haystack.match(GH_PR_REOPENED_LINE);
80
+ return reopenedMatch ? Number(reopenedMatch[1]) : null;
81
+ }
82
+ function applyPrReopened(state, prNumber) {
83
+ const pending = state.terminalPending ?? [];
84
+ const handled = state.terminalHandled ?? [];
85
+ if (!pending.includes(prNumber) && !handled.includes(prNumber)) return state;
86
+ return {
87
+ ...state,
88
+ terminalPending: pending.filter((number) => number !== prNumber),
89
+ terminalHandled: handled.filter((number) => number !== prNumber)
90
+ };
91
+ }
74
92
  function applyPrTerminalDetected(state, prNumber) {
75
93
  const pending = state.terminalPending ?? [];
76
94
  const handled = state.terminalHandled ?? [];
@@ -144,7 +162,6 @@ function e2eGateDecision(state, maxBlocks = MAX_E2E_BLOCKS) {
144
162
  if (blockCount >= maxBlocks) return { action: "release" /* Release */, blockCount };
145
163
  return { action: "block" /* Block */, blockCount: blockCount + 1 };
146
164
  }
147
- var HEARTBEAT_FRESH_MS = 15 * 60 * 1e3;
148
165
  var WATCH_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_WATCH_SKIP\b/;
149
166
  function isWatchSkipMarker(cmd) {
150
167
  return WATCH_SKIP_MARKER.test(cmd);
@@ -153,38 +170,13 @@ function applyWatchSkip(state, skipped) {
153
170
  if (!skipped || state.watchSkipped === true) return state;
154
171
  return { ...state, watchSkipped: true };
155
172
  }
156
- function slotHasArmedWatcher(slotDir) {
157
- if (existsSync(join(slotDir, "result.md"))) return true;
158
- const pidFile = join(slotDir, "watch.pid");
159
- if (existsSync(pidFile)) {
160
- const pid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
161
- if (Number.isInteger(pid) && pid > 0) {
162
- try {
163
- process.kill(pid, 0);
164
- return true;
165
- } catch (err) {
166
- if (err.code === "EPERM") return true;
167
- }
168
- }
169
- }
170
- const beat = join(slotDir, "watch-heartbeat");
171
- if (existsSync(beat)) {
172
- try {
173
- if (Date.now() - statSync(beat).mtimeMs < HEARTBEAT_FRESH_MS) return true;
174
- } catch {
175
- return false;
176
- }
177
- }
178
- return false;
179
- }
180
- function findUnarmedHandledPrs(handledUrls, sessionsDirOverride) {
173
+ function findUntrackedHandledPrs(handledUrls, sessionsDirOverride) {
181
174
  if (handledUrls.length === 0) return [];
182
175
  const sessionsDir = join(homedir(), ".muggle-ai", "muggle-do", "sessions");
183
176
  if (!existsSync(sessionsDir)) return [...handledUrls];
184
- const watchedUrls = /* @__PURE__ */ new Set();
177
+ const trackedUrls = /* @__PURE__ */ new Set();
185
178
  for (const slug of readdirSync(sessionsDir)) {
186
- const slotDir = join(sessionsDir, slug);
187
- const prsFile = join(slotDir, "prs.json");
179
+ const prsFile = join(sessionsDir, slug, "prs.json");
188
180
  if (!existsSync(prsFile)) continue;
189
181
  let slotUrl;
190
182
  try {
@@ -194,21 +186,23 @@ function findUnarmedHandledPrs(handledUrls, sessionsDirOverride) {
194
186
  } catch {
195
187
  continue;
196
188
  }
197
- if (slotUrl && handledUrls.includes(slotUrl) && slotHasArmedWatcher(slotDir)) {
198
- watchedUrls.add(slotUrl);
199
- }
189
+ if (slotUrl) trackedUrls.add(slotUrl);
200
190
  }
201
- return handledUrls.filter((url) => !watchedUrls.has(url));
191
+ return handledUrls.filter((url) => !trackedUrls.has(url));
202
192
  }
203
- function watchGateDecision(state, owedUrls, maxBlocks = MAX_WATCH_BLOCKS) {
193
+ function watchGateDecision(state, untrackedPrUrls, maxBlocks = MAX_WATCH_BLOCKS) {
204
194
  const blockCount = state.watchBlockCount ?? 0;
205
- if (state.watchSkipped === true || owedUrls.length === 0) {
206
- return { action: "none" /* None */, blockCount, owed: owedUrls };
195
+ if (state.watchSkipped === true || untrackedPrUrls.length === 0) {
196
+ return { action: "none" /* None */, blockCount, untracked: untrackedPrUrls };
207
197
  }
208
198
  if (blockCount >= maxBlocks) {
209
- return { action: "release" /* Release */, blockCount, owed: owedUrls };
199
+ return { action: "release" /* Release */, blockCount, untracked: untrackedPrUrls };
210
200
  }
211
- return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedUrls };
201
+ return {
202
+ action: "block" /* Block */,
203
+ blockCount: blockCount + 1,
204
+ untracked: untrackedPrUrls
205
+ };
212
206
  }
213
207
 
214
208
  // src/guardrails/detectBuildIntent.ts
@@ -323,6 +317,13 @@ Per the autoWatchPR preference, a muggle-pr-followup watcher should handle its i
323
317
  return envelope("PostToolUse", ctx, host);
324
318
  }
325
319
  function prTerminal() {
320
+ const reopenedPrNumber = detectPrReopened(input);
321
+ if (reopenedPrNumber !== null) {
322
+ const state2 = readState(sessionId);
323
+ const next2 = applyPrReopened(state2, reopenedPrNumber);
324
+ if (next2 !== state2) writeState(next2);
325
+ return "{}";
326
+ }
326
327
  const terminalEvent = detectPrTerminal(input);
327
328
  if (!terminalEvent) return "{}";
328
329
  const state = readState(sessionId);
@@ -372,21 +373,21 @@ function e2eGate() {
372
373
  }
373
374
  function watchGate() {
374
375
  const state = readState(sessionId);
375
- const owed = findUnarmedHandledPrs(state.prsHandled);
376
- const decision = watchGateDecision(state, owed);
376
+ const untrackedPrUrls = findUntrackedHandledPrs(state.prsHandled);
377
+ const decision = watchGateDecision(state, untrackedPrUrls);
377
378
  if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) {
378
379
  return "{}";
379
380
  }
380
381
  state.watchBlockCount = decision.blockCount;
381
382
  writeState(state);
382
- const prList = decision.owed.join(", ");
383
- const reason = decision.blockCount === 1 ? `Do not end the turn yet. A PR was opened this session but has no armed watcher: ${prList}. muggle-do Stage 8 seeds the watcher slot and arms one watcher per opened PR \u2014 arm it now with /muggle:muggle-pr-followup ${decision.owed[0]} (or reconcile). If this PR should NOT be watched (autoWatchPR=never, a manually-opened PR, one handed off elsewhere, or already merged/closed), tell the user why and run \`echo "MUGGLE_WATCH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Watcher hand-off still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WATCH_BLOCKS}): arm via /muggle:muggle-pr-followup, or record a legitimate skip via \`echo "MUGGLE_WATCH_SKIP: <reason>"\`.`;
383
+ const prList = decision.untracked.join(", ");
384
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. A PR was opened this session but no muggle-do session slot tracks it: ${prList}. Seed the slot and hand off per muggle-do Stage 8 \u2014 /muggle:muggle-pr-followup ${decision.untracked[0]} does both. Seeding is what matters: once a slot exists, reconcile arms it at the next session start and finalizes it when the PR goes terminal, so an unarmed slot is fine but no slot means nothing ever picks this PR up. If it genuinely should not be tracked (autoWatchPR=never, handed off elsewhere), tell the user why and run \`echo "MUGGLE_WATCH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `PR hand-off still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WATCH_BLOCKS}): seed a slot via /muggle:muggle-pr-followup, or record a legitimate skip via \`echo "MUGGLE_WATCH_SKIP: <reason>"\`.`;
384
385
  return blockStop(reason, host);
385
386
  }
386
387
  function reportGate() {
387
- const result = evaluateReportPost(input);
388
- if (!result.deny || !result.reason) return "{}";
389
- return denyTool(result.reason, host);
388
+ const reportPostVerdict = evaluateReportPost(input);
389
+ if (!reportPostVerdict.deny || !reportPostVerdict.reason) return "{}";
390
+ return denyTool(reportPostVerdict.reason, host);
390
391
  }
391
392
  function buildRouter() {
392
393
  if (!detectBuildIntent(input.prompt ?? "")) return "{}";
@@ -25,6 +25,7 @@ Resolve without prompting; use as questionnaire defaults:
25
25
  3. Candidate projects — `muggle-remote-project-list`, ranked against the repo's dev URL and the PR title.
26
26
  4. Existing test-user secrets — `muggle-remote-secret-list` per candidate project (`managed_profile_email` / `managed_profile_password`).
27
27
  5. Auth0 tenant for local dev — grep the repo env file for `*AUTH0_DOMAIN*`.
28
+ 6. E2E run instructions — read `~/.muggle-ai/e2e-instructions/<key>.md` when it exists, keyed on this stack's identity. It records startup order, manual steps, and local gotchas. Treat it as read-only input: surface its gotchas rather than re-deriving them, and never re-ask what it already answers. Absent is normal — proceed without it.
28
29
 
29
30
  ## Questions
30
31
 
@@ -12,7 +12,7 @@ Use this as the top-level Muggle Test command router.
12
12
 
13
13
  ## Preferences
14
14
 
15
- User preferences are injected by the SessionStart hook into a `Muggle Test Preferences` line in session context (key=value pairs). Resolution: defaults → `~/.muggle-ai/preferences.json` (global) → `<repo>/.muggle-ai/preferences.json` (project). Treat absent prefs as `ask`.
15
+ User preferences are injected by the SessionStart hook into a `Muggle Test Preferences` line in session context (key=value pairs). Resolution: defaults → `~/.muggle-ai/preferences.json`. Preferences are user-level and apply to every repo. Treat absent prefs as `ask`.
16
16
 
17
17
  This router skill itself does not gate any decision on a preference — it just routes user intent to a downstream skill. Each downstream skill consults its own preferences. For example, `checkForUpdates` is consulted by `muggle-status` (Check 4), not here.
18
18
 
@@ -29,4 +29,4 @@ Pick the operation, then read its op file for the procedure.
29
29
  - **Current values**: session-context line `Muggle Test Preferences key=value …`. Default `ask`.
30
30
  - **Per-key files**: `preference-gates/<key>.md`. Key list = `ls preference-gates/*.md` minus `README.md`.
31
31
  - **Allowed values**: `always`/`never`/`ask` (or `local`/`remote`/`ask` for `defaultExecutionMode`).
32
- - **Scope**: `global` default; `project` if user says "for this project" / "just this repo" (pass `cwd`).
32
+ - **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.
@@ -0,0 +1,45 @@
1
+ {
2
+ "skill_name": "muggle-preferences",
3
+ "notes": "These evals test the PLAN behavior after per-project preferences were removed. Preferences resolve from defaults overlaid by ~/.muggle-ai/preferences.json only; muggle-local-preferences-set rejects a `scope` argument. The prompts ask for a step-by-step plan rather than execution, since writing preferences mutates the user's home directory. Assertions check that the plan never offers a per-project scope, never passes `scope`, and confirms before applying a setting the user framed as project-specific.",
4
+ "evals": [
5
+ {
6
+ "id": 0,
7
+ "eval_name": "per-project-request-must-not-be-honoured-silently",
8
+ "prompt": "For this repo only, I want the Electron browser to actually show up when tests run — it's a UI-heavy project and I like watching it. Leave my other projects alone. Walk me through what you'd do.",
9
+ "files": [],
10
+ "assertions": [
11
+ { "name": "states_preferences_are_user_level", "text": "Plan states that preferences are user-level and apply to every repo, so a per-repo setting is not supported." },
12
+ { "name": "does_not_offer_project_scope", "text": "Plan does NOT offer a 'this project only' option, and does not claim a project-scoped preferences file can be written." },
13
+ { "name": "no_scope_argument", "text": "Plan does not pass a `scope` argument to muggle-local-preferences-set; it calls the tool with key and value only." },
14
+ { "name": "confirms_before_global_write", "text": "Plan explicitly confirms with the user before writing, because the user asked for one repo and the write will affect all of them." },
15
+ { "name": "names_the_correct_key", "text": "Plan identifies showElectronBrowser as the key to set." },
16
+ { "name": "does_not_invent_a_workaround", "text": "Plan does not invent a per-repo mechanism (env var, local override file, wrapper script) to simulate project scope." }
17
+ ]
18
+ },
19
+ {
20
+ "id": 1,
21
+ "eval_name": "list-shows-user-level-resolution",
22
+ "prompt": "show me my muggle settings and where they're stored",
23
+ "files": [],
24
+ "assertions": [
25
+ { "name": "resolution_is_two_layer", "text": "Plan describes resolution as defaults overlaid by ~/.muggle-ai/preferences.json, with no third per-project layer." },
26
+ { "name": "names_the_global_path", "text": "Plan names ~/.muggle-ai/preferences.json as the storage location." },
27
+ { "name": "no_project_file_mentioned", "text": "Plan does not tell the user that a preferences file inside the repo is read or honoured." },
28
+ { "name": "reads_from_session_context_or_tool", "text": "Plan reads current values from the session-context preferences line or the preferences tool rather than guessing." }
29
+ ]
30
+ },
31
+ {
32
+ "id": 2,
33
+ "eval_name": "stale-in-repo-file-explained",
34
+ "prompt": "I've got a .muggle-ai/preferences.json checked into my repo with autoLogin set to never, but muggle keeps logging me in automatically. Why isn't it being picked up?",
35
+ "files": [],
36
+ "assertions": [
37
+ { "name": "explains_file_is_not_read", "text": "Plan explains that a preferences file inside the project is no longer read — preferences are user-level." },
38
+ { "name": "points_at_global_file", "text": "Plan points the user at ~/.muggle-ai/preferences.json as the file that actually takes effect." },
39
+ { "name": "offers_to_reapply_globally", "text": "Plan offers to re-apply the affected key globally, noting it will then apply to every repo." },
40
+ { "name": "does_not_delete_user_file", "text": "Plan does not delete or rewrite the user's in-repo preferences file without asking." },
41
+ { "name": "no_scope_argument", "text": "Plan does not attempt to pass a `scope` argument to make the project file work." }
42
+ ]
43
+ }
44
+ ]
45
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  1. Verify `preference-gates/<key>.md` exists. If not, list `preference-gates/*.md` and ask.
4
4
  2. Read it. Run **Picker 1** with the options defined in the gate file, plus an additional `Ask me each time` (sub: `Prompt me at decision time.`) → `ask`.
5
- 3. `muggle-local-preferences-set` with the mapped value, `scope: "global"`.
5
+ 3. `muggle-local-preferences-set` with the mapped value.
6
6
  4. Confirm: `Set <key> to <value>.`
7
7
 
8
8
  Skip Picker 2 — user explicitly asked to change.
@@ -33,18 +33,17 @@ For each option: label = key name, description = first paragraph of `preference-
33
33
  - `multiSelect: true`, `header: "Branch hygiene"` — `autoUseWorktree`, `autoRebase`, `autoCleanup`
34
34
  - `multiSelect: false`, `header: "E2E acceptance"` — `autoE2ETest`. Options: `Always run Stage 6 at the end` (`always` — default), `Ask each cycle` (`ask`). No `never` option.
35
35
  - `multiSelect: false`, `header: "Default mode"` — `defaultExecutionMode`. Options: `Local — run on my computer` (`local`), `Remote — run in the Muggle Test cloud` (`remote`), `Ask each time` (don't change).
36
- - `multiSelect: false`, `header: "Scope"` — final scope question. Options: `Global (all repos)` (~/.muggle-ai/), `This project only` (.muggle-ai/ in repo).
37
36
 
38
- `AskUserQuestion` accepts up to 4 questions per call — split into two calls if needed (categories first, scope second).
37
+ `AskUserQuestion` accepts up to 4 questions per call — split across calls.
39
38
 
40
39
  ## Step 4 — apply selections
41
40
 
42
- For each toggled key (multi-select questions): `muggle-local-preferences-set` with `value: "always"`. For `defaultExecutionMode`: only set if user picked Local/Remote (skip "Ask each time"). Pass `scope` from the scope question; pass `cwd` when scope is `project`.
41
+ For each toggled key (multi-select questions): `muggle-local-preferences-set` with `value: "always"`. For `defaultExecutionMode`: only set if user picked Local/Remote (skip "Ask each time").
43
42
 
44
43
  ## Step 5 — `never` follow-up
45
44
 
46
- Ask: `Want any of these set to "never" (auto-skip without asking)? Name them, e.g. "never on showElectronBrowser", or say "no".`. For named keys, call `muggle-local-preferences-set` with `value: "never"`, same scope.
45
+ Ask: `Want any of these set to "never" (auto-skip without asking)? Name them, e.g. "never on showElectronBrowser", or say "no".`. For named keys, call `muggle-local-preferences-set` with `value: "never"`.
47
46
 
48
47
  ## Step 6 — confirm
49
48
 
50
- One-liner summary: `Set autoLogin=always, openTestResultsAfterRun=always (global).`
49
+ One-liner summary: `Set autoLogin=always, openTestResultsAfterRun=always.`
@@ -5,5 +5,5 @@
5
5
  3. Footer:
6
6
  ```
7
7
  Values: always · ask · never (defaultExecutionMode: local/remote/ask)
8
- Scope: global (~/.muggle-ai/) or project (.muggle-ai/ in repo root)
8
+ Stored: ~/.muggle-ai/preferences.json — applies to every repo
9
9
  ```
@@ -1,10 +1,9 @@
1
1
  # Set — direct (key + value)
2
2
 
3
- Trigger: user names both key and value (e.g. "set autoLogin to always", "make showElectronBrowser never for this project").
3
+ Trigger: user names both key and value (e.g. "set autoLogin to always", "make showElectronBrowser never").
4
4
 
5
5
  1. Parse `key` and `value`.
6
6
  2. Verify `preference-gates/<key>.md` exists. If not, list `preference-gates/*.md` and ask.
7
7
  3. Validate `value` per Shared context.
8
- 4. Resolve scope per Shared context.
9
- 5. `muggle-local-preferences-set`.
10
- 6. Confirm: `Set {key} to {value} ({scope}).`
8
+ 4. `muggle-local-preferences-set`.
9
+ 5. Confirm: `Set {key} to {value}.`
@@ -11,8 +11,8 @@ which uses `local` / `remote` / `ask`).
11
11
  ## Resolution
12
12
 
13
13
  `SessionStart` injects a `Muggle Test Preferences` line (`key=value` pairs) from
14
- `~/.muggle-ai/preferences.json` (global) overlaid by
15
- `<repo>/.muggle-ai/preferences.json` (project). Absent → treat as `ask`.
14
+ `~/.muggle-ai/preferences.json`. Preferences are user-level — there is no
15
+ per-project layer. Absent → treat as `ask`.
16
16
 
17
17
  ## Gate behavior
18
18
 
@@ -48,7 +48,7 @@ Header `Remember this choice?`. Question: `"Always <restate Picker 1 choice in
48
48
  plain language> from now on, without asking?"`. Never put the raw key or
49
49
  `=` in the user-visible text.
50
50
 
51
- - `Yes, always` (sub: `You can change this later in muggle preferences.`) → `muggle-local-preferences-set` with the value Picker 1 mapped to, `scope: "global"`.
51
+ - `Yes, always` (sub: `You can change this later in muggle preferences.`) → `muggle-local-preferences-set` with the value Picker 1 mapped to.
52
52
  - `Just this once` (sub: `I'll ask again next time.`) → don't save.
53
53
 
54
54
  A few keys override this template (their per-key file says how).
@@ -2,7 +2,7 @@
2
2
 
3
3
  Reuse the saved local dev server URL for this repo, or pick one each run. Substitute `{lastHost}` (the URL used in the previous run for this repo — omit the option entirely when no cache exists) and `{suggestedHost}` (auto-detected from running ports, e.g. `http://localhost:3000`).
4
4
 
5
- Cache lives at `<cwd>/.muggle-ai/last-host.json`. The calling skill **always** updates the cache after the user picks/types a URL — independent of Picker 2 — so `Use {lastHost}` reflects the most recent run.
5
+ Cache lives in `~/.muggle-ai/last-host.json`, in the entry keyed on `<cwd>`. The calling skill **always** updates the cache after the user picks/types a URL — independent of Picker 2 — so `Use {lastHost}` reflects the most recent run.
6
6
 
7
7
  **Picker 1** — header `Local server`, question `"Which local URL should the test target?"`
8
8
  - `Use {lastHost}` — `From your last run in this repo.` → reuse cached URL. *Skip this option when no cache exists.*
@@ -11,7 +11,7 @@ Cache lives at `<cwd>/.muggle-ai/last-host.json`. The calling skill **always** u
11
11
 
12
12
  **Picker 2 — overrides shared template.** Fires after the user picks a URL.
13
13
  - Header `Remember this URL?`, question `"Always use {chosenHost} for this repo from now on, without asking?"`
14
- - `Yes, always` (sub: `You can change this later in muggle preferences.`) → `muggle-local-preferences-set` (`autoSelectLocalHost=always`, global). The cache is already up to date.
14
+ - `Yes, always` (sub: `You can change this later in muggle preferences.`) → `muggle-local-preferences-set` (`autoSelectLocalHost=always`). The cache is already up to date.
15
15
  - `Just this once` (sub: `I'll ask again next time.`) → don't save the preference. The cache still updates.
16
16
 
17
17
  **Silent action**
@@ -8,7 +8,7 @@ tail options like "Show full list" / "Create new project" are skill-defined).
8
8
  **Picker 2 — overrides shared template.** Fires only after picking an
9
9
  *existing* project; skip if user picked "Create new project".
10
10
  - Header `Reuse this project next time?`, question `"Always reuse {projectName} for this repo from now on, without asking?"`
11
- - `Yes, always` (sub: `You can change this later in muggle preferences.`) → call BOTH `muggle-local-preferences-set` (`autoSelectProject=always`, global) AND `muggle-local-last-project-set` (`cwd`, `projectId`, `projectUrl`, `projectName`).
11
+ - `Yes, always` (sub: `You can change this later in muggle preferences.`) → call BOTH `muggle-local-preferences-set` (`autoSelectProject=always`) AND `muggle-local-last-project-set` (`cwd`, `projectId`, `projectUrl`, `projectName`).
12
12
  - `Just this once` (sub: `I'll ask again next time.`) → don't save.
13
13
 
14
14
  **Silent action**
@@ -128,7 +128,7 @@ If auth fails repeatedly, suggest: `muggle logout && muggle login` from terminal
128
128
 
129
129
  A **project** is where all your test results, use cases, and test scripts are grouped on the Muggle AI dashboard. Pick the project that matches what you're working on.
130
130
 
131
- The per-repo cache lives at `<cwd>/.muggle-ai/last-project.json` (managed via the `muggle-local-last-project-get` / `muggle-local-last-project-set` MCP tools). Look for the `Muggle Test Last Project: id=… url=… name="…"` line in session context — if present, that's this repo's cached pick.
131
+ The per-repo cache lives in `~/.muggle-ai/last-project.json`, in the entry keyed on `<cwd>` (managed via the `muggle-local-last-project-get` / `muggle-local-last-project-set` MCP tools). Look for the `Muggle Test Last Project: id=… url=… name="…"` line in session context — if present, that's this repo's cached pick.
132
132
 
133
133
  Gate `autoSelectProject` (per `preference-gates/README.md`). Cache: `Muggle Test Last Project` session line.
134
134
  - `always` + cache → use cached `projectId`, skip to Step 5. No cache → fall through to `ask`.
@@ -12,6 +12,8 @@
12
12
 
13
13
  Before anything else, invoke [`muggle-test-prepare`](../muggle-test-prepare/SKILL.md) — the readiness/service-start owner (idempotent; halt on what it surfaces). The URL gate below only *selects* the target; prepare is what guarantees something is listening and compiled.
14
14
 
15
+ Then read `~/.muggle-ai/e2e-instructions/<key>.md` when it exists — this stack's recorded startup order, manual steps, and local gotchas. Use it to interpret what you see: a gotcha listed there explains a symptom that would otherwise read as a failure. Absent is normal.
16
+
15
17
  ## Pre-flight question — Local URL (gated by `autoSelectLocalHost`)
16
18
 
17
19
  Skill responsibilities (the rest is in `preference-gates/autoSelectLocalHost.md`):