@muggleai/works 5.9.0 → 5.11.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 (171) hide show
  1. package/README.md +8 -0
  2. package/dist/{chunk-LVUJ4E62.js → chunk-MSPMXBO5.js} +638 -468
  3. package/dist/{chunk-5PYK7DWB.js → chunk-WUMAJOBT.js} +17 -6
  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 +4 -1
  10. package/dist/plugin/agents/visual-walkthrough-builder.md +12 -5
  11. package/dist/plugin/commands/mtest.md +1 -1
  12. package/dist/plugin/hooks/README.md +15 -3
  13. package/dist/plugin/hooks/hooks.json +79 -0
  14. package/dist/plugin/scripts/ensure-electron-app.sh +62 -47
  15. package/dist/plugin/scripts/guardrail-classify-gate.sh +23 -0
  16. package/dist/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
  17. package/dist/plugin/scripts/guardrail-pr-terminal.sh +6 -2
  18. package/dist/plugin/scripts/guardrail-record-stage-read.sh +21 -0
  19. package/dist/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
  20. package/dist/plugin/scripts/guardrail-record-tests.sh +11 -4
  21. package/dist/plugin/scripts/guardrail-report-format.sh +9 -5
  22. package/dist/plugin/scripts/guardrail-skill-stages.sh +35 -0
  23. package/dist/plugin/scripts/guardrail-stage-gate.sh +43 -0
  24. package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
  25. package/dist/plugin/scripts/guardrails.mjs +474 -80
  26. package/dist/plugin/scripts/pr-watch-events.sh +116 -0
  27. package/dist/plugin/scripts/pr-watch-guards.sh +8 -2
  28. package/dist/plugin/scripts/pr-watch-loop.sh +254 -0
  29. package/dist/plugin/scripts/reconcile-stale-watchers.sh +66 -18
  30. package/dist/plugin/scripts/sign-body.sh +88 -0
  31. package/dist/plugin/skills/CLAUDE.md +16 -0
  32. package/dist/plugin/skills/_shared/post-merge-cleanup.md +83 -3
  33. package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
  34. package/dist/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
  35. package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
  36. package/dist/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
  37. package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
  38. package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
  39. package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
  40. package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
  41. package/dist/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
  42. package/dist/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
  43. package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
  44. package/dist/plugin/skills/_shared/vcs/post-signature.md +23 -12
  45. package/dist/plugin/skills/do/open-prs/forward.md +2 -2
  46. package/dist/plugin/skills/do/per-comment-replies.md +2 -15
  47. package/dist/plugin/skills/muggle/SKILL.md +1 -1
  48. package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
  49. package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
  50. package/dist/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
  51. package/dist/plugin/skills/muggle-pr-followup/adopt.md +60 -0
  52. package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
  53. package/dist/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
  54. package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
  55. package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
  56. package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
  57. package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
  58. package/dist/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
  59. package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
  60. package/dist/plugin/skills/muggle-preferences/SKILL.md +1 -1
  61. package/dist/plugin/skills/muggle-preferences/evals/evals.json +45 -0
  62. package/dist/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
  63. package/dist/plugin/skills/muggle-preferences/ops/configure.md +4 -5
  64. package/dist/plugin/skills/muggle-preferences/ops/list.md +1 -1
  65. package/dist/plugin/skills/muggle-preferences/ops/set.md +3 -4
  66. package/dist/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
  67. package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
  68. package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
  69. package/dist/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
  70. package/dist/plugin/skills/muggle-test/SKILL.md +23 -3
  71. package/dist/plugin/skills/muggle-test/execute-local.md +2 -0
  72. package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +6 -1
  73. package/dist/plugin/skills/muggle-test-import/SKILL.md +1 -1
  74. package/dist/plugin/skills/muggle-test-prepare/SKILL.md +28 -15
  75. package/dist/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
  76. package/dist/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
  77. package/dist/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
  78. package/dist/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +111 -0
  79. package/dist/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
  80. package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +12 -5
  81. package/dist/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
  82. package/dist/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
  83. package/dist/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +9 -6
  84. package/dist/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
  85. package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
  86. package/dist/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
  87. package/dist/release-manifest.json +4 -4
  88. package/dist/src-B5OTAZKM.js +1 -0
  89. package/package.json +7 -6
  90. package/plugin/.claude-plugin/plugin.json +1 -1
  91. package/plugin/.cursor-plugin/plugin.json +1 -1
  92. package/plugin/README.md +19 -13
  93. package/plugin/agents/test-prepare-runner.md +4 -1
  94. package/plugin/agents/visual-walkthrough-builder.md +12 -5
  95. package/plugin/commands/mtest.md +1 -1
  96. package/plugin/hooks/README.md +15 -3
  97. package/plugin/hooks/hooks.json +79 -0
  98. package/plugin/scripts/ensure-electron-app.sh +62 -47
  99. package/plugin/scripts/guardrail-classify-gate.sh +23 -0
  100. package/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
  101. package/plugin/scripts/guardrail-pr-terminal.sh +6 -2
  102. package/plugin/scripts/guardrail-record-stage-read.sh +21 -0
  103. package/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
  104. package/plugin/scripts/guardrail-record-tests.sh +11 -4
  105. package/plugin/scripts/guardrail-report-format.sh +9 -5
  106. package/plugin/scripts/guardrail-skill-stages.sh +35 -0
  107. package/plugin/scripts/guardrail-stage-gate.sh +43 -0
  108. package/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
  109. package/plugin/scripts/guardrails.mjs +474 -80
  110. package/plugin/scripts/pr-watch-events.sh +116 -0
  111. package/plugin/scripts/pr-watch-guards.sh +8 -2
  112. package/plugin/scripts/pr-watch-loop.sh +254 -0
  113. package/plugin/scripts/reconcile-stale-watchers.sh +66 -18
  114. package/plugin/scripts/sign-body.sh +88 -0
  115. package/plugin/skills/CLAUDE.md +16 -0
  116. package/plugin/skills/_shared/post-merge-cleanup.md +83 -3
  117. package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
  118. package/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
  119. package/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
  120. package/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
  121. package/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
  122. package/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
  123. package/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
  124. package/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
  125. package/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
  126. package/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
  127. package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
  128. package/plugin/skills/_shared/vcs/post-signature.md +23 -12
  129. package/plugin/skills/do/open-prs/forward.md +2 -2
  130. package/plugin/skills/do/per-comment-replies.md +2 -15
  131. package/plugin/skills/muggle/SKILL.md +1 -1
  132. package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
  133. package/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
  134. package/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
  135. package/plugin/skills/muggle-pr-followup/adopt.md +60 -0
  136. package/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
  137. package/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
  138. package/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
  139. package/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
  140. package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
  141. package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
  142. package/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
  143. package/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
  144. package/plugin/skills/muggle-preferences/SKILL.md +1 -1
  145. package/plugin/skills/muggle-preferences/evals/evals.json +45 -0
  146. package/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
  147. package/plugin/skills/muggle-preferences/ops/configure.md +4 -5
  148. package/plugin/skills/muggle-preferences/ops/list.md +1 -1
  149. package/plugin/skills/muggle-preferences/ops/set.md +3 -4
  150. package/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
  151. package/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
  152. package/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
  153. package/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
  154. package/plugin/skills/muggle-test/SKILL.md +23 -3
  155. package/plugin/skills/muggle-test/execute-local.md +2 -0
  156. package/plugin/skills/muggle-test-feature-local/SKILL.md +6 -1
  157. package/plugin/skills/muggle-test-import/SKILL.md +1 -1
  158. package/plugin/skills/muggle-test-prepare/SKILL.md +28 -15
  159. package/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
  160. package/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
  161. package/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
  162. package/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +111 -0
  163. package/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
  164. package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +12 -5
  165. package/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
  166. package/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
  167. package/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +9 -6
  168. package/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
  169. package/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
  170. package/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
  171. package/dist/src-FDK2KQSZ.js +0 -1
