@muggleai/works 5.1.0 → 5.3.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.
- package/dist/{chunk-2BHC6TQR.js → chunk-IGXNFT4M.js} +11 -4
- package/dist/{chunk-MOHR7QUN.js → chunk-MPLQNDZX.js} +52 -20
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/README.md +19 -8
- package/dist/plugin/hooks/hooks.json +22 -0
- package/dist/plugin/scripts/guardrail-report-format.sh +9 -0
- package/dist/plugin/scripts/guardrails.mjs +104 -15
- package/dist/plugin/skills/_shared/dev-loop/run.md +1 -1
- package/dist/plugin/skills/_shared/failure-mode-handling.md +1 -1
- package/dist/plugin/skills/do/e2e-acceptance.md +1 -1
- package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -0
- package/dist/plugin/skills/muggle-preferences/preference-gates/README.md +8 -5
- package/dist/plugin/skills/muggle-test/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-test/execute-remote.md +1 -1
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +1 -1
- package/dist/release-manifest.json +4 -4
- package/dist/{src-SS2AL3O7.js → src-MQFKDZW5.js} +1 -1
- package/package.json +8 -7
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/hooks/README.md +19 -8
- package/plugin/hooks/hooks.json +22 -0
- package/plugin/scripts/guardrail-report-format.sh +9 -0
- package/plugin/scripts/guardrails.mjs +104 -15
- package/plugin/skills/_shared/dev-loop/run.md +1 -1
- package/plugin/skills/_shared/failure-mode-handling.md +1 -1
- package/plugin/skills/do/e2e-acceptance.md +1 -1
- package/plugin/skills/muggle-browser-task/SKILL.md +1 -0
- package/plugin/skills/muggle-preferences/preference-gates/README.md +8 -5
- package/plugin/skills/muggle-test/SKILL.md +1 -1
- package/plugin/skills/muggle-test/execute-remote.md +1 -1
- package/plugin/skills/muggle-test-feature-local/SKILL.md +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, isFirstRun, writePreferences, DEFAULT_PREFERENCES, getDataDir, PREFERENCES_FILE_NAME, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, getDisclosureCopy, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-
|
|
1
|
+
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, isFirstRun, writePreferences, DEFAULT_PREFERENCES, getDataDir, PREFERENCES_FILE_NAME, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, verifyFileChecksum, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, performLogin, toolRequiresAuth, getCallerCredentials, hasShownDisclosure, getDisclosureCopy, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-MPLQNDZX.js';
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
@@ -736,7 +736,7 @@ async function resolveGsScreenshotUrls(report, opts) {
|
|
|
736
736
|
if (gsUrls.length === 0) {
|
|
737
737
|
return report;
|
|
738
738
|
}
|
|
739
|
-
const mcps = await import('./src-
|
|
739
|
+
const mcps = await import('./src-MQFKDZW5.js');
|
|
740
740
|
const credentials = await mcps.getCallerCredentialsAsync();
|
|
741
741
|
if (!credentials.bearerToken && !credentials.apiKey) {
|
|
742
742
|
stderrWrite(
|
|
@@ -784,6 +784,9 @@ async function resolveGsScreenshotUrls(report, opts) {
|
|
|
784
784
|
|
|
785
785
|
// src/cli/build-pr-section.ts
|
|
786
786
|
var DEFAULT_MAX_BODY_BYTES = 6e4;
|
|
787
|
+
var REPORT_SECTION_SENTINEL = "<!-- muggle-pr-section:v1 -->";
|
|
788
|
+
var withSentinel = (s) => s ? `${REPORT_SECTION_SENTINEL}
|
|
789
|
+
${s}` : s;
|
|
787
790
|
async function readAll(stream) {
|
|
788
791
|
const chunks = [];
|
|
789
792
|
for await (const chunk of stream) {
|
|
@@ -828,8 +831,12 @@ ${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
|
|
|
828
831
|
return 1;
|
|
829
832
|
}
|
|
830
833
|
const resolvedReport = await resolveGsScreenshotUrls(report, { stderrWrite: opts.stderrWrite });
|
|
831
|
-
const
|
|
832
|
-
|
|
834
|
+
const sentinelCost = Buffer.byteLength(`${REPORT_SECTION_SENTINEL}
|
|
835
|
+
`, "utf-8");
|
|
836
|
+
const result = buildPrSection(resolvedReport, { maxBodyBytes: opts.maxBodyBytes - sentinelCost });
|
|
837
|
+
opts.stdoutWrite(
|
|
838
|
+
JSON.stringify({ body: withSentinel(result.body), comment: withSentinel(result.comment) })
|
|
839
|
+
);
|
|
833
840
|
return 0;
|
|
834
841
|
}
|
|
835
842
|
async function buildPrSectionCommand(options) {
|
|
@@ -3938,7 +3938,8 @@ var TestCaseUpdateInputSchema = z.object({
|
|
|
3938
3938
|
});
|
|
3939
3939
|
var TestScriptListInputSchema = z.object({
|
|
3940
3940
|
projectId: IdSchema.describe("Project ID (UUID) to list test scripts for"),
|
|
3941
|
-
testCaseId: IdSchema.optional().describe("Optional test case ID (UUID) to filter scripts by")
|
|
3941
|
+
testCaseId: IdSchema.optional().describe("Optional test case ID (UUID) to filter scripts by"),
|
|
3942
|
+
runEnvironmentType: RunEnvironmentInputSchema
|
|
3942
3943
|
}).merge(PaginationInputSchema);
|
|
3943
3944
|
var TestScriptGetInputSchema = z.object({
|
|
3944
3945
|
testScriptId: IdSchema.describe("Test script ID (UUID) to retrieve")
|
|
@@ -4912,7 +4913,7 @@ var bulkPreviewTools = [
|
|
|
4912
4913
|
var testScriptTools = [
|
|
4913
4914
|
{
|
|
4914
4915
|
name: "muggle-remote-test-script-list",
|
|
4915
|
-
description: "List test scripts for a project, optionally filtered by test case. Returns up to 10 items per page by default (max 100). Response includes pagination metadata (totalCount, totalPages, hasMore) \u2014 check `hasMore` to decide whether to fetch additional pages.",
|
|
4916
|
+
description: "List test scripts for a project, optionally filtered by test case and by environment lane. Returns up to 10 items per page by default (max 100). Response includes pagination metadata (totalCount, totalPages, hasMore) \u2014 check `hasMore` to decide whether to fetch additional pages.",
|
|
4916
4917
|
inputSchema: TestScriptListInputSchema,
|
|
4917
4918
|
mapToUpstream: (input) => {
|
|
4918
4919
|
const data = input;
|
|
@@ -4922,6 +4923,7 @@ var testScriptTools = [
|
|
|
4922
4923
|
queryParams: {
|
|
4923
4924
|
projectId: data.projectId,
|
|
4924
4925
|
testCaseId: data.testCaseId,
|
|
4926
|
+
runEnvironmentType: data.runEnvironmentType,
|
|
4925
4927
|
page: data.page,
|
|
4926
4928
|
pageSize: data.pageSize,
|
|
4927
4929
|
sortBy: data.sortBy,
|
|
@@ -6236,6 +6238,11 @@ var PreferenceKey = /* @__PURE__ */ ((PreferenceKey2) => {
|
|
|
6236
6238
|
PreferenceKey2["AutoRebase"] = "autoRebase";
|
|
6237
6239
|
PreferenceKey2["AutoCleanup"] = "autoCleanup";
|
|
6238
6240
|
PreferenceKey2["AutoE2ETest"] = "autoE2ETest";
|
|
6241
|
+
PreferenceKey2["AutoResolveConflicts"] = "autoResolveConflicts";
|
|
6242
|
+
PreferenceKey2["AutoReuseValidationContext"] = "autoReuseValidationContext";
|
|
6243
|
+
PreferenceKey2["AutoRouteBuildToMuggleDo"] = "autoRouteBuildToMuggleDo";
|
|
6244
|
+
PreferenceKey2["AutoWatchPR"] = "autoWatchPR";
|
|
6245
|
+
PreferenceKey2["ReusePreparePlan"] = "reusePreparePlan";
|
|
6239
6246
|
return PreferenceKey2;
|
|
6240
6247
|
})(PreferenceKey || {});
|
|
6241
6248
|
var PreferenceValue = /* @__PURE__ */ ((PreferenceValue2) => {
|
|
@@ -6252,23 +6259,28 @@ var PREFERENCES_FILE_NAME = "preferences.json";
|
|
|
6252
6259
|
var PREFERENCES_PROJECT_DIR_NAME = ".muggle-ai";
|
|
6253
6260
|
var PREFERENCES_VERSION = 1;
|
|
6254
6261
|
var DEFAULT_PREFERENCES = {
|
|
6255
|
-
["autoLogin" /* AutoLogin */]: "
|
|
6256
|
-
["autoSelectProject" /* AutoSelectProject */]: "
|
|
6257
|
-
["autoSelectLocalHost" /* AutoSelectLocalHost */]: "
|
|
6258
|
-
["showElectronBrowser" /* ShowElectronBrowser */]: "
|
|
6259
|
-
["openTestResultsAfterRun" /* OpenTestResultsAfterRun */]: "
|
|
6260
|
-
["defaultExecutionMode" /* DefaultExecutionMode */]: "
|
|
6261
|
-
["suggestRelatedUseCases" /* SuggestRelatedUseCases */]: "
|
|
6262
|
-
["suggestRelatedTestCases" /* SuggestRelatedTestCases */]: "
|
|
6263
|
-
["autoDetectChanges" /* AutoDetectChanges */]: "
|
|
6264
|
-
["postPRVisualWalkthrough" /* PostPRVisualWalkthrough */]: "
|
|
6265
|
-
["autoCreatePR" /* AutoCreatePR */]: "
|
|
6266
|
-
["checkForUpdates" /* CheckForUpdates */]: "
|
|
6267
|
-
["verboseOutput" /* VerboseOutput */]: "
|
|
6268
|
-
["autoUseWorktree" /* AutoUseWorktree */]: "
|
|
6269
|
-
["autoRebase" /* AutoRebase */]: "
|
|
6270
|
-
["autoCleanup" /* AutoCleanup */]: "
|
|
6271
|
-
["autoE2ETest" /* AutoE2ETest */]: "always" /* Always
|
|
6262
|
+
["autoLogin" /* AutoLogin */]: "always" /* Always */,
|
|
6263
|
+
["autoSelectProject" /* AutoSelectProject */]: "always" /* Always */,
|
|
6264
|
+
["autoSelectLocalHost" /* AutoSelectLocalHost */]: "always" /* Always */,
|
|
6265
|
+
["showElectronBrowser" /* ShowElectronBrowser */]: "always" /* Always */,
|
|
6266
|
+
["openTestResultsAfterRun" /* OpenTestResultsAfterRun */]: "always" /* Always */,
|
|
6267
|
+
["defaultExecutionMode" /* DefaultExecutionMode */]: "local" /* Local */,
|
|
6268
|
+
["suggestRelatedUseCases" /* SuggestRelatedUseCases */]: "always" /* Always */,
|
|
6269
|
+
["suggestRelatedTestCases" /* SuggestRelatedTestCases */]: "always" /* Always */,
|
|
6270
|
+
["autoDetectChanges" /* AutoDetectChanges */]: "always" /* Always */,
|
|
6271
|
+
["postPRVisualWalkthrough" /* PostPRVisualWalkthrough */]: "always" /* Always */,
|
|
6272
|
+
["autoCreatePR" /* AutoCreatePR */]: "always" /* Always */,
|
|
6273
|
+
["checkForUpdates" /* CheckForUpdates */]: "always" /* Always */,
|
|
6274
|
+
["verboseOutput" /* VerboseOutput */]: "never" /* Never */,
|
|
6275
|
+
["autoUseWorktree" /* AutoUseWorktree */]: "always" /* Always */,
|
|
6276
|
+
["autoRebase" /* AutoRebase */]: "always" /* Always */,
|
|
6277
|
+
["autoCleanup" /* AutoCleanup */]: "always" /* Always */,
|
|
6278
|
+
["autoE2ETest" /* AutoE2ETest */]: "always" /* Always */,
|
|
6279
|
+
["autoResolveConflicts" /* AutoResolveConflicts */]: "always" /* Always */,
|
|
6280
|
+
["autoReuseValidationContext" /* AutoReuseValidationContext */]: "always" /* Always */,
|
|
6281
|
+
["autoRouteBuildToMuggleDo" /* AutoRouteBuildToMuggleDo */]: "always" /* Always */,
|
|
6282
|
+
["autoWatchPR" /* AutoWatchPR */]: "always" /* Always */,
|
|
6283
|
+
["reusePreparePlan" /* ReusePreparePlan */]: "always" /* Always */
|
|
6272
6284
|
};
|
|
6273
6285
|
var ALWAYS_ASK_NEVER = [
|
|
6274
6286
|
"always" /* Always */,
|
|
@@ -6301,7 +6313,12 @@ var PREFERENCE_ALLOWED_VALUES = {
|
|
|
6301
6313
|
["autoUseWorktree" /* AutoUseWorktree */]: ALWAYS_ASK_NEVER,
|
|
6302
6314
|
["autoRebase" /* AutoRebase */]: ALWAYS_ASK_NEVER,
|
|
6303
6315
|
["autoCleanup" /* AutoCleanup */]: ALWAYS_ASK_NEVER,
|
|
6304
|
-
["autoE2ETest" /* AutoE2ETest */]: ALWAYS_ASK
|
|
6316
|
+
["autoE2ETest" /* AutoE2ETest */]: ALWAYS_ASK,
|
|
6317
|
+
["autoResolveConflicts" /* AutoResolveConflicts */]: ALWAYS_ASK_NEVER,
|
|
6318
|
+
["autoReuseValidationContext" /* AutoReuseValidationContext */]: ALWAYS_ASK_NEVER,
|
|
6319
|
+
["autoRouteBuildToMuggleDo" /* AutoRouteBuildToMuggleDo */]: ALWAYS_ASK_NEVER,
|
|
6320
|
+
["autoWatchPR" /* AutoWatchPR */]: ALWAYS_ASK_NEVER,
|
|
6321
|
+
["reusePreparePlan" /* ReusePreparePlan */]: ALWAYS_ASK_NEVER
|
|
6305
6322
|
};
|
|
6306
6323
|
var PREFERENCES_SCHEMA = {
|
|
6307
6324
|
["autoLogin" /* AutoLogin */]: {
|
|
@@ -6354,6 +6371,21 @@ var PREFERENCES_SCHEMA = {
|
|
|
6354
6371
|
},
|
|
6355
6372
|
["autoE2ETest" /* AutoE2ETest */]: {
|
|
6356
6373
|
description: "Run Stage 6 (E2E acceptance) at the end of every /muggle-do cycle (default always \u2014 running E2E is the point of muggle-do; never is not an option)"
|
|
6374
|
+
},
|
|
6375
|
+
["autoResolveConflicts" /* AutoResolveConflicts */]: {
|
|
6376
|
+
description: "When a rebase onto the default branch hits conflicts, resolve them autonomously behind a verify-or-rollback gate instead of aborting and escalating"
|
|
6377
|
+
},
|
|
6378
|
+
["autoReuseValidationContext" /* AutoReuseValidationContext */]: {
|
|
6379
|
+
description: "When a prior session left an E2E validation context for this working tree, reuse it instead of re-asking the validation questions"
|
|
6380
|
+
},
|
|
6381
|
+
["autoRouteBuildToMuggleDo" /* AutoRouteBuildToMuggleDo */]: {
|
|
6382
|
+
description: "When a prompt looks like a build/implement/fix request, route it through the /muggle-do pipeline (requirements \u2192 build \u2192 impact \u2192 tests \u2192 E2E \u2192 PR \u2192 watcher)"
|
|
6383
|
+
},
|
|
6384
|
+
["autoWatchPR" /* AutoWatchPR */]: {
|
|
6385
|
+
description: "After a test run opens a PR, start a muggle-pr-followup watcher that polls for new reviews and hands them to /muggle-do"
|
|
6386
|
+
},
|
|
6387
|
+
["reusePreparePlan" /* ReusePreparePlan */]: {
|
|
6388
|
+
description: "Reuse the saved prepare plan for this stack (skip discovery, jump to check-running + smoke-test) instead of rediscovering from scratch"
|
|
6357
6389
|
}
|
|
6358
6390
|
};
|
|
6359
6391
|
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
2
|
-
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-
|
|
1
|
+
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-IGXNFT4M.js';
|
|
2
|
+
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-MPLQNDZX.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.3.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -9,18 +9,29 @@ Condition-triggered hooks that make Muggle Test's high-value handoffs fire path-
|
|
|
9
9
|
- **Claude Code layer** — the agent runtime that fires these hooks. A guardrail is a Claude-Code-layer trigger, nothing more.
|
|
10
10
|
- **Muggle Test layer** — the product (muggle-do, muggle-test, the watcher). This is what a guardrail *invokes*.
|
|
11
11
|
|
|
12
|
-
A guardrail
|
|
12
|
+
A guardrail steers the model toward a Muggle Test flow; it never reimplements the flow.
|
|
13
13
|
|
|
14
|
-
Design rationale: `muggle-ai-brain/architecture/2026-06-02-harness-pipeline-integration-design.md
|
|
14
|
+
Design rationale: `muggle-ai-brain/architecture/2026-06-02-harness-pipeline-integration-design.md` (original advisory design; the E2E gate and report gate below now enforce rather than advise).
|
|
15
|
+
|
|
16
|
+
## Advise vs enforce
|
|
17
|
+
|
|
18
|
+
A guardrail emits one of two strengths:
|
|
19
|
+
|
|
20
|
+
- **Advise** — `additionalContext` (PostToolUse/UserPromptSubmit) or a plain Stop message. A soft nudge the model can ignore.
|
|
21
|
+
- **Enforce** — a `Stop` `decision: "block"` that refuses to end the turn, or a `PreToolUse` `permissionDecision: "deny"` that refuses a tool call. The model cannot proceed until the condition is met.
|
|
22
|
+
|
|
23
|
+
Enforcement is reserved for the handoffs that were being skipped: the E2E acceptance run and posting a deterministically-rendered report. Each enforcing gate carries an escape so it can't trap a turn — the E2E gate releases to advisory after `MAX_E2E_BLOCKS` (3) blocks; the report gate only denies a body it can positively see is a hand-written report and fails open otherwise.
|
|
15
24
|
|
|
16
25
|
## Mechanism
|
|
17
26
|
|
|
18
|
-
Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json`. The wrapper pipes the event payload (stdin JSON) to the bundled `../scripts/guardrails.mjs <subcommand>`, which holds the decision logic (built from `src/guardrails/`, vitest-covered). Per-session state in `~/.muggle-ai/guardrails/<session_id>.json`
|
|
27
|
+
Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json`. The wrapper pipes the event payload (stdin JSON) to the bundled `../scripts/guardrails.mjs <subcommand>`, which holds the decision logic (built from `src/guardrails/`, vitest-covered). Per-session state in `~/.muggle-ai/guardrails/<session_id>.json` tracks what fired. Any *failure* degrades to `{}` (allow) — a gate blocks only by an explicit, tested decision, never by accident.
|
|
19
28
|
|
|
20
29
|
## Guardrails
|
|
21
30
|
|
|
22
|
-
| Hook event | Wrapper | Condition | Preference |
|
|
23
|
-
| :--------- | :------ | :-------- | :--------- |
|
|
24
|
-
| `PostToolUse` (Bash) | `guardrail-pr-opened.sh` | a `gh pr create`/`gh pr ready` just succeeded | `autoWatchPR` | start a `muggle-pr-followup` watcher on the new PR |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
31
|
+
| Hook event | Wrapper | Strength | Condition | Preference | Effect |
|
|
32
|
+
| :--------- | :------ | :------- | :-------- | :--------- | :----- |
|
|
33
|
+
| `PostToolUse` (Bash) | `guardrail-pr-opened.sh` | advise | a `gh pr create`/`gh pr ready` just succeeded | `autoWatchPR` | start a `muggle-pr-followup` watcher on the new PR |
|
|
34
|
+
| `PostToolUse` (Bash + muggle execute/replay MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, or an E2E run happened | — | set `unitTestsGreen` / `e2eRun` session state |
|
|
35
|
+
| `PreToolUse` (Bash) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
|
|
36
|
+
| `Stop` | `guardrail-e2e-gate.sh` | **enforce** | unit tests passed this session and no E2E ran yet | `autoE2ETest` | **block** the turn until E2E runs via `muggle-test` (releases after 3 blocks) |
|
|
37
|
+
| `UserPromptSubmit` | `guardrail-build-router.sh` | advise | a build/implement/fix request (first one this session) | `autoRouteBuildToMuggleDo` | route the work through `muggle-do` (build delegated to superpowers) |
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
]
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
|
+
"PreToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Bash",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-report-format.sh\"",
|
|
22
|
+
"async": false
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
15
27
|
"PostToolUse": [
|
|
16
28
|
{
|
|
17
29
|
"matcher": "Bash",
|
|
@@ -27,6 +39,16 @@
|
|
|
27
39
|
"async": false
|
|
28
40
|
}
|
|
29
41
|
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"matcher": "mcp__.*muggle.*(execute|replay)",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-tests.sh\"",
|
|
49
|
+
"async": false
|
|
50
|
+
}
|
|
51
|
+
]
|
|
30
52
|
}
|
|
31
53
|
],
|
|
32
54
|
"Stop": [
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Report-format gate (PreToolUse, Bash). Denies a `gh pr comment|create|edit`
|
|
5
|
+
# whose body reads like a hand-written E2E report — one that lacks the
|
|
6
|
+
# build-pr-section sentinel — so every posted walkthrough goes through the
|
|
7
|
+
# deterministic renderer. Degrades to {} so it never blocks an unrelated command.
|
|
8
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
9
|
+
node "${root}/scripts/guardrails.mjs" report-gate 2>/dev/null || printf '{}'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { isAbsolute, resolve, join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
|
|
5
5
|
// src/guardrails/cli.ts
|
|
@@ -60,9 +60,26 @@ function isE2ERun(input2) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// src/guardrails/shouldRunE2E.ts
|
|
63
|
+
var MAX_E2E_BLOCKS = 3;
|
|
63
64
|
function shouldRunE2E(state) {
|
|
64
65
|
return state.unitTestsGreen === true && state.e2eRun !== true;
|
|
65
66
|
}
|
|
67
|
+
function applyRecordedRun(state, run) {
|
|
68
|
+
let next = state;
|
|
69
|
+
if (run.unitTestPassed) {
|
|
70
|
+
next = { ...next, unitTestsGreen: true, e2eRun: false, e2eBlockCount: 0 };
|
|
71
|
+
}
|
|
72
|
+
if (run.e2eRan) {
|
|
73
|
+
next = { ...next, e2eRun: true };
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function e2eGateDecision(state, maxBlocks = MAX_E2E_BLOCKS) {
|
|
78
|
+
const blockCount = state.e2eBlockCount ?? 0;
|
|
79
|
+
if (!shouldRunE2E(state)) return { action: "none" /* None */, blockCount };
|
|
80
|
+
if (blockCount >= maxBlocks) return { action: "release" /* Release */, blockCount };
|
|
81
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1 };
|
|
82
|
+
}
|
|
66
83
|
|
|
67
84
|
// src/guardrails/detectBuildIntent.ts
|
|
68
85
|
var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
|
|
@@ -74,6 +91,60 @@ function detectBuildIntent(prompt) {
|
|
|
74
91
|
if (QUESTION.test(p)) return false;
|
|
75
92
|
return BUILD.test(p) || DEVCYCLE.test(p);
|
|
76
93
|
}
|
|
94
|
+
var REPORT_SENTINEL = "muggle-pr-section";
|
|
95
|
+
var PR_POST_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
|
|
96
|
+
var defaultReader = (path, cwd) => {
|
|
97
|
+
try {
|
|
98
|
+
const abs = isAbsolute(path) ? path : resolve(cwd ?? process.cwd(), path);
|
|
99
|
+
if (!existsSync(abs)) return null;
|
|
100
|
+
return readFileSync(abs, "utf-8");
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
function unquote(s) {
|
|
106
|
+
const t = s.trim();
|
|
107
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
108
|
+
return t.slice(1, -1);
|
|
109
|
+
}
|
|
110
|
+
return t;
|
|
111
|
+
}
|
|
112
|
+
function looksLikeE2EReport(text) {
|
|
113
|
+
const t = text.toLowerCase();
|
|
114
|
+
const statusEmojis = (text.match(/[✅❌⚠]/gu) ?? []).length;
|
|
115
|
+
const tally = /\b\d+\s+(tests?\s+)?passed\b/.test(t) && /\b\d+\s+(tests?\s+)?(failed|inconclusive)\b/.test(t);
|
|
116
|
+
const slashTally = /\bpassed\b\s*[/|]\s*\d*\s*(tests?\s*)?\bfailed\b/.test(t) || /\bfailed\b\s*[/|]\s*\d*\s*(tests?\s*)?\bpassed\b/.test(t);
|
|
117
|
+
const resultsStructure = /acceptance results/.test(t) || tally || slashTally || statusEmojis >= 2;
|
|
118
|
+
const muggleContext = /\bmuggle\b/.test(t) || /muggle-ai\.com/.test(t) || /\be2e\b/.test(t) || /\bacceptance\b/.test(t);
|
|
119
|
+
return resultsStructure && muggleContext;
|
|
120
|
+
}
|
|
121
|
+
function collectInspectableText(cmd, cwd, read) {
|
|
122
|
+
let text = cmd;
|
|
123
|
+
for (const m of cmd.matchAll(/--body-file[=\s]+("[^"]+"|'[^']+'|\S+)/g)) {
|
|
124
|
+
const p = unquote(m[1]);
|
|
125
|
+
if (p && p !== "-") {
|
|
126
|
+
const c = read(p, cwd);
|
|
127
|
+
if (c) text += "\n" + c;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const m of cmd.matchAll(/jq\b[^|]*?("[^"]+\.json"|'[^']+\.json'|\S+\.json)/g)) {
|
|
131
|
+
const c = read(unquote(m[1]), cwd);
|
|
132
|
+
if (c) text += "\n" + c;
|
|
133
|
+
}
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
function evaluateReportPost(input2, read = defaultReader) {
|
|
137
|
+
if (input2.tool_name !== "Bash") return { deny: false };
|
|
138
|
+
const cmd = input2.tool_input?.command ?? "";
|
|
139
|
+
if (!PR_POST_CMD.test(cmd)) return { deny: false };
|
|
140
|
+
const text = collectInspectableText(cmd, input2.cwd, read);
|
|
141
|
+
if (text.includes(REPORT_SENTINEL)) return { deny: false };
|
|
142
|
+
if (!looksLikeE2EReport(text)) return { deny: false };
|
|
143
|
+
return {
|
|
144
|
+
deny: true,
|
|
145
|
+
reason: "Blocked: this looks like a hand-written E2E test report. Muggle requires the deterministic renderer \u2014 build the run's E2eReport JSON and pipe it through `muggle build-pr-section` (or invoke /muggle:muggle-pr-visual-walkthrough), then post that output. Never hand-write the walkthrough markdown."
|
|
146
|
+
};
|
|
147
|
+
}
|
|
77
148
|
|
|
78
149
|
// src/guardrails/emit.ts
|
|
79
150
|
function envelope(eventName, context, host2) {
|
|
@@ -83,6 +154,22 @@ function envelope(eventName, context, host2) {
|
|
|
83
154
|
hookSpecificOutput: { hookEventName: eventName, additionalContext: context }
|
|
84
155
|
});
|
|
85
156
|
}
|
|
157
|
+
function blockStop(reason, host2) {
|
|
158
|
+
if (!reason) return "{}";
|
|
159
|
+
if (host2 === "cursor") return JSON.stringify({ additional_context: reason });
|
|
160
|
+
return JSON.stringify({ decision: "block", reason });
|
|
161
|
+
}
|
|
162
|
+
function denyTool(reason, host2) {
|
|
163
|
+
if (!reason) return "{}";
|
|
164
|
+
if (host2 === "cursor") return JSON.stringify({ additional_context: reason });
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
hookSpecificOutput: {
|
|
167
|
+
hookEventName: "PreToolUse",
|
|
168
|
+
permissionDecision: "deny",
|
|
169
|
+
permissionDecisionReason: reason
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
86
173
|
|
|
87
174
|
// src/guardrails/cli.ts
|
|
88
175
|
function readStdin() {
|
|
@@ -108,25 +195,26 @@ Per the autoWatchPR preference, a muggle-pr-followup watcher should handle its i
|
|
|
108
195
|
function recordTests() {
|
|
109
196
|
const cmd = input.tool_input?.command ?? "";
|
|
110
197
|
const state = readState(sessionId);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (isE2ERun(input)) {
|
|
117
|
-
state.e2eRun = true;
|
|
118
|
-
changed = true;
|
|
119
|
-
}
|
|
120
|
-
if (changed) writeState(state);
|
|
198
|
+
const next = applyRecordedRun(state, {
|
|
199
|
+
unitTestPassed: isTestCommand(cmd) && testsPassed(input),
|
|
200
|
+
e2eRan: isE2ERun(input)
|
|
201
|
+
});
|
|
202
|
+
if (next !== state) writeState(next);
|
|
121
203
|
return "{}";
|
|
122
204
|
}
|
|
123
205
|
function e2eGate() {
|
|
124
206
|
const state = readState(sessionId);
|
|
125
|
-
|
|
126
|
-
|
|
207
|
+
const decision = e2eGateDecision(state);
|
|
208
|
+
if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) return "{}";
|
|
209
|
+
state.e2eBlockCount = decision.blockCount;
|
|
127
210
|
writeState(state);
|
|
128
|
-
const
|
|
129
|
-
return
|
|
211
|
+
const reason = `Do not end the turn yet. Unit tests passed this session but no E2E acceptance run has happened. Per the autoE2ETest preference (default: always), run change-driven E2E now via /muggle:muggle-test, then finish. If E2E genuinely cannot run here (no app, services down, no PR), say so explicitly to the user \u2014 this gate releases after ${MAX_E2E_BLOCKS} attempts.`;
|
|
212
|
+
return blockStop(reason, host);
|
|
213
|
+
}
|
|
214
|
+
function reportGate() {
|
|
215
|
+
const result = evaluateReportPost(input);
|
|
216
|
+
if (!result.deny || !result.reason) return "{}";
|
|
217
|
+
return denyTool(result.reason, host);
|
|
130
218
|
}
|
|
131
219
|
function buildRouter() {
|
|
132
220
|
if (!detectBuildIntent(input.prompt ?? "")) return "{}";
|
|
@@ -141,6 +229,7 @@ var handlers = {
|
|
|
141
229
|
"pr-opened": prOpened,
|
|
142
230
|
"record-tests": recordTests,
|
|
143
231
|
"e2e-gate": e2eGate,
|
|
232
|
+
"report-gate": reportGate,
|
|
144
233
|
"build-router": buildRouter
|
|
145
234
|
};
|
|
146
235
|
process.stdout.write((handlers[sub] ?? (() => "{}"))());
|
|
@@ -23,7 +23,7 @@ One local browser exists, so **execution is sequential** — one test case at a
|
|
|
23
23
|
Per test case, branch on `mode`:
|
|
24
24
|
|
|
25
25
|
**Replay**
|
|
26
|
-
1. `muggle-remote-test-script-get` (latest replayable script) → note `actionScriptId`.
|
|
26
|
+
1. `muggle-remote-test-script-get` (latest replayable script — the lane-scoped one the caller resolved during classification per [`../failure-mode-handling.md`](../failure-mode-handling.md) §A) → note `actionScriptId`.
|
|
27
27
|
2. `muggle-remote-action-script-get` with that id → full `actionScript` (see [`action-script.md`](action-script.md)).
|
|
28
28
|
3. `muggle-local-execute-replay` with `testScript`, `actionScript`, `localUrl`, `cwd`, `showUi`, `freshSession` (see [`fresh-session.md`](fresh-session.md)), `timeoutMs` (see [`timeouts.md`](timeouts.md)).
|
|
29
29
|
|
|
@@ -61,7 +61,7 @@ Run during change analysis, **per impacted test case**. Picks the initial execut
|
|
|
61
61
|
|
|
62
62
|
- The change summary from `git diff` (file paths + diff content).
|
|
63
63
|
- The test case (title, description, instructions, last passing run timestamp).
|
|
64
|
-
- Existing test scripts for that test case from `muggle-remote-test-script-list
|
|
64
|
+
- Existing test scripts for that test case from `muggle-remote-test-script-list`, scoped to the run's lane: pass `runEnvironmentType: "local"` in Local mode (Step 7A), `"remote"` in Remote mode (Step 7B). The lane is fixed at runtime — it selects the versioned runSettings the cloud resolves — so a local run keys replay-vs-regen off the localhost-lane script and a remote run off the deployed-lane script, never the other lane's.
|
|
65
65
|
|
|
66
66
|
### Rules (fire in order; first match wins)
|
|
67
67
|
|
|
@@ -98,7 +98,7 @@ Based on the changed files and the requirements goal, determine which test cases
|
|
|
98
98
|
|
|
99
99
|
### Step 4: Run the dev loop, gather screenshots
|
|
100
100
|
|
|
101
|
-
For each relevant test case, run the shared loop in [`../_shared/dev-loop/run.md`](../_shared/dev-loop/run.md): `muggle-remote-test-script-list` by `testCaseId` to pick [replay vs regen](../_shared/dev-loop/run.md), [execute with `timeoutMs`](../_shared/dev-loop/timeouts.md), [fetch the result](../_shared/dev-loop/failures.md) and [interpret failures](../_shared/dev-loop/failures.md), then read the studio-published [cloud refs and per-step screenshots](../_shared/dev-loop/publish.md) off the run result.
|
|
101
|
+
For each relevant test case, run the shared loop in [`../_shared/dev-loop/run.md`](../_shared/dev-loop/run.md): `muggle-remote-test-script-list` by `testCaseId` with `runEnvironmentType: "local"` (this stage runs against localhost) to pick [replay vs regen](../_shared/dev-loop/run.md), [execute with `timeoutMs`](../_shared/dev-loop/timeouts.md), [fetch the result](../_shared/dev-loop/failures.md) and [interpret failures](../_shared/dev-loop/failures.md), then read the studio-published [cloud refs and per-step screenshots](../_shared/dev-loop/publish.md) off the run result.
|
|
102
102
|
|
|
103
103
|
Inputs to the loop: `mode` from the script-exists check, `localUrl`/project from Step 1.7, `cwd` = the working tree recorded in `state.md`.
|
|
104
104
|
|
|
@@ -74,6 +74,7 @@ Tell the user: `Created test case: <id>`.
|
|
|
74
74
|
Load and call `muggle-remote-test-script-list` with:
|
|
75
75
|
- `projectId`: from Step 2
|
|
76
76
|
- `testCaseId`: from Step 4
|
|
77
|
+
- `runEnvironmentType`: `"local"` — this skill executes in the local browser, so it resolves the local-lane script. The lane is an explicit env type, exactly as the remote flow passes `"remote"`; the target URL is just the lane's url and never decides the lane.
|
|
77
78
|
|
|
78
79
|
Tell the user which mutations will be applied: `Mutations: <mutations[]>` (or "no mutations" if empty).
|
|
79
80
|
|
|
@@ -16,16 +16,19 @@ which uses `local` / `remote` / `ask`).
|
|
|
16
16
|
|
|
17
17
|
## Gate behavior
|
|
18
18
|
|
|
19
|
-
- `always` → take the pro-action, then print silent footer.
|
|
20
|
-
- `never` → take the skip-action, then print silent footer.
|
|
19
|
+
- `always` → take the pro-action, then **always** print the silent footer.
|
|
20
|
+
- `never` → take the skip-action, then **always** print the silent footer.
|
|
21
21
|
- `ask` (or absent) → run Picker 1 (per-key file) → Picker 2 (below).
|
|
22
22
|
|
|
23
23
|
`defaultExecutionMode` uses `local`/`remote` instead of `always`/`never`.
|
|
24
24
|
|
|
25
|
-
## Silent footer (whenever
|
|
25
|
+
## Silent footer (mandatory whenever a prompt is skipped)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
**
|
|
27
|
+
Whenever a gate resolves to a non-`ask` value and skips its picker, the footer
|
|
28
|
+
below is **required** — every gate, every time, no exceptions. It tells the
|
|
29
|
+
user **what happened**, **why it was silent**, and **how to change it**. The
|
|
30
|
+
`preference-gates-lint` test enforces that this contract exists; omitting the
|
|
31
|
+
footer when a gate fires is a bug. Two lines:
|
|
29
32
|
|
|
30
33
|
```
|
|
31
34
|
✓ <silent action from per-key file>
|
|
@@ -219,7 +219,7 @@ Wait for user confirmation before moving to execution.
|
|
|
219
219
|
|
|
220
220
|
### 6f: Classify execution mode per test case (replay vs regen)
|
|
221
221
|
|
|
222
|
-
For each selected test case, decide whether the run should be a **replay** of an existing script or a fresh **regen**, using the rules in [`_shared/failure-mode-handling.md`](../_shared/failure-mode-handling.md) section A. Inputs: the change summary from Step 2, the test case body, and the result of `muggle-remote-test-script-list` for that test case (last passing timestamp + whether any replayable script exists).
|
|
222
|
+
For each selected test case, decide whether the run should be a **replay** of an existing script or a fresh **regen**, using the rules in [`_shared/failure-mode-handling.md`](../_shared/failure-mode-handling.md) section A. Inputs: the change summary from Step 2, the test case body, and the result of `muggle-remote-test-script-list` for that test case (last passing timestamp + whether any replayable script exists). Scope that list call to the run's lane — `runEnvironmentType: "local"` in Local mode, `"remote"` in Remote mode (section A) — so replay reuses the script for the lane the run will execute against.
|
|
223
223
|
|
|
224
224
|
Per test case, fire one `muggle-local-telemetry-event-emit` with `eventType: "pre-execution-classification"` capturing the picked mode, the rule that fired, and the matched changed-file paths.
|
|
225
225
|
|
|
@@ -28,7 +28,7 @@ Branch each test case on its `mode`, then issue **all** workflow-start calls in
|
|
|
28
28
|
- `instructions`: From the test case
|
|
29
29
|
- `expectedResult`: From the test case
|
|
30
30
|
|
|
31
|
-
**Replay-mode test case** — `muggle-remote-workflow-start-test-script-replay` against the latest replayable script for that test case (resolve via `muggle-remote-test-script-list` if not already in hand from the classification step). Tag results with `mode: "replay"` so the router routes failures correctly.
|
|
31
|
+
**Replay-mode test case** — `muggle-remote-workflow-start-test-script-replay` against the latest replayable script for that test case (resolve via `muggle-remote-test-script-list` with `runEnvironmentType: "remote"` if not already in hand from the classification step). Tag results with `mode: "replay"` so the router routes failures correctly.
|
|
32
32
|
|
|
33
33
|
Store each returned workflow runtime ID along with its mode tag.
|
|
34
34
|
|
|
@@ -136,7 +136,7 @@ Before deciding the target's script, resolve its prerequisite chain from the bac
|
|
|
136
136
|
|
|
137
137
|
### 5. Existing scripts vs new generation
|
|
138
138
|
|
|
139
|
-
`muggle-remote-test-script-list` with `testCaseId
|
|
139
|
+
`muggle-remote-test-script-list` with `testCaseId` and `runEnvironmentType: "local"` — this skill always replays against localhost, so resolve the local-lane script (the remote-lane script carries the deployed URL).
|
|
140
140
|
|
|
141
141
|
- **If any replayable/succeeded scripts exist:** use `AskUserQuestion` to present them as clickable options. Show: name, created/updated, step count per option. Include **"Generate new script"** as the last option.
|
|
142
142
|
- **If none:** go straight to generation (no need to ask replay vs generate).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release": "5.
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-06-
|
|
2
|
+
"release": "5.3.0",
|
|
3
|
+
"buildId": "run-55-1",
|
|
4
|
+
"commitSha": "b48301cf3c16a8ff043854510aa3584808f9a7c6",
|
|
5
|
+
"buildTime": "2026-06-17T02:44:35Z",
|
|
6
6
|
"serviceName": "muggle-ai-works-mcp"
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { DEFAULT_PREFERENCES, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports as e2e, formatPreferencesOneLiner, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports as localQa, mcp_exports as mcp, openBrowserUrl, performLogin, performLogout, pollDeviceCode, e2e_exports as qa, readGlobalPreferences, readProjectPreferences, resetConfig, resetLogger, resetPreference, resolveElectronAppPathOrNull, resolvePreferences, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, toolRequiresAuth, validatePreference, verifyFileChecksum, writePreferences } from './chunk-
|
|
1
|
+
export { DEFAULT_PREFERENCES, PREFERENCES_FILE_NAME, PREFERENCES_PROJECT_DIR_NAME, PREFERENCES_SCHEMA, PREFERENCES_VERSION, PREFERENCE_ALLOWED_VALUES, PreferenceKey, PreferenceValue, buildElectronAppChecksumsUrl, buildElectronAppReleaseAssetUrl, buildElectronAppReleaseTag, calculateFileChecksum, createApiKeyWithToken, createChildLogger, deleteApiKeyData, deleteCredentials, e2e_exports as e2e, formatPreferencesOneLiner, getApiKey, getApiKeyFilePath, getAuthService, getBundledElectronAppVersion, getCallerCredentials, getCallerCredentialsAsync, getChecksumForPlatform, getConfig, getCredentialsFilePath, getDataDir, getDownloadBaseUrl, getElectronAppChecksums, getElectronAppDir, getElectronAppVersion, getElectronAppVersionSource, getLocalQaTools, getLogger, getPlatformKey, getQaTools, getValidApiKeyData, getValidCredentials, hasApiKey, isElectronAppInstalled, isFirstRun, loadApiKeyData, loadCredentials, local_exports as localQa, mcp_exports as mcp, openBrowserUrl, performLogin, performLogout, pollDeviceCode, e2e_exports as qa, readGlobalPreferences, readProjectPreferences, resetConfig, resetLogger, resetPreference, resolveElectronAppPathOrNull, resolvePreferences, saveApiKey, saveApiKeyData, saveCredentials, startDeviceCodeFlow, toolRequiresAuth, validatePreference, verifyFileChecksum, writePreferences } from './chunk-MPLQNDZX.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muggleai/works",
|
|
3
3
|
"mcpName": "io.github.multiplex-ai/muggle",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.3.0",
|
|
5
5
|
"description": "Ship quality products with AI-powered E2E acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -38,17 +38,18 @@
|
|
|
38
38
|
"lint:check": "eslint .",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
40
|
"test": "vitest run",
|
|
41
|
-
"test:watch": "vitest"
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"test:gates:behavioral": "tsx internal/skill-gate-eval/src/run.ts"
|
|
42
43
|
},
|
|
43
44
|
"muggleConfig": {
|
|
44
|
-
"electronAppVersion": "1.
|
|
45
|
+
"electronAppVersion": "1.5.1",
|
|
45
46
|
"downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
|
|
46
47
|
"runtimeTargetDefault": "production",
|
|
47
48
|
"checksums": {
|
|
48
|
-
"darwin-arm64": "
|
|
49
|
-
"darwin-x64": "
|
|
50
|
-
"linux-x64": "
|
|
51
|
-
"win32-x64": "
|
|
49
|
+
"darwin-arm64": "70649bed80b3ae407893728abd426e32c54eeda03fde7856e530bb6f67e359cd",
|
|
50
|
+
"darwin-x64": "26a2176c0c926a6f20a4af99e4bc7362e8365ad260c3ca42d18bb1f6cf1b29d5",
|
|
51
|
+
"linux-x64": "69f33e49c6baa017604ffd49a4f4db2eba90260375c4fb5918326843c51d7bdd",
|
|
52
|
+
"win32-x64": "559d02c0e80860dec3ed5c82e8e1d3d4aa2c4dea4f73a2c62c149ad4bd6131d0"
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.3.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/plugin/hooks/README.md
CHANGED
|
@@ -9,18 +9,29 @@ Condition-triggered hooks that make Muggle Test's high-value handoffs fire path-
|
|
|
9
9
|
- **Claude Code layer** — the agent runtime that fires these hooks. A guardrail is a Claude-Code-layer trigger, nothing more.
|
|
10
10
|
- **Muggle Test layer** — the product (muggle-do, muggle-test, the watcher). This is what a guardrail *invokes*.
|
|
11
11
|
|
|
12
|
-
A guardrail
|
|
12
|
+
A guardrail steers the model toward a Muggle Test flow; it never reimplements the flow.
|
|
13
13
|
|
|
14
|
-
Design rationale: `muggle-ai-brain/architecture/2026-06-02-harness-pipeline-integration-design.md
|
|
14
|
+
Design rationale: `muggle-ai-brain/architecture/2026-06-02-harness-pipeline-integration-design.md` (original advisory design; the E2E gate and report gate below now enforce rather than advise).
|
|
15
|
+
|
|
16
|
+
## Advise vs enforce
|
|
17
|
+
|
|
18
|
+
A guardrail emits one of two strengths:
|
|
19
|
+
|
|
20
|
+
- **Advise** — `additionalContext` (PostToolUse/UserPromptSubmit) or a plain Stop message. A soft nudge the model can ignore.
|
|
21
|
+
- **Enforce** — a `Stop` `decision: "block"` that refuses to end the turn, or a `PreToolUse` `permissionDecision: "deny"` that refuses a tool call. The model cannot proceed until the condition is met.
|
|
22
|
+
|
|
23
|
+
Enforcement is reserved for the handoffs that were being skipped: the E2E acceptance run and posting a deterministically-rendered report. Each enforcing gate carries an escape so it can't trap a turn — the E2E gate releases to advisory after `MAX_E2E_BLOCKS` (3) blocks; the report gate only denies a body it can positively see is a hand-written report and fails open otherwise.
|
|
15
24
|
|
|
16
25
|
## Mechanism
|
|
17
26
|
|
|
18
|
-
Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json`. The wrapper pipes the event payload (stdin JSON) to the bundled `../scripts/guardrails.mjs <subcommand>`, which holds the decision logic (built from `src/guardrails/`, vitest-covered). Per-session state in `~/.muggle-ai/guardrails/<session_id>.json`
|
|
27
|
+
Each guardrail is a thin bash wrapper in `../scripts/` registered in `hooks.json`. The wrapper pipes the event payload (stdin JSON) to the bundled `../scripts/guardrails.mjs <subcommand>`, which holds the decision logic (built from `src/guardrails/`, vitest-covered). Per-session state in `~/.muggle-ai/guardrails/<session_id>.json` tracks what fired. Any *failure* degrades to `{}` (allow) — a gate blocks only by an explicit, tested decision, never by accident.
|
|
19
28
|
|
|
20
29
|
## Guardrails
|
|
21
30
|
|
|
22
|
-
| Hook event | Wrapper | Condition | Preference |
|
|
23
|
-
| :--------- | :------ | :-------- | :--------- |
|
|
24
|
-
| `PostToolUse` (Bash) | `guardrail-pr-opened.sh` | a `gh pr create`/`gh pr ready` just succeeded | `autoWatchPR` | start a `muggle-pr-followup` watcher on the new PR |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
31
|
+
| Hook event | Wrapper | Strength | Condition | Preference | Effect |
|
|
32
|
+
| :--------- | :------ | :------- | :-------- | :--------- | :----- |
|
|
33
|
+
| `PostToolUse` (Bash) | `guardrail-pr-opened.sh` | advise | a `gh pr create`/`gh pr ready` just succeeded | `autoWatchPR` | start a `muggle-pr-followup` watcher on the new PR |
|
|
34
|
+
| `PostToolUse` (Bash + muggle execute/replay MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, or an E2E run happened | — | set `unitTestsGreen` / `e2eRun` session state |
|
|
35
|
+
| `PreToolUse` (Bash) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
|
|
36
|
+
| `Stop` | `guardrail-e2e-gate.sh` | **enforce** | unit tests passed this session and no E2E ran yet | `autoE2ETest` | **block** the turn until E2E runs via `muggle-test` (releases after 3 blocks) |
|
|
37
|
+
| `UserPromptSubmit` | `guardrail-build-router.sh` | advise | a build/implement/fix request (first one this session) | `autoRouteBuildToMuggleDo` | route the work through `muggle-do` (build delegated to superpowers) |
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
]
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
|
+
"PreToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Bash",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-report-format.sh\"",
|
|
22
|
+
"async": false
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
15
27
|
"PostToolUse": [
|
|
16
28
|
{
|
|
17
29
|
"matcher": "Bash",
|
|
@@ -27,6 +39,16 @@
|
|
|
27
39
|
"async": false
|
|
28
40
|
}
|
|
29
41
|
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"matcher": "mcp__.*muggle.*(execute|replay)",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-tests.sh\"",
|
|
49
|
+
"async": false
|
|
50
|
+
}
|
|
51
|
+
]
|
|
30
52
|
}
|
|
31
53
|
],
|
|
32
54
|
"Stop": [
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Report-format gate (PreToolUse, Bash). Denies a `gh pr comment|create|edit`
|
|
5
|
+
# whose body reads like a hand-written E2E report — one that lacks the
|
|
6
|
+
# build-pr-section sentinel — so every posted walkthrough goes through the
|
|
7
|
+
# deterministic renderer. Degrades to {} so it never blocks an unrelated command.
|
|
8
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
9
|
+
node "${root}/scripts/guardrails.mjs" report-gate 2>/dev/null || printf '{}'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { isAbsolute, resolve, join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
|
|
5
5
|
// src/guardrails/cli.ts
|
|
@@ -60,9 +60,26 @@ function isE2ERun(input2) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// src/guardrails/shouldRunE2E.ts
|
|
63
|
+
var MAX_E2E_BLOCKS = 3;
|
|
63
64
|
function shouldRunE2E(state) {
|
|
64
65
|
return state.unitTestsGreen === true && state.e2eRun !== true;
|
|
65
66
|
}
|
|
67
|
+
function applyRecordedRun(state, run) {
|
|
68
|
+
let next = state;
|
|
69
|
+
if (run.unitTestPassed) {
|
|
70
|
+
next = { ...next, unitTestsGreen: true, e2eRun: false, e2eBlockCount: 0 };
|
|
71
|
+
}
|
|
72
|
+
if (run.e2eRan) {
|
|
73
|
+
next = { ...next, e2eRun: true };
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
function e2eGateDecision(state, maxBlocks = MAX_E2E_BLOCKS) {
|
|
78
|
+
const blockCount = state.e2eBlockCount ?? 0;
|
|
79
|
+
if (!shouldRunE2E(state)) return { action: "none" /* None */, blockCount };
|
|
80
|
+
if (blockCount >= maxBlocks) return { action: "release" /* Release */, blockCount };
|
|
81
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1 };
|
|
82
|
+
}
|
|
66
83
|
|
|
67
84
|
// src/guardrails/detectBuildIntent.ts
|
|
68
85
|
var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
|
|
@@ -74,6 +91,60 @@ function detectBuildIntent(prompt) {
|
|
|
74
91
|
if (QUESTION.test(p)) return false;
|
|
75
92
|
return BUILD.test(p) || DEVCYCLE.test(p);
|
|
76
93
|
}
|
|
94
|
+
var REPORT_SENTINEL = "muggle-pr-section";
|
|
95
|
+
var PR_POST_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
|
|
96
|
+
var defaultReader = (path, cwd) => {
|
|
97
|
+
try {
|
|
98
|
+
const abs = isAbsolute(path) ? path : resolve(cwd ?? process.cwd(), path);
|
|
99
|
+
if (!existsSync(abs)) return null;
|
|
100
|
+
return readFileSync(abs, "utf-8");
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
function unquote(s) {
|
|
106
|
+
const t = s.trim();
|
|
107
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
108
|
+
return t.slice(1, -1);
|
|
109
|
+
}
|
|
110
|
+
return t;
|
|
111
|
+
}
|
|
112
|
+
function looksLikeE2EReport(text) {
|
|
113
|
+
const t = text.toLowerCase();
|
|
114
|
+
const statusEmojis = (text.match(/[✅❌⚠]/gu) ?? []).length;
|
|
115
|
+
const tally = /\b\d+\s+(tests?\s+)?passed\b/.test(t) && /\b\d+\s+(tests?\s+)?(failed|inconclusive)\b/.test(t);
|
|
116
|
+
const slashTally = /\bpassed\b\s*[/|]\s*\d*\s*(tests?\s*)?\bfailed\b/.test(t) || /\bfailed\b\s*[/|]\s*\d*\s*(tests?\s*)?\bpassed\b/.test(t);
|
|
117
|
+
const resultsStructure = /acceptance results/.test(t) || tally || slashTally || statusEmojis >= 2;
|
|
118
|
+
const muggleContext = /\bmuggle\b/.test(t) || /muggle-ai\.com/.test(t) || /\be2e\b/.test(t) || /\bacceptance\b/.test(t);
|
|
119
|
+
return resultsStructure && muggleContext;
|
|
120
|
+
}
|
|
121
|
+
function collectInspectableText(cmd, cwd, read) {
|
|
122
|
+
let text = cmd;
|
|
123
|
+
for (const m of cmd.matchAll(/--body-file[=\s]+("[^"]+"|'[^']+'|\S+)/g)) {
|
|
124
|
+
const p = unquote(m[1]);
|
|
125
|
+
if (p && p !== "-") {
|
|
126
|
+
const c = read(p, cwd);
|
|
127
|
+
if (c) text += "\n" + c;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const m of cmd.matchAll(/jq\b[^|]*?("[^"]+\.json"|'[^']+\.json'|\S+\.json)/g)) {
|
|
131
|
+
const c = read(unquote(m[1]), cwd);
|
|
132
|
+
if (c) text += "\n" + c;
|
|
133
|
+
}
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
function evaluateReportPost(input2, read = defaultReader) {
|
|
137
|
+
if (input2.tool_name !== "Bash") return { deny: false };
|
|
138
|
+
const cmd = input2.tool_input?.command ?? "";
|
|
139
|
+
if (!PR_POST_CMD.test(cmd)) return { deny: false };
|
|
140
|
+
const text = collectInspectableText(cmd, input2.cwd, read);
|
|
141
|
+
if (text.includes(REPORT_SENTINEL)) return { deny: false };
|
|
142
|
+
if (!looksLikeE2EReport(text)) return { deny: false };
|
|
143
|
+
return {
|
|
144
|
+
deny: true,
|
|
145
|
+
reason: "Blocked: this looks like a hand-written E2E test report. Muggle requires the deterministic renderer \u2014 build the run's E2eReport JSON and pipe it through `muggle build-pr-section` (or invoke /muggle:muggle-pr-visual-walkthrough), then post that output. Never hand-write the walkthrough markdown."
|
|
146
|
+
};
|
|
147
|
+
}
|
|
77
148
|
|
|
78
149
|
// src/guardrails/emit.ts
|
|
79
150
|
function envelope(eventName, context, host2) {
|
|
@@ -83,6 +154,22 @@ function envelope(eventName, context, host2) {
|
|
|
83
154
|
hookSpecificOutput: { hookEventName: eventName, additionalContext: context }
|
|
84
155
|
});
|
|
85
156
|
}
|
|
157
|
+
function blockStop(reason, host2) {
|
|
158
|
+
if (!reason) return "{}";
|
|
159
|
+
if (host2 === "cursor") return JSON.stringify({ additional_context: reason });
|
|
160
|
+
return JSON.stringify({ decision: "block", reason });
|
|
161
|
+
}
|
|
162
|
+
function denyTool(reason, host2) {
|
|
163
|
+
if (!reason) return "{}";
|
|
164
|
+
if (host2 === "cursor") return JSON.stringify({ additional_context: reason });
|
|
165
|
+
return JSON.stringify({
|
|
166
|
+
hookSpecificOutput: {
|
|
167
|
+
hookEventName: "PreToolUse",
|
|
168
|
+
permissionDecision: "deny",
|
|
169
|
+
permissionDecisionReason: reason
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
86
173
|
|
|
87
174
|
// src/guardrails/cli.ts
|
|
88
175
|
function readStdin() {
|
|
@@ -108,25 +195,26 @@ Per the autoWatchPR preference, a muggle-pr-followup watcher should handle its i
|
|
|
108
195
|
function recordTests() {
|
|
109
196
|
const cmd = input.tool_input?.command ?? "";
|
|
110
197
|
const state = readState(sessionId);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (isE2ERun(input)) {
|
|
117
|
-
state.e2eRun = true;
|
|
118
|
-
changed = true;
|
|
119
|
-
}
|
|
120
|
-
if (changed) writeState(state);
|
|
198
|
+
const next = applyRecordedRun(state, {
|
|
199
|
+
unitTestPassed: isTestCommand(cmd) && testsPassed(input),
|
|
200
|
+
e2eRan: isE2ERun(input)
|
|
201
|
+
});
|
|
202
|
+
if (next !== state) writeState(next);
|
|
121
203
|
return "{}";
|
|
122
204
|
}
|
|
123
205
|
function e2eGate() {
|
|
124
206
|
const state = readState(sessionId);
|
|
125
|
-
|
|
126
|
-
|
|
207
|
+
const decision = e2eGateDecision(state);
|
|
208
|
+
if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) return "{}";
|
|
209
|
+
state.e2eBlockCount = decision.blockCount;
|
|
127
210
|
writeState(state);
|
|
128
|
-
const
|
|
129
|
-
return
|
|
211
|
+
const reason = `Do not end the turn yet. Unit tests passed this session but no E2E acceptance run has happened. Per the autoE2ETest preference (default: always), run change-driven E2E now via /muggle:muggle-test, then finish. If E2E genuinely cannot run here (no app, services down, no PR), say so explicitly to the user \u2014 this gate releases after ${MAX_E2E_BLOCKS} attempts.`;
|
|
212
|
+
return blockStop(reason, host);
|
|
213
|
+
}
|
|
214
|
+
function reportGate() {
|
|
215
|
+
const result = evaluateReportPost(input);
|
|
216
|
+
if (!result.deny || !result.reason) return "{}";
|
|
217
|
+
return denyTool(result.reason, host);
|
|
130
218
|
}
|
|
131
219
|
function buildRouter() {
|
|
132
220
|
if (!detectBuildIntent(input.prompt ?? "")) return "{}";
|
|
@@ -141,6 +229,7 @@ var handlers = {
|
|
|
141
229
|
"pr-opened": prOpened,
|
|
142
230
|
"record-tests": recordTests,
|
|
143
231
|
"e2e-gate": e2eGate,
|
|
232
|
+
"report-gate": reportGate,
|
|
144
233
|
"build-router": buildRouter
|
|
145
234
|
};
|
|
146
235
|
process.stdout.write((handlers[sub] ?? (() => "{}"))());
|
|
@@ -23,7 +23,7 @@ One local browser exists, so **execution is sequential** — one test case at a
|
|
|
23
23
|
Per test case, branch on `mode`:
|
|
24
24
|
|
|
25
25
|
**Replay**
|
|
26
|
-
1. `muggle-remote-test-script-get` (latest replayable script) → note `actionScriptId`.
|
|
26
|
+
1. `muggle-remote-test-script-get` (latest replayable script — the lane-scoped one the caller resolved during classification per [`../failure-mode-handling.md`](../failure-mode-handling.md) §A) → note `actionScriptId`.
|
|
27
27
|
2. `muggle-remote-action-script-get` with that id → full `actionScript` (see [`action-script.md`](action-script.md)).
|
|
28
28
|
3. `muggle-local-execute-replay` with `testScript`, `actionScript`, `localUrl`, `cwd`, `showUi`, `freshSession` (see [`fresh-session.md`](fresh-session.md)), `timeoutMs` (see [`timeouts.md`](timeouts.md)).
|
|
29
29
|
|
|
@@ -61,7 +61,7 @@ Run during change analysis, **per impacted test case**. Picks the initial execut
|
|
|
61
61
|
|
|
62
62
|
- The change summary from `git diff` (file paths + diff content).
|
|
63
63
|
- The test case (title, description, instructions, last passing run timestamp).
|
|
64
|
-
- Existing test scripts for that test case from `muggle-remote-test-script-list
|
|
64
|
+
- Existing test scripts for that test case from `muggle-remote-test-script-list`, scoped to the run's lane: pass `runEnvironmentType: "local"` in Local mode (Step 7A), `"remote"` in Remote mode (Step 7B). The lane is fixed at runtime — it selects the versioned runSettings the cloud resolves — so a local run keys replay-vs-regen off the localhost-lane script and a remote run off the deployed-lane script, never the other lane's.
|
|
65
65
|
|
|
66
66
|
### Rules (fire in order; first match wins)
|
|
67
67
|
|
|
@@ -98,7 +98,7 @@ Based on the changed files and the requirements goal, determine which test cases
|
|
|
98
98
|
|
|
99
99
|
### Step 4: Run the dev loop, gather screenshots
|
|
100
100
|
|
|
101
|
-
For each relevant test case, run the shared loop in [`../_shared/dev-loop/run.md`](../_shared/dev-loop/run.md): `muggle-remote-test-script-list` by `testCaseId` to pick [replay vs regen](../_shared/dev-loop/run.md), [execute with `timeoutMs`](../_shared/dev-loop/timeouts.md), [fetch the result](../_shared/dev-loop/failures.md) and [interpret failures](../_shared/dev-loop/failures.md), then read the studio-published [cloud refs and per-step screenshots](../_shared/dev-loop/publish.md) off the run result.
|
|
101
|
+
For each relevant test case, run the shared loop in [`../_shared/dev-loop/run.md`](../_shared/dev-loop/run.md): `muggle-remote-test-script-list` by `testCaseId` with `runEnvironmentType: "local"` (this stage runs against localhost) to pick [replay vs regen](../_shared/dev-loop/run.md), [execute with `timeoutMs`](../_shared/dev-loop/timeouts.md), [fetch the result](../_shared/dev-loop/failures.md) and [interpret failures](../_shared/dev-loop/failures.md), then read the studio-published [cloud refs and per-step screenshots](../_shared/dev-loop/publish.md) off the run result.
|
|
102
102
|
|
|
103
103
|
Inputs to the loop: `mode` from the script-exists check, `localUrl`/project from Step 1.7, `cwd` = the working tree recorded in `state.md`.
|
|
104
104
|
|
|
@@ -74,6 +74,7 @@ Tell the user: `Created test case: <id>`.
|
|
|
74
74
|
Load and call `muggle-remote-test-script-list` with:
|
|
75
75
|
- `projectId`: from Step 2
|
|
76
76
|
- `testCaseId`: from Step 4
|
|
77
|
+
- `runEnvironmentType`: `"local"` — this skill executes in the local browser, so it resolves the local-lane script. The lane is an explicit env type, exactly as the remote flow passes `"remote"`; the target URL is just the lane's url and never decides the lane.
|
|
77
78
|
|
|
78
79
|
Tell the user which mutations will be applied: `Mutations: <mutations[]>` (or "no mutations" if empty).
|
|
79
80
|
|
|
@@ -16,16 +16,19 @@ which uses `local` / `remote` / `ask`).
|
|
|
16
16
|
|
|
17
17
|
## Gate behavior
|
|
18
18
|
|
|
19
|
-
- `always` → take the pro-action, then print silent footer.
|
|
20
|
-
- `never` → take the skip-action, then print silent footer.
|
|
19
|
+
- `always` → take the pro-action, then **always** print the silent footer.
|
|
20
|
+
- `never` → take the skip-action, then **always** print the silent footer.
|
|
21
21
|
- `ask` (or absent) → run Picker 1 (per-key file) → Picker 2 (below).
|
|
22
22
|
|
|
23
23
|
`defaultExecutionMode` uses `local`/`remote` instead of `always`/`never`.
|
|
24
24
|
|
|
25
|
-
## Silent footer (whenever
|
|
25
|
+
## Silent footer (mandatory whenever a prompt is skipped)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
**
|
|
27
|
+
Whenever a gate resolves to a non-`ask` value and skips its picker, the footer
|
|
28
|
+
below is **required** — every gate, every time, no exceptions. It tells the
|
|
29
|
+
user **what happened**, **why it was silent**, and **how to change it**. The
|
|
30
|
+
`preference-gates-lint` test enforces that this contract exists; omitting the
|
|
31
|
+
footer when a gate fires is a bug. Two lines:
|
|
29
32
|
|
|
30
33
|
```
|
|
31
34
|
✓ <silent action from per-key file>
|
|
@@ -219,7 +219,7 @@ Wait for user confirmation before moving to execution.
|
|
|
219
219
|
|
|
220
220
|
### 6f: Classify execution mode per test case (replay vs regen)
|
|
221
221
|
|
|
222
|
-
For each selected test case, decide whether the run should be a **replay** of an existing script or a fresh **regen**, using the rules in [`_shared/failure-mode-handling.md`](../_shared/failure-mode-handling.md) section A. Inputs: the change summary from Step 2, the test case body, and the result of `muggle-remote-test-script-list` for that test case (last passing timestamp + whether any replayable script exists).
|
|
222
|
+
For each selected test case, decide whether the run should be a **replay** of an existing script or a fresh **regen**, using the rules in [`_shared/failure-mode-handling.md`](../_shared/failure-mode-handling.md) section A. Inputs: the change summary from Step 2, the test case body, and the result of `muggle-remote-test-script-list` for that test case (last passing timestamp + whether any replayable script exists). Scope that list call to the run's lane — `runEnvironmentType: "local"` in Local mode, `"remote"` in Remote mode (section A) — so replay reuses the script for the lane the run will execute against.
|
|
223
223
|
|
|
224
224
|
Per test case, fire one `muggle-local-telemetry-event-emit` with `eventType: "pre-execution-classification"` capturing the picked mode, the rule that fired, and the matched changed-file paths.
|
|
225
225
|
|
|
@@ -28,7 +28,7 @@ Branch each test case on its `mode`, then issue **all** workflow-start calls in
|
|
|
28
28
|
- `instructions`: From the test case
|
|
29
29
|
- `expectedResult`: From the test case
|
|
30
30
|
|
|
31
|
-
**Replay-mode test case** — `muggle-remote-workflow-start-test-script-replay` against the latest replayable script for that test case (resolve via `muggle-remote-test-script-list` if not already in hand from the classification step). Tag results with `mode: "replay"` so the router routes failures correctly.
|
|
31
|
+
**Replay-mode test case** — `muggle-remote-workflow-start-test-script-replay` against the latest replayable script for that test case (resolve via `muggle-remote-test-script-list` with `runEnvironmentType: "remote"` if not already in hand from the classification step). Tag results with `mode: "replay"` so the router routes failures correctly.
|
|
32
32
|
|
|
33
33
|
Store each returned workflow runtime ID along with its mode tag.
|
|
34
34
|
|
|
@@ -136,7 +136,7 @@ Before deciding the target's script, resolve its prerequisite chain from the bac
|
|
|
136
136
|
|
|
137
137
|
### 5. Existing scripts vs new generation
|
|
138
138
|
|
|
139
|
-
`muggle-remote-test-script-list` with `testCaseId
|
|
139
|
+
`muggle-remote-test-script-list` with `testCaseId` and `runEnvironmentType: "local"` — this skill always replays against localhost, so resolve the local-lane script (the remote-lane script carries the deployed URL).
|
|
140
140
|
|
|
141
141
|
- **If any replayable/succeeded scripts exist:** use `AskUserQuestion` to present them as clickable options. Show: name, created/updated, step count per option. Include **"Generate new script"** as the last option.
|
|
142
142
|
- **If none:** go straight to generation (no need to ask replay vs generate).
|