@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
@@ -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`):
@@ -29,6 +29,8 @@ Three gates apply, each per the standard procedure in [`preference-gates/README.
29
29
 
30
30
  Before any workflow step, invoke [`muggle-test-prepare`](../muggle-test-prepare/SKILL.md). Halt on what it surfaces.
31
31
 
32
+ Then read `~/.muggle-ai/e2e-instructions/<key>.md` when it exists — this stack's recorded startup order, manual steps, and local gotchas. A gotcha listed there explains a symptom that would otherwise read as a failure. Absent is normal.
33
+
32
34
  ## UX Guidelines — Minimize Typing
33
35
 
34
36
  **Every selection-based question MUST use the `AskUserQuestion` tool** (or the platform's equivalent structured selection tool). Never ask the user to "reply with a number" in a plain text message — always present clickable options.
@@ -67,7 +69,7 @@ Gates run per `preference-gates/README.md`.
67
69
 
68
70
  ### 2. Targets (user must confirm)
69
71
 
70
- The per-repo project cache lives at `<cwd>/.muggle-ai/last-project.json` (via the `muggle-local-last-project-get` / `muggle-local-last-project-set` MCP tools). Look for `Muggle Test Last Project: id=… url=… name="…"` in session context.
72
+ The per-repo project cache lives in `~/.muggle-ai/last-project.json`, in the entry keyed on `<cwd>` (via the `muggle-local-last-project-get` / `muggle-local-last-project-set` MCP tools). Look for `Muggle Test Last Project: id=… url=… name="…"` in session context.
71
73
 
72
74
  Gate `autoSelectProject` (per `preference-gates/README.md`). Cache: `Muggle Test Last Project` session line.
73
75
  - `always` + cache → use cached `projectId`, skip to use case selection. No cache → fall through to `ask`.
@@ -151,7 +151,7 @@ If **not authenticated**:
151
151
 
152
152
  A **project** is where all your imported use cases, test cases, and future test results are grouped on the Muggle AI dashboard.
153
153
 
154
- The per-repo project cache lives at `<cwd>/.muggle-ai/last-project.json` (via the `muggle-local-last-project-get` / `muggle-local-last-project-set` MCP tools). Look for `Muggle Test Last Project: id=… url=… name="…"` in session context.
154
+ The per-repo project cache lives in `~/.muggle-ai/last-project.json`, in the entry keyed on `<cwd>` (via the `muggle-local-last-project-get` / `muggle-local-last-project-set` MCP tools). Look for `Muggle Test Last Project: id=… url=… name="…"` in session context.
155
155
 
156
156
  Gate `autoSelectProject` (per `preference-gates/README.md`). Cache: `Muggle Test Last Project` session line.
157
157
  - `always` + cache → use cached `projectId`, skip to Step 6. No cache → fall through to `ask`.
@@ -48,7 +48,11 @@ All launched processes are tracked in `/tmp/muggle-test-prepare.json`:
48
48
 
49
49
  `testing_scope` records what the user is testing (from [scope](./steps/scope.md)). `excluded_services` records services the user said can't run locally (from [viability-check](./steps/viability-check.md)).
50
50
 
51
- This file is **ephemeral runtime state**, not the saved recipe. The durable plan lives at `<repo>/.muggle-ai/prepare-plan.json` (or the parent-dir-keyed entry in `~/.muggle-ai/prepare-plans.json`) and is consulted in [reuse-plan](./steps/reuse-plan.md) before any other stage. The two files never merge. The `test-prepare-runner` agent writes this file during execution; the triage below and Cleanup read it.
51
+ This file is **ephemeral runtime state**, not the saved recipe. The durable plan lives in `~/.muggle-ai/prepare-plans.json`, under the entry keyed on this stack, and is consulted in [reuse-plan](./steps/reuse-plan.md) before any other stage. The two files never merge. The `test-prepare-runner` agent writes this file during execution; the triage below and Cleanup read it.
52
+
53
+ The prose companion to the plan is `~/.muggle-ai/e2e-instructions/<key>.md`, written by [e2e-instructions](./steps/e2e-instructions.md), holding startup order, manual steps, and local gotchas. The plan remains the single source of truth for each service's start command; the markdown never restates one.
54
+
55
+ **Everything this skill saves is machine-local, user-level data and lives under the Muggle home directory — never inside the user's project.** A project directory is shared, versioned, and cloned onto machines set up differently; a local run recipe is none of those things. Both files are keyed on the same stack identity, so they stay in lockstep.
52
56
 
53
57
  **On every invocation**, check this file first. If it exists with live PIDs (verify with `kill -0`), `AskUserQuestion`:
54
58
  - Option 1: "Keep them running — skip to testing"
@@ -64,7 +68,7 @@ Gates run per [`preference-gates/README.md`](../muggle-preferences/preference-ga
64
68
  | Preference | Gates |
65
69
  |------------|-------|
66
70
  | `autoRebase` | [rebase-check](./steps/rebase-check.md) — rebase onto `origin/<default>` before starting dev servers |
67
- | `reusePreparePlan` | [reuse-plan](./steps/reuse-plan.md) — reuse the saved prepare plan for this stack, or rediscover |
71
+ | `reusePreparePlan` | [reuse-plan](./steps/reuse-plan.md) — reuse the saved prepare plan for this stack, or rediscover; also [e2e-instructions](./steps/e2e-instructions.md), which goes stale for the same reason |
68
72
  | `autoSelectLocalHost` | [check-running](./steps/check-running.md) — reuse the recorded dev-server URL silently, or confirm it each run |
69
73
 
70
74
  ## Workflow
@@ -78,8 +82,9 @@ Gates run per [`preference-gates/README.md`](../muggle-preferences/preference-ga
78
82
  | 2 | [scope](./steps/scope.md) | Frontend / backend / full stack |
79
83
  | 3 | [viability-check](./steps/viability-check.md) | Exclude services that can't run locally |
80
84
  | 4 | [identify-services](./steps/identify-services.md) | Pick required services + startup mode |
85
+ | 5 | [e2e-instructions](./steps/e2e-instructions.md) | Capture startup order, manual steps, local gotchas (gated); persisted for reuse |
81
86
 
82
- The Decide phase's output is the **resolved prepare plan**: `services[]` (name, dir, start command, expected port, `external` flag, approval granted), `testingScope`, `excludedServices[]`, the recorded dev-server URL, and resolved gate outcomes.
87
+ The Decide phase's output is the **resolved prepare plan**: `services[]` (name, dir, start command, expected port, `external` flag, approval granted), `testingScope`, `excludedServices[]`, the recorded dev-server URL, the E2E run instructions, and resolved gate outcomes.
83
88
 
84
89
  **Execute (agent).** Dispatch the `test-prepare-runner` agent (subagent type `muggle:test-prepare-runner`; bare `test-prepare-runner` where the plugin namespace is absent), synchronously, passing the resolved plan; it returns `READY` / `DEGRADED` plus the readiness table. The agent's own definition lists its stage files; in a harness with no agent/subagent facility, run the execute-phase stages ([check-running](./steps/check-running.md) through [readiness-report](./steps/readiness-report.md)) inline instead.
85
90
 
@@ -114,12 +119,13 @@ After a test run, the caller can re-invoke for cleanup or leave services running
114
119
 
115
120
  ## Guardrails
116
121
 
117
- - **Never invent or default a host/port** — the dev-server URL is a recorded value, not a guess. Resolve it from `<repo>/.muggle-ai/last-host.json` (the [`autoSelectLocalHost`](../muggle-preferences/preference-gates/autoSelectLocalHost.md) cache) before probing ports; a framework default like `:3000` is never a fallback. See [check-running](./steps/check-running.md).
122
+ - **Never invent or default a host/port** — the dev-server URL is a recorded value, not a guess. Read it from the last-host cache via `muggle-local-last-host-get` (the [`autoSelectLocalHost`](../muggle-preferences/preference-gates/autoSelectLocalHost.md) cache) before probing ports; a framework default like `:3000` is never a fallback. The cache's own storage location belongs to that tool — don't restate it here. See [check-running](./steps/check-running.md).
118
123
  - **No silent auto-selection without a gate** — when no preference authorizes a silent choice (host, restart, kill), confirm with the user. A gate set to `always` is the only license to skip the question; absent that, ask.
119
124
  - **Verify first, offer to start second** — check what's already running before proposing to start anything.
120
125
  - **The user may prefer to start services themselves** — always offer that option.
121
126
  - **Never start a process the user didn't approve** — approvals are granted in Decide and travel in the plan; the agent starts nothing outside it.
122
127
  - **Never read file contents outside confirmed directories** — folder names are discoverable; file contents require explicit user selection.
128
+ - **Never write a credential into the E2E run instructions** — plaintext notes are not a secret store, and this is the file a user pastes when asking why their stack won't start. Record the env-var or secret name, never its value.
123
129
  - **Never leave orphan processes untracked** — every background PID goes into the tracking file.
124
130
  - **Never kill a process the user started independently** — `external: true` survives cleanup.
125
131
  - **Never assume start commands** — verify via indicator file; confirm with user.
@@ -4,7 +4,7 @@
4
4
 
5
5
  The dev-server URL the tests will hit is a **recorded value, not a guess** — resolve it before probing anything.
6
6
 
7
- 1. Read the cached host with `muggle-local-last-host-get`. It reads `<cwd>/.muggle-ai/last-host.json`; a worktree usually has **no cache of its own**, so when the worktree returns nothing, pass the **main** working-tree root as `cwd` — `git rev-parse --git-common-dir`, then its parent directory.
7
+ 1. Read the cached host with `muggle-local-last-host-get`. It reads the entry keyed on `<cwd>` in `~/.muggle-ai/last-host.json`; a worktree usually has **no cache of its own**, so when the worktree returns nothing, pass the **main** working-tree root as `cwd` — `git rev-parse --git-common-dir`, then its parent directory.
8
8
  2. Apply the [`autoSelectLocalHost`](../../muggle-preferences/preference-gates/autoSelectLocalHost.md) gate (read its value from the `Muggle Test Preferences` session-context line; absent → `ask`):
9
9
  - `always` **and** a cache exists → use it silently: `Using saved local URL {lastHost}`.
10
10
  - otherwise (`ask` / `never`, or no cache) → **confirm before using any host.** Run the gate's Picker 1 with `{lastHost}` (cached URL, omitted when absent) and `{suggestedHost}` (a port you actually detect listening). Never auto-pick, and never fall back to a framework default like `:3000`; if nothing is cached or detected, ask the user to type the URL.
@@ -0,0 +1,100 @@
1
+ # Stage 5 — E2E run instructions
2
+
3
+ Capture what `prepare-plan.json` cannot express: the order services must come up in, steps that aren't a single command, and the local gotchas that make a healthy stack look broken. Persisted under `~/.muggle-ai/e2e-instructions/` and reused on later runs.
4
+
5
+ Runs after [identify-services](./identify-services.md) — startup order and per-service gotchas are unanswerable until the service set is known.
6
+
7
+ ## Scope boundary
8
+
9
+ `prepare-plan.json` owns the per-service start command. Never restate a command here; reference the plan. This file holds only what the plan has no field for:
10
+
11
+ | Belongs here | Belongs in `prepare-plan.json` |
12
+ |:-------------|:-------------------------------|
13
+ | Order and dependencies between services | Each service's `name`, `dir`, `command`, `port` |
14
+ | Steps that aren't one command (a migration to run first, a tunnel to open, a container to bring up by hand) | — |
15
+ | Gotchas — slow first build, a port that isn't the framework default, a rate limit, a warning that is safe to ignore | — |
16
+
17
+ ## Resolve the saved file
18
+
19
+ This is machine-local, user-level data. It lives under the Muggle home directory and never inside the user's project — a project directory is shared, versioned, and cloned by people whose machines are set up differently, and none of that is true of a local run recipe.
20
+
21
+ One file per stack:
22
+
23
+ ```
24
+ ~/.muggle-ai/e2e-instructions/<key>.md
25
+ ```
26
+
27
+ `<key>` is the absolute path of the working directory's parent — the same stack identity the global prepare plan keys its entries on — reduced to a filename-safe token by replacing every path separator, and any drive-letter colon, with `-`. Derive it from the resolved absolute path rather than assuming a separator character; they differ per platform.
28
+
29
+ Missing → no saved instructions; run the capture below.
30
+
31
+ ## Gate `reusePreparePlan`
32
+
33
+ Same gate as [reuse-plan](./reuse-plan.md) — this content goes stale for the same reason the service plan does, so one answer governs both. Only fires when a saved file was loaded.
34
+
35
+ - `always` → reuse silently. Print `Reusing saved E2E run instructions`.
36
+ - `never` → discard and run the capture.
37
+ - `ask` → print the saved file, then Picker 1 from the gate contract. Reuse on `Reuse this plan`, capture on `Rediscover from scratch`.
38
+
39
+ On the [reuse-plan](./reuse-plan.md) short-circuit path this stage is skipped along with the rest of the Decide phase; the saved file is loaded there and carried forward unchanged.
40
+
41
+ ## Capture
42
+
43
+ Print the resolved service list first so the user answers against concrete names, then ask one `AskUserQuestion`, multi-select:
44
+
45
+ > "Anything Muggle should know about running these locally?"
46
+
47
+ - `Startup order matters` — `One service must be up before another, or something fails.`
48
+ - `Manual steps` — `Something has to happen that isn't one of the start commands.`
49
+ - `Known gotchas` — `Behaviour that looks like a failure but isn't, or a trap to avoid.`
50
+ - `Nothing special` — `They start independently and just work.`
51
+
52
+ `Nothing special` (or no selection) → write the sentinel from [Sentinel](#sentinel) and return. Do not re-ask on later runs; the sentinel is a recorded answer, not an empty file.
53
+
54
+ Otherwise ask once more, free-text, naming only the selected categories. One turn — never a question per category.
55
+
56
+ ## Written format
57
+
58
+ Fixed headings. Omit a section the user had nothing for; never emit an empty one.
59
+
60
+ ```markdown
61
+ # E2E run instructions
62
+
63
+ <!-- Managed by muggle-test-prepare. Hand-edits are preserved — re-run the skill to revise. -->
64
+
65
+ **Updated:** 2026-08-04T12:00:00Z
66
+
67
+ ## Startup order
68
+
69
+ api → worker → ui. The UI 500s on /dashboard when api isn't listening yet.
70
+
71
+ ## Manual steps
72
+
73
+ Run `pnpm db:migrate` once after a fresh clone; the dev servers don't migrate on boot.
74
+
75
+ ## Local gotchas
76
+
77
+ - First build after a clean install takes ~4 min. It is not hung.
78
+ - UI is on :3999, not the Next.js default :3000.
79
+ - Auth0 dev tenant rate-limits past ~20 logins/hour.
80
+ ```
81
+
82
+ ### Sentinel
83
+
84
+ ```markdown
85
+ # E2E run instructions
86
+
87
+ <!-- Managed by muggle-test-prepare. Hand-edits are preserved — re-run the skill to revise. -->
88
+
89
+ **Updated:** 2026-08-04T12:00:00Z
90
+
91
+ Nothing special — services start independently.
92
+ ```
93
+
94
+ ## Secrets
95
+
96
+ Never write a credential value. A password, token, API key, or connection string with embedded credentials belongs in a secret store or an env file — not in plaintext notes. Living outside the project keeps this file out of version control, but it is still readable on disk and is exactly the kind of file a user pastes into an issue when asking why their stack won't come up.
97
+
98
+ Record a **pointer** instead: the env-var name, or the name of the Muggle secret. `Set LOCAL_TEST_PASSWORD before running` is fine; the password is not.
99
+
100
+ If the user's free text contains something that looks like a credential, drop it, write the pointer form, and say so in one line.
@@ -41,10 +41,7 @@ jq '{
41
41
  }' /tmp/muggle-test-prepare.json
42
42
  ```
43
43
 
44
- Resolve the write location:
45
-
46
- - If `git rev-parse --show-toplevel` succeeds (call the result `$REPO`) → write `$REPO/.muggle-ai/prepare-plan.json`. Create `$REPO/.muggle-ai/` if missing.
47
- - Else → upsert the entry under key `$(dirname "$PWD")` (absolute) in `~/.muggle-ai/prepare-plans.json`. Create the file as `{}` if missing.
44
+ Upsert it under the stack's key — the absolute path of the working directory's parent — in `~/.muggle-ai/prepare-plans.json`, creating the file as `{}` if missing. The plan is machine-local, user-level data: it never goes inside the user's project, and resolving it requires no version-control tool.
48
45
 
49
46
  Then print, once:
50
47
 
@@ -54,3 +51,11 @@ Then print, once:
54
51
  ```
55
52
 
56
53
  If this run short-circuited via [reuse-plan](./reuse-plan.md), don't rewrite — but **do** refresh `updated` and any `command` that was re-derived during validation. Skip the announcement on the refresh path.
54
+
55
+ ## Save the E2E run instructions
56
+
57
+ The instructions arrive resolved in the dispatch prompt — captured while the user was present, in [e2e-instructions](./e2e-instructions.md). Write them verbatim, in that stage's format, to the location it resolves: `~/.muggle-ai/e2e-instructions/<key>.md`. Create the directory if missing. This file is machine-local and never goes inside the user's project.
58
+
59
+ Nothing resolved in the plan → write nothing. An absent file is a stage that has not run yet; an empty one would read as "nothing to say" and suppress the question forever.
60
+
61
+ Never write a credential value here — pointers only, per that stage's Secrets rule.
@@ -6,12 +6,13 @@ A previously saved **prepare plan** is the durable recipe for this stack. Distin
6
6
 
7
7
  In order; first hit wins.
8
8
 
9
- 1. **Project plan.** If `git rev-parse --show-toplevel` succeeds (call the result `$REPO`) and `$REPO/.muggle-ai/prepare-plan.json` exists → load it.
10
- 2. **Global plan.** Else if `~/.muggle-ai/prepare-plans.json` exists, read the entry keyed by `$(dirname "$PWD")` (absolute path). If present → load that entry's value.
11
- 3. **No plan found** → exit this step; the workflow continues at [rebase-check](./rebase-check.md).
9
+ 1. **Saved plan.** If `~/.muggle-ai/prepare-plans.json` exists, read the entry keyed on this stack — the absolute path of the working directory's parent. If present → load that entry's value. The plan is machine-local and is never read from, or written to, the user's project.
10
+ 2. **No plan found** → exit this step; the workflow continues at [rebase-check](./rebase-check.md).
12
11
 
13
12
  A loaded plan is a JSON object with `version`, `updated`, `testing_scope`, `excluded_services`, `services`. Reject and treat as "no plan" if `version != 1` or `services` is empty.
14
13
 
14
+ Load the prose companion per [e2e-instructions](./e2e-instructions.md) — `~/.muggle-ai/e2e-instructions/<key>.md`, keyed on the same stack identity as the global plan entry. It is independent of the plan: a missing companion is not a missing plan, and vice versa.
15
+
15
16
  ## Gate `reusePreparePlan`
16
17
 
17
18
  Per [`muggle-preferences/preference-gates/README.md`](../../muggle-preferences/preference-gates/README.md). Read the current value from the `Muggle Test Preferences` session-context line; absent → `ask`.
@@ -37,7 +38,7 @@ Per [`muggle-preferences/preference-gates/README.md`](../../muggle-preferences/p
37
38
  - The indicator file that produced `command` still exists in `dir` (e.g. `package.json` for an `npm`/`node` command; see the indicator table in [start-commands](./start-commands.md)) → keep. Else re-derive **just that one entry** by running the indicator-detection from [start-commands](./start-commands.md) against `dir`, and replace its `command`. Log `"Re-derived <name>: <old> → <new>"`.
38
39
  2. **All entries dropped** → discard the plan; continue at [rebase-check](./rebase-check.md). Otherwise proceed with surviving + re-derived entries.
39
40
  3. **Hydrate** `/tmp/muggle-test-prepare.json` with the surviving entries (no PIDs yet, `testing_scope` from the plan, `excluded_services` from the plan).
40
- 4. **Short-circuit** to [check-running](./check-running.md). The skipped stages are [scope](./scope.md), [viability-check](./viability-check.md), [identify-services](./identify-services.md), [start-commands](./start-commands.md) — the reused plan supplies their answers. The remaining stages run normally: [env-file](./env-file.md), [fresh-install](./fresh-install.md), [start-services](./start-services.md) (only for entries not already listening), [smoke-test](./smoke-test.md), [readiness-report](./readiness-report.md).
41
+ 4. **Short-circuit** to [check-running](./check-running.md). The skipped stages are [scope](./scope.md), [viability-check](./viability-check.md), [identify-services](./identify-services.md), [start-commands](./start-commands.md), [e2e-instructions](./e2e-instructions.md) — the reused plan and its companion supply their answers. Carry the loaded instructions forward unchanged; the user is not re-asked. When the companion is absent, run [e2e-instructions](./e2e-instructions.md) once to capture it, then continue the short-circuit. The remaining stages run normally: [env-file](./env-file.md), [fresh-install](./fresh-install.md), [start-services](./start-services.md) (only for entries not already listening), [smoke-test](./smoke-test.md), [readiness-report](./readiness-report.md).
41
42
 
42
43
  ## Rediscover path
43
44