@@ -1,6 +1,8 @@
1
- import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs';
2
- import { isAbsolute, resolve, join } from 'path';
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
2
+ import { join, isAbsolute, resolve, dirname } from 'path';
3
3
  import { homedir } from 'os';
4
+ import { fileURLToPath } from 'url';
5
+ import { execFileSync } from 'child_process';
4
6
 
5
7
  // src/guardrails/cli.ts
6
8
  var baseDir = (override) => override ?? join(homedir(), ".muggle-ai", "guardrails");
@@ -43,11 +45,27 @@ ${input2.tool_response?.output ?? ""}`;
43
45
  // src/guardrails/constants.ts
44
46
  var GH_PR_MERGED_LINE = /\b(?:Merged|Squashed and merged|Rebased and merged) pull request [\w./-]*#(\d+)/;
45
47
  var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
48
+ var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
46
49
  var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
47
50
  var MAX_PR_TERMINAL_BLOCKS = 3;
48
51
  var MAX_WATCH_BLOCKS = 3;
52
+ var MAX_WALKTHROUGH_BLOCKS = 3;
53
+ var GH_LOOKUP_TIMEOUT_MS = 1e4;
49
54
  var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
50
55
  var MUGGLE_TEST_SKILL_NAME = "muggle-test";
56
+ var MANDATORY_STAGES_FRONTMATTER_KEY = "mandatoryStages";
57
+ var SKILL_NAME_INPUT_KEYS = ["skill", "skillName", "name", "command"];
58
+ var MAX_STAGE_BLOCKS = 3;
59
+ var MAX_DEBUG_BLOCKS = 3;
60
+ var ANY_TEST_CASE = "*";
61
+ var MUGGLE_EXECUTION_TOOL = /muggle-local-(execute-test-generation|execute-replay)/i;
62
+ var MUGGLE_EVENT_EMIT_TOOL = /muggle-local-telemetry-event-emit/i;
63
+ var MUGGLE_FEEDBACK_CREATE_TOOL = /muggle-remote-user-feedback-create/i;
64
+ var PRE_EXECUTION_CLASSIFICATION_EVENT = "pre-execution-classification";
65
+ var FAILURE_DIAGNOSIS_EVENT = /-failure-(classified|resolved)$/;
66
+ var MUGGLE_RUN_ID_LINE = /\*\*Run ID:\*\*\s*([^\s*]+)/;
67
+ var MUGGLE_RUN_STATUS_LINE = /\*\*Status:\*\*\s*([A-Za-z_]+)/;
68
+ var MUGGLE_RUN_PASSED_STATUS = "passed";
51
69
 
52
70
  // src/guardrails/prTerminal.ts
53
71
  function detectPrTerminal(input2) {
@@ -71,6 +89,23 @@ function detectPrTerminal(input2) {
71
89
  }
72
90
  return null;
73
91
  }
92
+ function detectPrReopened(input2) {
93
+ if (input2.tool_name !== "Bash") return null;
94
+ const response = input2.tool_response;
95
+ const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
96
+ const reopenedMatch = haystack.match(GH_PR_REOPENED_LINE);
97
+ return reopenedMatch ? Number(reopenedMatch[1]) : null;
98
+ }
99
+ function applyPrReopened(state, prNumber) {
100
+ const pending = state.terminalPending ?? [];
101
+ const handled = state.terminalHandled ?? [];
102
+ if (!pending.includes(prNumber) && !handled.includes(prNumber)) return state;
103
+ return {
104
+ ...state,
105
+ terminalPending: pending.filter((number) => number !== prNumber),
106
+ terminalHandled: handled.filter((number) => number !== prNumber)
107
+ };
108
+ }
74
109
  function applyPrTerminalDetected(state, prNumber) {
75
110
  const pending = state.terminalPending ?? [];
76
111
  const handled = state.terminalHandled ?? [];
@@ -97,6 +132,207 @@ function prTerminalGateDecision(state, maxBlocks = MAX_PR_TERMINAL_BLOCKS) {
97
132
  }
98
133
  return { action: "block" /* Block */, blockCount: blockCount + 1 };
99
134
  }
135
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
136
+ var SEQUENCE_ENTRY = /^\s*-\s*(.+?)\s*$/;
137
+ var FLOW_LIST = /^\s*\[(.*)\]\s*$/;
138
+ var STAGE_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_STAGE_SKIP\b/;
139
+ var SKILLS_DIR_NAME = "skills";
140
+ var SKILLS_PATH_SEGMENT = `/${SKILLS_DIR_NAME}/`;
141
+ var SINGLE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;
142
+ var unquote = (raw) => raw.trim().replace(/^["']|["']$/g, "").trim();
143
+ function parseDeclaredStages(skillMarkdown) {
144
+ const frontmatter = FRONTMATTER.exec(skillMarkdown)?.[1];
145
+ if (!frontmatter) return [];
146
+ const lines = frontmatter.split(/\r?\n/);
147
+ const keyIndex = lines.findIndex(
148
+ (line) => line.startsWith(`${MANDATORY_STAGES_FRONTMATTER_KEY}:`)
149
+ );
150
+ if (keyIndex < 0) return [];
151
+ const inlineValue = lines[keyIndex].slice(MANDATORY_STAGES_FRONTMATTER_KEY.length + 1).trim();
152
+ const flowEntries = FLOW_LIST.exec(inlineValue)?.[1];
153
+ if (flowEntries !== void 0) {
154
+ return flowEntries.split(",").map(unquote).filter((entry) => entry.length > 0);
155
+ }
156
+ const declared = [];
157
+ for (const line of lines.slice(keyIndex + 1)) {
158
+ const entry = SEQUENCE_ENTRY.exec(line);
159
+ if (!entry) break;
160
+ declared.push(unquote(entry[1]));
161
+ }
162
+ return declared;
163
+ }
164
+ function normalizeStagePath(filePath) {
165
+ return filePath.replaceAll("\\", "/").toLowerCase();
166
+ }
167
+ function stageLabel(normalizedStagePath) {
168
+ const insideSkills = normalizedStagePath.lastIndexOf(SKILLS_PATH_SEGMENT);
169
+ if (insideSkills < 0) return normalizedStagePath;
170
+ return normalizedStagePath.slice(insideSkills + SKILLS_PATH_SEGMENT.length);
171
+ }
172
+ function resolvePluginSkillsRoot() {
173
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || process.env.CURSOR_PLUGIN_ROOT;
174
+ if (pluginRoot) return join(pluginRoot, SKILLS_DIR_NAME);
175
+ return join(dirname(dirname(fileURLToPath(import.meta.url))), SKILLS_DIR_NAME);
176
+ }
177
+ function resolveSkillNameFromToolInput(toolInput) {
178
+ if (!toolInput) return void 0;
179
+ const indexed = toolInput;
180
+ for (const inputKey of SKILL_NAME_INPUT_KEYS) {
181
+ const value = indexed[inputKey];
182
+ if (typeof value !== "string" || value.trim().length === 0) continue;
183
+ const skillName = value.trim().split(":").pop()?.replace(/^\//, "") ?? "";
184
+ return SINGLE_PATH_SEGMENT.test(skillName) ? skillName : void 0;
185
+ }
186
+ return void 0;
187
+ }
188
+ function resolveSkillStagePaths(skillName, skillsRootDir) {
189
+ const skillDir = join(skillsRootDir, skillName);
190
+ const skillFile = join(skillDir, "SKILL.md");
191
+ if (!existsSync(skillFile)) return [];
192
+ const normalizedRoot = normalizeStagePath(skillsRootDir);
193
+ const staysInsideSkillsTree = (candidate) => normalizeStagePath(candidate).startsWith(`${normalizedRoot}/`);
194
+ const resolved = [];
195
+ for (const declared of parseDeclaredStages(readFileSync(skillFile, "utf-8"))) {
196
+ if (isAbsolute(declared)) continue;
197
+ const stagePath = [resolve(skillDir, declared), resolve(skillsRootDir, declared)].find(
198
+ (candidate) => staysInsideSkillsTree(candidate) && existsSync(candidate)
199
+ );
200
+ if (!stagePath) continue;
201
+ const normalized = normalizeStagePath(stagePath);
202
+ if (!resolved.includes(normalized)) resolved.push(normalized);
203
+ }
204
+ return resolved;
205
+ }
206
+ function applySkillInvocation(state, skillName, stagePaths) {
207
+ const merged = [...state.mandatoryStages ?? []];
208
+ for (const stagePath of stagePaths) if (!merged.includes(stagePath)) merged.push(stagePath);
209
+ if (state.lastInvokedSkillName === skillName && merged.length === (state.mandatoryStages?.length ?? 0)) {
210
+ return state;
211
+ }
212
+ return { ...state, lastInvokedSkillName: skillName, mandatoryStages: merged };
213
+ }
214
+ function applyStageRead(state, filePath) {
215
+ const stagePath = normalizeStagePath(filePath);
216
+ if ((state.stagesRead ?? []).includes(stagePath)) return state;
217
+ return { ...state, stagesRead: [...state.stagesRead ?? [], stagePath] };
218
+ }
219
+ function unreadMandatoryStages(state) {
220
+ const read = new Set(state.stagesRead ?? []);
221
+ return (state.mandatoryStages ?? []).filter((stagePath) => !read.has(stagePath));
222
+ }
223
+ function isStageSkipMarker(command) {
224
+ return STAGE_SKIP_MARKER.test(command);
225
+ }
226
+ function applyStageSkip(state, skipped) {
227
+ if (!skipped || state.stageSkipped === true) return state;
228
+ return { ...state, stageSkipped: true };
229
+ }
230
+ function stageGateDecision(state, unreadStagePaths, maxBlocks = MAX_STAGE_BLOCKS) {
231
+ const blockCount = state.stageBlockCount ?? 0;
232
+ if (state.stageSkipped === true || unreadStagePaths.length === 0) {
233
+ return { action: "none" /* None */, blockCount, unread: unreadStagePaths };
234
+ }
235
+ if (blockCount >= maxBlocks) {
236
+ return { action: "release" /* Release */, blockCount, unread: unreadStagePaths };
237
+ }
238
+ return { action: "block" /* Block */, blockCount: blockCount + 1, unread: unreadStagePaths };
239
+ }
240
+
241
+ // src/guardrails/preExecutionClassification.ts
242
+ var CLASSIFICATION_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_CLASSIFY_SKIP\b/;
243
+ function detectClassifiedTestCaseId(input2) {
244
+ if (!MUGGLE_EVENT_EMIT_TOOL.test(input2.tool_name ?? "")) return void 0;
245
+ if (input2.tool_input?.eventType !== PRE_EXECUTION_CLASSIFICATION_EVENT) return void 0;
246
+ return input2.tool_input?.testCaseId ?? ANY_TEST_CASE;
247
+ }
248
+ function applyClassifiedTestCase(state, testCaseId) {
249
+ if ((state.classifiedTestCaseIds ?? []).includes(testCaseId)) return state;
250
+ return { ...state, classifiedTestCaseIds: [...state.classifiedTestCaseIds ?? [], testCaseId] };
251
+ }
252
+ function resolveExecutionTargetTestCaseId(input2) {
253
+ if (!MUGGLE_EXECUTION_TOOL.test(input2.tool_name ?? "")) return void 0;
254
+ return input2.tool_input?.testCase?.id ?? input2.tool_input?.testScript?.testCaseId;
255
+ }
256
+ function isClassificationSkipMarker(command) {
257
+ return CLASSIFICATION_SKIP_MARKER.test(command);
258
+ }
259
+ function applyClassificationSkip(state, skipped) {
260
+ if (!skipped || state.classificationSkipped === true) return state;
261
+ return { ...state, classificationSkipped: true };
262
+ }
263
+ function classificationGateDecision(state, input2) {
264
+ if (state.lastInvokedSkillName !== MUGGLE_TEST_SKILL_NAME) return { deny: false };
265
+ if (state.classificationSkipped === true) return { deny: false };
266
+ const testCaseId = resolveExecutionTargetTestCaseId(input2);
267
+ if (!testCaseId) return { deny: false };
268
+ const classified = state.classifiedTestCaseIds ?? [];
269
+ if (classified.includes(ANY_TEST_CASE) || classified.includes(testCaseId)) return { deny: false };
270
+ return { deny: true, testCaseId };
271
+ }
272
+
273
+ // src/guardrails/debugPath.ts
274
+ var DEBUG_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_DEBUG_SKIP\b/;
275
+ var serialize = (input2) => `${JSON.stringify(input2.tool_input ?? {})}
276
+ ${JSON.stringify(input2.tool_response ?? {})}`;
277
+ function renderedResult(toolResponse) {
278
+ const rendered = [];
279
+ const collect = (value) => {
280
+ if (typeof value === "string") rendered.push(value);
281
+ else if (Array.isArray(value)) value.forEach(collect);
282
+ else if (value && typeof value === "object") Object.values(value).forEach(collect);
283
+ };
284
+ collect(toolResponse);
285
+ return rendered.join("\n");
286
+ }
287
+ function detectFailedRunId(input2) {
288
+ if (!MUGGLE_EXECUTION_TOOL.test(input2.tool_name ?? "")) return void 0;
289
+ const rendered = renderedResult(input2.tool_response);
290
+ const status = MUGGLE_RUN_STATUS_LINE.exec(rendered)?.[1];
291
+ if (!status || status.toLowerCase() === MUGGLE_RUN_PASSED_STATUS) return void 0;
292
+ return MUGGLE_RUN_ID_LINE.exec(rendered)?.[1];
293
+ }
294
+ function applyFailedRun(state, runId) {
295
+ if ((state.failedRuns ?? []).includes(runId)) return state;
296
+ return { ...state, failedRuns: [...state.failedRuns ?? [], runId] };
297
+ }
298
+ function detectDebugEvidenceRunIds(input2, owedRunIds) {
299
+ const toolName = input2.tool_name ?? "";
300
+ const isDiagnosisEmit = MUGGLE_EVENT_EMIT_TOOL.test(toolName) && FAILURE_DIAGNOSIS_EVENT.test(input2.tool_input?.eventType ?? "");
301
+ if (!isDiagnosisEmit && !MUGGLE_FEEDBACK_CREATE_TOOL.test(toolName)) return [];
302
+ const payload = serialize(input2);
303
+ return owedRunIds.filter((runId) => payload.includes(runId));
304
+ }
305
+ function applyDebugEvidence(state, runIds) {
306
+ const debugged = [...state.debuggedRuns ?? []];
307
+ for (const runId of runIds) if (!debugged.includes(runId)) debugged.push(runId);
308
+ if (debugged.length === (state.debuggedRuns ?? []).length) return state;
309
+ return { ...state, debuggedRuns: debugged };
310
+ }
311
+ function isDebugSkipMarker(command) {
312
+ return DEBUG_SKIP_MARKER.test(command);
313
+ }
314
+ function applyDebugSkip(state, command) {
315
+ if (!isDebugSkipMarker(command)) return state;
316
+ const namedRuns = (state.failedRuns ?? []).filter((runId) => command.includes(runId));
317
+ if (namedRuns.length > 0) return applyDebugEvidence(state, namedRuns);
318
+ if (state.debugSkipped === true) return state;
319
+ return { ...state, debugSkipped: true };
320
+ }
321
+ function undebuggedFailedRuns(state) {
322
+ const debugged = new Set(state.debuggedRuns ?? []);
323
+ return (state.failedRuns ?? []).filter((runId) => !debugged.has(runId));
324
+ }
325
+ function debugGateDecision(state, maxBlocks = MAX_DEBUG_BLOCKS) {
326
+ const blockCount = state.debugBlockCount ?? 0;
327
+ const undebugged = undebuggedFailedRuns(state);
328
+ if (state.debugSkipped === true || undebugged.length === 0) {
329
+ return { action: "none" /* None */, blockCount, undebugged };
330
+ }
331
+ if (blockCount >= maxBlocks) {
332
+ return { action: "release" /* Release */, blockCount, undebugged };
333
+ }
334
+ return { action: "block" /* Block */, blockCount: blockCount + 1, undebugged };
335
+ }
100
336
 
101
337
  // src/guardrails/testsGreen.ts
102
338
  var TEST_CMD = /\b(pnpm|npm|yarn)\s+(run\s+)?test\b|\b(jest|vitest|pytest)\b|\bgo\s+test\b|\bcargo\s+test\b/;
@@ -144,7 +380,6 @@ function e2eGateDecision(state, maxBlocks = MAX_E2E_BLOCKS) {
144
380
  if (blockCount >= maxBlocks) return { action: "release" /* Release */, blockCount };
145
381
  return { action: "block" /* Block */, blockCount: blockCount + 1 };
146
382
  }
147
- var HEARTBEAT_FRESH_MS = 15 * 60 * 1e3;
148
383
  var WATCH_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_WATCH_SKIP\b/;
149
384
  function isWatchSkipMarker(cmd) {
150
385
  return WATCH_SKIP_MARKER.test(cmd);
@@ -153,38 +388,13 @@ function applyWatchSkip(state, skipped) {
153
388
  if (!skipped || state.watchSkipped === true) return state;
154
389
  return { ...state, watchSkipped: true };
155
390
  }
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) {
391
+ function findUntrackedHandledPrs(handledUrls, sessionsDirOverride) {
181
392
  if (handledUrls.length === 0) return [];
182
393
  const sessionsDir = join(homedir(), ".muggle-ai", "muggle-do", "sessions");
183
394
  if (!existsSync(sessionsDir)) return [...handledUrls];
184
- const watchedUrls = /* @__PURE__ */ new Set();
395
+ const trackedUrls = /* @__PURE__ */ new Set();
185
396
  for (const slug of readdirSync(sessionsDir)) {
186
- const slotDir = join(sessionsDir, slug);
187
- const prsFile = join(slotDir, "prs.json");
397
+ const prsFile = join(sessionsDir, slug, "prs.json");
188
398
  if (!existsSync(prsFile)) continue;
189
399
  let slotUrl;
190
400
  try {
@@ -194,36 +404,30 @@ function findUnarmedHandledPrs(handledUrls, sessionsDirOverride) {
194
404
  } catch {
195
405
  continue;
196
406
  }
197
- if (slotUrl && handledUrls.includes(slotUrl) && slotHasArmedWatcher(slotDir)) {
198
- watchedUrls.add(slotUrl);
199
- }
407
+ if (slotUrl) trackedUrls.add(slotUrl);
200
408
  }
201
- return handledUrls.filter((url) => !watchedUrls.has(url));
409
+ return handledUrls.filter((url) => !trackedUrls.has(url));
202
410
  }
203
- function watchGateDecision(state, owedUrls, maxBlocks = MAX_WATCH_BLOCKS) {
411
+ function watchGateDecision(state, untrackedPrUrls, maxBlocks = MAX_WATCH_BLOCKS) {
204
412
  const blockCount = state.watchBlockCount ?? 0;
205
- if (state.watchSkipped === true || owedUrls.length === 0) {
206
- return { action: "none" /* None */, blockCount, owed: owedUrls };
413
+ if (state.watchSkipped === true || untrackedPrUrls.length === 0) {
414
+ return { action: "none" /* None */, blockCount, untracked: untrackedPrUrls };
207
415
  }
208
416
  if (blockCount >= maxBlocks) {
209
- return { action: "release" /* Release */, blockCount, owed: owedUrls };
417
+ return { action: "release" /* Release */, blockCount, untracked: untrackedPrUrls };
210
418
  }
211
- return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedUrls };
212
- }
213
-
214
- // src/guardrails/detectBuildIntent.ts
215
- var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
216
- var DEVCYCLE = /\bresolve\b[^.?!]{0,40}\bconflicts?\b|\bget\b[^.?!]{0,40}\bpr\b[^.?!]{0,40}\b(green|merged?|passing)\b/i;
217
- var QUESTION = /^\s*(why|what|how|when|where|who|is|are|does|do|can you (explain|tell)|explain)\b/i;
218
- function detectBuildIntent(prompt) {
219
- const p = (prompt ?? "").trim();
220
- if (!p || p.startsWith("/")) return false;
221
- if (QUESTION.test(p)) return false;
222
- return BUILD.test(p) || DEVCYCLE.test(p);
419
+ return {
420
+ action: "block" /* Block */,
421
+ blockCount: blockCount + 1,
422
+ untracked: untrackedPrUrls
423
+ };
223
424
  }
224
425
  var REPORT_SENTINEL = "muggle-pr-section";
225
- var PR_POST_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
226
- var defaultReader = (path, cwd) => {
426
+ var PR_PROSE_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
427
+ var GH_API_CMD = /\bgh\s+api\b/;
428
+ var ISSUE_COMMENT_PATH = /\bissues\/comments\/\d+/;
429
+ var PATCH_METHOD = /(?:--method|-X)[=\s]+PATCH\b/;
430
+ var defaultFileReader = (path, cwd) => {
227
431
  try {
228
432
  const abs = isAbsolute(path) ? path : resolve(cwd ?? process.cwd(), path);
229
433
  if (!existsSync(abs)) return null;
@@ -232,13 +436,115 @@ var defaultReader = (path, cwd) => {
232
436
  return null;
233
437
  }
234
438
  };
235
- function unquote(s) {
439
+ function unquote2(s) {
236
440
  const t = s.trim();
237
441
  if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
238
442
  return t.slice(1, -1);
239
443
  }
240
444
  return t;
241
445
  }
446
+ function isCommentEditCommand(cmd) {
447
+ return GH_API_CMD.test(cmd) && ISSUE_COMMENT_PATH.test(cmd) && PATCH_METHOD.test(cmd);
448
+ }
449
+ function isPrReportPostCommand(cmd) {
450
+ return PR_PROSE_CMD.test(cmd) || isCommentEditCommand(cmd);
451
+ }
452
+ function collectPrPostText(cmd, cwd, read) {
453
+ let text = cmd;
454
+ for (const match of cmd.matchAll(/(?:--body-file|--input)[=\s]+("[^"]+"|'[^']+'|\S+)/g)) {
455
+ const path = unquote2(match[1]);
456
+ if (path && path !== "-") {
457
+ const contents = read(path, cwd);
458
+ if (contents) text += "\n" + contents;
459
+ }
460
+ }
461
+ for (const match of cmd.matchAll(/jq\b[^|]*?("[^"]+\.json"|'[^']+\.json'|\S+\.json)/g)) {
462
+ const contents = read(unquote2(match[1]), cwd);
463
+ if (contents) text += "\n" + contents;
464
+ }
465
+ return text;
466
+ }
467
+
468
+ // src/guardrails/walkthroughPosted.ts
469
+ var WALKTHROUGH_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_WALKTHROUGH_SKIP\b/;
470
+ function isWalkthroughSkipMarker(cmd) {
471
+ return WALKTHROUGH_SKIP_MARKER.test(cmd);
472
+ }
473
+ function detectWalkthroughPost(input2, read = defaultFileReader) {
474
+ if (input2.tool_name !== "Bash") return false;
475
+ const cmd = input2.tool_input?.command ?? "";
476
+ if (!isPrReportPostCommand(cmd)) return false;
477
+ return collectPrPostText(cmd, input2.cwd, read).includes(REPORT_SENTINEL);
478
+ }
479
+ function applyWalkthroughPosted(state, posted) {
480
+ if (!posted || state.walkthroughPosted === true) return state;
481
+ return { ...state, walkthroughPosted: true };
482
+ }
483
+ function applyWalkthroughSkip(state, skipped) {
484
+ if (!skipped || state.walkthroughSkipped === true) return state;
485
+ return { ...state, walkthroughSkipped: true };
486
+ }
487
+ function runGh(args) {
488
+ try {
489
+ return execFileSync("gh", args, {
490
+ encoding: "utf-8",
491
+ timeout: GH_LOOKUP_TIMEOUT_MS,
492
+ stdio: ["ignore", "pipe", "ignore"]
493
+ });
494
+ } catch {
495
+ return null;
496
+ }
497
+ }
498
+ var defaultPrWalkthroughLookup = {
499
+ branchPrUrl: () => {
500
+ const url = runGh(["pr", "view", "--json", "url", "-q", ".url"])?.trim();
501
+ return url && url.startsWith("http") ? url : null;
502
+ },
503
+ // Scans the description as well as the comments: muggle-do embeds the
504
+ // walkthrough in the body at PR-creation time, and a comments-only check
505
+ // would report that PR as owing one it already carries.
506
+ prCarriesWalkthrough: (prUrl) => {
507
+ const rendered = runGh(["pr", "view", prUrl, "--json", "body,comments"]);
508
+ if (rendered === null) return true;
509
+ return rendered.includes(REPORT_SENTINEL);
510
+ }
511
+ };
512
+ function scanForOwedWalkthroughs(state, lookup = defaultPrWalkthroughLookup) {
513
+ const candidates = new Set(state.prsHandled);
514
+ const branchPrUrl = lookup.branchPrUrl();
515
+ if (branchPrUrl) candidates.add(branchPrUrl);
516
+ const owed = [];
517
+ const verified = [];
518
+ for (const prUrl of candidates) {
519
+ if (lookup.prCarriesWalkthrough(prUrl)) verified.push(prUrl);
520
+ else owed.push(prUrl);
521
+ }
522
+ return { owed, verified };
523
+ }
524
+ function walkthroughGateDecision(state, owedPrUrls, maxBlocks = MAX_WALKTHROUGH_BLOCKS) {
525
+ const blockCount = state.walkthroughBlockCount ?? 0;
526
+ const alreadySettled = state.walkthroughPosted === true || state.walkthroughSkipped === true || state.e2eRun !== true;
527
+ if (alreadySettled || owedPrUrls.length === 0) {
528
+ return { action: "none" /* None */, blockCount, owed: owedPrUrls };
529
+ }
530
+ if (blockCount >= maxBlocks) {
531
+ return { action: "release" /* Release */, blockCount, owed: owedPrUrls };
532
+ }
533
+ return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedPrUrls };
534
+ }
535
+
536
+ // src/guardrails/detectBuildIntent.ts
537
+ var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
538
+ var DEVCYCLE = /\bresolve\b[^.?!]{0,40}\bconflicts?\b|\bget\b[^.?!]{0,40}\bpr\b[^.?!]{0,40}\b(green|merged?|passing)\b/i;
539
+ var QUESTION = /^\s*(why|what|how|when|where|who|is|are|does|do|can you (explain|tell)|explain)\b/i;
540
+ function detectBuildIntent(prompt) {
541
+ const p = (prompt ?? "").trim();
542
+ if (!p || p.startsWith("/")) return false;
543
+ if (QUESTION.test(p)) return false;
544
+ return BUILD.test(p) || DEVCYCLE.test(p);
545
+ }
546
+
547
+ // src/guardrails/reportGate.ts
242
548
  function looksLikeE2EReport(text) {
243
549
  const t = text.toLowerCase();
244
550
  const statusEmojis = (text.match(/[✅❌⚠]/gu) ?? []).length;
@@ -248,26 +554,11 @@ function looksLikeE2EReport(text) {
248
554
  const muggleContext = /\bmuggle\b/.test(t) || /muggle-ai\.com/.test(t) || /\be2e\b/.test(t) || /\bacceptance\b/.test(t);
249
555
  return resultsStructure && muggleContext;
250
556
  }
251
- function collectInspectableText(cmd, cwd, read) {
252
- let text = cmd;
253
- for (const m of cmd.matchAll(/--body-file[=\s]+("[^"]+"|'[^']+'|\S+)/g)) {
254
- const p = unquote(m[1]);
255
- if (p && p !== "-") {
256
- const c = read(p, cwd);
257
- if (c) text += "\n" + c;
258
- }
259
- }
260
- for (const m of cmd.matchAll(/jq\b[^|]*?("[^"]+\.json"|'[^']+\.json'|\S+\.json)/g)) {
261
- const c = read(unquote(m[1]), cwd);
262
- if (c) text += "\n" + c;
263
- }
264
- return text;
265
- }
266
- function evaluateReportPost(input2, read = defaultReader) {
557
+ function evaluateReportPost(input2, read = defaultFileReader) {
267
558
  if (input2.tool_name !== "Bash") return { deny: false };
268
559
  const cmd = input2.tool_input?.command ?? "";
269
- if (!PR_POST_CMD.test(cmd)) return { deny: false };
270
- const text = collectInspectableText(cmd, input2.cwd, read);
560
+ if (!isPrReportPostCommand(cmd)) return { deny: false };
561
+ const text = collectPrPostText(cmd, input2.cwd, read);
271
562
  if (text.includes(REPORT_SENTINEL)) return { deny: false };
272
563
  if (!looksLikeE2EReport(text)) return { deny: false };
273
564
  return {
@@ -323,6 +614,13 @@ Per the autoWatchPR preference, a muggle-pr-followup watcher should handle its i
323
614
  return envelope("PostToolUse", ctx, host);
324
615
  }
325
616
  function prTerminal() {
617
+ const reopenedPrNumber = detectPrReopened(input);
618
+ if (reopenedPrNumber !== null) {
619
+ const state2 = readState(sessionId);
620
+ const next2 = applyPrReopened(state2, reopenedPrNumber);
621
+ if (next2 !== state2) writeState(next2);
622
+ return "{}";
623
+ }
326
624
  const terminalEvent = detectPrTerminal(input);
327
625
  if (!terminalEvent) return "{}";
328
626
  const state = readState(sessionId);
@@ -357,10 +655,81 @@ function recordTests() {
357
655
  e2eRan: isE2ERun(input),
358
656
  e2eSkipped: isE2ESkipMarker(cmd)
359
657
  });
360
- const next = applyWatchSkip(recorded, isWatchSkipMarker(cmd));
658
+ const withWatchSkip = applyWatchSkip(recorded, isWatchSkipMarker(cmd));
659
+ const withWalkthroughPost = applyWalkthroughPosted(withWatchSkip, detectWalkthroughPost(input));
660
+ const withWalkthroughSkip = applyWalkthroughSkip(withWalkthroughPost, isWalkthroughSkipMarker(cmd));
661
+ const failedRunId = detectFailedRunId(input);
662
+ const next = failedRunId ? applyFailedRun(withWalkthroughSkip, failedRunId) : withWalkthroughSkip;
361
663
  if (next !== state) writeState(next);
362
664
  return "{}";
363
665
  }
666
+ function skillStages() {
667
+ const skillName = resolveSkillNameFromToolInput(input.tool_input);
668
+ if (!skillName) return "{}";
669
+ const state = readState(sessionId);
670
+ const next = applySkillInvocation(
671
+ state,
672
+ skillName,
673
+ resolveSkillStagePaths(skillName, resolvePluginSkillsRoot())
674
+ );
675
+ if (next !== state) writeState(next);
676
+ const unread = unreadMandatoryStages(next);
677
+ if (unread.length === 0) return "{}";
678
+ const ctx = `The ${skillName} skill declares required reading: ${unread.map(stageLabel).join(", ")}. Read those files now, before working through the skill's steps \u2014 they carry mandatory steps SKILL.md only links to, and treating them as optional elaboration is how those steps get silently dropped. A Stop gate holds the turn open until they are opened.`;
679
+ return envelope("PostToolUse", ctx, host);
680
+ }
681
+ function recordStageRead() {
682
+ const filePath = input.tool_input?.file_path;
683
+ if (!filePath) return "{}";
684
+ const state = readState(sessionId);
685
+ const next = applyStageRead(state, filePath);
686
+ if (next !== state) writeState(next);
687
+ return "{}";
688
+ }
689
+ function recordStageSignals() {
690
+ const cmd = input.tool_input?.command ?? "";
691
+ const state = readState(sessionId);
692
+ const withStageSkip = applyStageSkip(state, isStageSkipMarker(cmd));
693
+ const withClassificationSkip = applyClassificationSkip(
694
+ withStageSkip,
695
+ isClassificationSkipMarker(cmd)
696
+ );
697
+ const withDebugSkip = applyDebugSkip(withClassificationSkip, cmd);
698
+ const classifiedTestCaseId = detectClassifiedTestCaseId(input);
699
+ const withClassification = classifiedTestCaseId ? applyClassifiedTestCase(withDebugSkip, classifiedTestCaseId) : withDebugSkip;
700
+ const next = applyDebugEvidence(
701
+ withClassification,
702
+ detectDebugEvidenceRunIds(input, undebuggedFailedRuns(withClassification))
703
+ );
704
+ if (next !== state) writeState(next);
705
+ return "{}";
706
+ }
707
+ function classifyGate() {
708
+ const decision = classificationGateDecision(readState(sessionId), input);
709
+ if (!decision.deny) return "{}";
710
+ const reason = `Test case ${decision.testCaseId} has no pre-execution classification this session. Run muggle-test Step 6f for it first: classify replay-vs-regen per _shared/failure-mode-handling.md \xA7A, then emit one muggle-local-telemetry-event-emit with eventType "pre-execution-classification" for this test case. That step is what calls muggle-remote-test-script-list, which is the only place this run learns the test case has never passed or has failed repeatedly \u2014 cheap now, ~5 minutes of browser time to rediscover after the fact. If this execution genuinely has no classification step (a single user-picked target), say why and run \`echo "MUGGLE_CLASSIFY_SKIP: <reason>"\` \u2014 that records the skip for the rest of the session.`;
711
+ return denyTool(reason, host);
712
+ }
713
+ function stageGate() {
714
+ const state = readState(sessionId);
715
+ const decision = stageGateDecision(state, unreadMandatoryStages(state));
716
+ if (decision.action !== "block" /* Block */) return "{}";
717
+ state.stageBlockCount = decision.blockCount;
718
+ writeState(state);
719
+ const stageList = decision.unread.map(stageLabel).join(", ");
720
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. A skill invoked this session declares mandatory stages that were never opened: ${stageList}. Read them and carry out what they require \u2014 they are steps, not background reading, and SKILL.md only links to them. If they genuinely do not apply to this run, say why and run \`echo "MUGGLE_STAGE_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Mandatory stages still unread (reminder ${decision.blockCount}/${MAX_STAGE_BLOCKS}): ${stageList}. Read them, or record a legitimate skip via \`echo "MUGGLE_STAGE_SKIP: <reason>"\`.`;
721
+ return blockStop(reason, host);
722
+ }
723
+ function debugPathGate() {
724
+ const state = readState(sessionId);
725
+ const decision = debugGateDecision(state);
726
+ if (decision.action !== "block" /* Block */) return "{}";
727
+ state.debugBlockCount = decision.blockCount;
728
+ writeState(state);
729
+ const runList = decision.undebugged.join(", ");
730
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. These runs failed and never went through the debug path: ${runList}. muggle-test Step 7C makes that mandatory \u2014 route each through _shared/debug-failed-run.md: gather the attempted steps and the failing screenshot, diagnose the bucket per _shared/failure-mode-handling.md \xA7B/\xA7C with its classified telemetry emit, and present the offer in which "give feedback & rerun" is always available. A summarized-and-dropped failure is the run a reviewer most needs to see. If a run genuinely cannot be debugged, run \`echo "MUGGLE_DEBUG_SKIP: <runId> <reason>"\` \u2014 that clears just that run.` : `Failed runs still owe the debug path (reminder ${decision.blockCount}/${MAX_DEBUG_BLOCKS}): ${runList}. Route each through _shared/debug-failed-run.md, or record a legitimate skip via \`echo "MUGGLE_DEBUG_SKIP: <runId> <reason>"\`.`;
731
+ return blockStop(reason, host);
732
+ }
364
733
  function e2eGate() {
365
734
  const state = readState(sessionId);
366
735
  const decision = e2eGateDecision(state);
@@ -372,21 +741,39 @@ function e2eGate() {
372
741
  }
373
742
  function watchGate() {
374
743
  const state = readState(sessionId);
375
- const owed = findUnarmedHandledPrs(state.prsHandled);
376
- const decision = watchGateDecision(state, owed);
744
+ const untrackedPrUrls = findUntrackedHandledPrs(state.prsHandled);
745
+ const decision = watchGateDecision(state, untrackedPrUrls);
377
746
  if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) {
378
747
  return "{}";
379
748
  }
380
749
  state.watchBlockCount = decision.blockCount;
381
750
  writeState(state);
751
+ const prList = decision.untracked.join(", ");
752
+ 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>"\`.`;
753
+ return blockStop(reason, host);
754
+ }
755
+ function walkthroughGate() {
756
+ const state = readState(sessionId);
757
+ if (state.e2eRun !== true || state.walkthroughPosted === true || state.walkthroughSkipped === true) {
758
+ return "{}";
759
+ }
760
+ const scan = scanForOwedWalkthroughs(state);
761
+ if (scan.owed.length === 0) {
762
+ if (scan.verified.length > 0) writeState({ ...state, walkthroughPosted: true });
763
+ return "{}";
764
+ }
765
+ const decision = walkthroughGateDecision(state, scan.owed);
766
+ if (decision.action !== "block" /* Block */) return "{}";
767
+ state.walkthroughBlockCount = decision.blockCount;
768
+ writeState(state);
382
769
  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>"\`.`;
770
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. An E2E acceptance run happened this session but no visual walkthrough has reached ${prList}. Per the postPRVisualWalkthrough preference (default: always), post it now via /muggle:muggle-pr-visual-walkthrough \u2014 include the failed runs, which are the ones reviewers most need to see. If this result genuinely should not be posted (postPRVisualWalkthrough=never, someone else's PR, nothing renderable), tell the user why and run \`echo "MUGGLE_WALKTHROUGH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Walkthrough still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WALKTHROUGH_BLOCKS}): post via /muggle:muggle-pr-visual-walkthrough, or record a legitimate skip via \`echo "MUGGLE_WALKTHROUGH_SKIP: <reason>"\`.`;
384
771
  return blockStop(reason, host);
385
772
  }
386
773
  function reportGate() {
387
- const result = evaluateReportPost(input);
388
- if (!result.deny || !result.reason) return "{}";
389
- return denyTool(result.reason, host);
774
+ const reportPostVerdict = evaluateReportPost(input);
775
+ if (!reportPostVerdict.deny || !reportPostVerdict.reason) return "{}";
776
+ return denyTool(reportPostVerdict.reason, host);
390
777
  }
391
778
  function buildRouter() {
392
779
  if (!detectBuildIntent(input.prompt ?? "")) return "{}";
@@ -405,7 +792,14 @@ var handlers = {
405
792
  "e2e-gate": e2eGate,
406
793
  "terminal-gate": terminalGate,
407
794
  "watch-gate": watchGate,
795
+ "walkthrough-gate": walkthroughGate,
408
796
  "report-gate": reportGate,
409
- "build-router": buildRouter
797
+ "build-router": buildRouter,
798
+ "skill-stages": skillStages,
799
+ "record-stage-read": recordStageRead,
800
+ "record-stage-signals": recordStageSignals,
801
+ "classify-gate": classifyGate,
802
+ "stage-gate": stageGate,
803
+ "debug-path-gate": debugPathGate
410
804
  };
411
805
  process.stdout.write((handlers[sub] ?? (() => "{}"))());