@muggleai/works 5.10.0 → 5.11.1
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-OCLMXAVJ.js → chunk-MSPMXBO5.js} +418 -326
- package/dist/{chunk-7NC2DJTK.js → chunk-WUMAJOBT.js} +9 -4
- 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/agents/test-prepare-runner.md +3 -1
- package/dist/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/dist/plugin/commands/mtest.md +1 -1
- package/dist/plugin/hooks/README.md +15 -3
- package/dist/plugin/hooks/hooks.json +79 -0
- package/dist/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/dist/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/dist/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/dist/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/dist/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/dist/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/dist/plugin/scripts/guardrail-report-format.sh +9 -5
- package/dist/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/dist/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/dist/plugin/scripts/guardrails.mjs +428 -35
- package/dist/plugin/scripts/pr-watch-events.sh +116 -0
- package/dist/plugin/scripts/pr-watch-guards.sh +8 -2
- package/dist/plugin/scripts/pr-watch-loop.sh +254 -0
- package/dist/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/dist/plugin/scripts/sign-body.sh +88 -0
- package/dist/plugin/skills/CLAUDE.md +16 -0
- package/dist/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/dist/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/dist/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/dist/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/dist/plugin/skills/do/open-prs/forward.md +2 -2
- package/dist/plugin/skills/do/per-comment-replies.md +2 -15
- package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/dist/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/dist/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/dist/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/dist/plugin/skills/muggle-test/SKILL.md +22 -2
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +3 -0
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +19 -12
- package/dist/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +13 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +3 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/dist/release-manifest.json +4 -4
- package/dist/src-B5OTAZKM.js +1 -0
- package/package.json +7 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/agents/test-prepare-runner.md +3 -1
- package/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/plugin/commands/mtest.md +1 -1
- package/plugin/hooks/README.md +15 -3
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/plugin/scripts/guardrail-report-format.sh +9 -5
- package/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/plugin/scripts/guardrails.mjs +428 -35
- package/plugin/scripts/pr-watch-events.sh +116 -0
- package/plugin/scripts/pr-watch-guards.sh +8 -2
- package/plugin/scripts/pr-watch-loop.sh +254 -0
- package/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/plugin/scripts/sign-body.sh +88 -0
- package/plugin/skills/CLAUDE.md +16 -0
- package/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/plugin/skills/do/open-prs/forward.md +2 -2
- package/plugin/skills/do/per-comment-replies.md +2 -15
- package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/plugin/skills/muggle-test/SKILL.md +22 -2
- package/plugin/skills/muggle-test-feature-local/SKILL.md +3 -0
- package/plugin/skills/muggle-test-prepare/SKILL.md +19 -12
- package/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +13 -2
- package/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +3 -1
- package/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/dist/src-AWUORQZZ.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, 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, reconcileProjectPreferences, getDataDir, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, 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-MSPMXBO5.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';
|
|
@@ -751,7 +751,7 @@ async function resolveGsScreenshotUrls(report, opts) {
|
|
|
751
751
|
if (gsUrls.length === 0) {
|
|
752
752
|
return report;
|
|
753
753
|
}
|
|
754
|
-
const mcps = await import('./src-
|
|
754
|
+
const mcps = await import('./src-B5OTAZKM.js');
|
|
755
755
|
const credentials = await mcps.getCallerCredentialsAsync();
|
|
756
756
|
if (!credentials.bearerToken && !credentials.apiKey) {
|
|
757
757
|
stderrWrite(
|
|
@@ -848,9 +848,14 @@ ${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
|
|
|
848
848
|
const resolvedReport = await resolveGsScreenshotUrls(report, { stderrWrite: opts.stderrWrite });
|
|
849
849
|
const sentinelCost = Buffer.byteLength(`${REPORT_SECTION_SENTINEL}
|
|
850
850
|
`, "utf-8");
|
|
851
|
-
const
|
|
851
|
+
const renderedSection = buildPrSection(resolvedReport, {
|
|
852
|
+
maxBodyBytes: opts.maxBodyBytes - sentinelCost
|
|
853
|
+
});
|
|
852
854
|
opts.stdoutWrite(
|
|
853
|
-
JSON.stringify({
|
|
855
|
+
JSON.stringify({
|
|
856
|
+
body: withSentinel(renderedSection.body),
|
|
857
|
+
comment: withSentinel(renderedSection.comment)
|
|
858
|
+
})
|
|
854
859
|
);
|
|
855
860
|
return 0;
|
|
856
861
|
}
|
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-WUMAJOBT.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-MSPMXBO5.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.11.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.11.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -38,7 +38,9 @@ Track every launched process in `/tmp/muggle-test-prepare.json` exactly per the
|
|
|
38
38
|
|
|
39
39
|
## Output contract
|
|
40
40
|
|
|
41
|
-
Return the readiness-report table verbatim as your report, prefixed by one line: `READY` (all services green), `DEGRADED: <which service, why>` (something is up but failed its smoke test after the clean-restart loop), or `needs-input: <decision>`.
|
|
41
|
+
Return the readiness-report table verbatim as your report, prefixed by one line: `READY` (all services green), `DEGRADED: <which service, why>` (something is up but failed its smoke test after the clean-restart loop), or `needs-input: <decision>`.
|
|
42
|
+
|
|
43
|
+
Also return every problem you hit and what cleared it — signal, service, resolution — per [`../skills/muggle-test-prepare/steps/record-resolution.md`](../skills/muggle-test-prepare/steps/record-resolution.md). On a learning run the dispatcher folds these into the recipe gate; on a replay they confirm the recipe still holds. A problem you could not clear is not a resolution: report it as an exclusion with its reason. You persist none of this yourself — the dispatcher owns every write. The dispatcher relays this to its caller — other skills gate on it, so a wrong `READY` is expensive; when in doubt between READY and DEGRADED, pick DEGRADED and say why. `needs-input:` is only for a decision the plan failed to resolve (a missing URL, an unapproved start, an unknown directory) — a service that stays broken after the loop is `DEGRADED` with the diagnosis, never `needs-input:`, even when no further automated fix exists. Repairing the app's own source code is out of scope entirely: a source-level bug surfaced by the smoke test is a `DEGRADED` diagnosis to report, not a decision to escalate.
|
|
42
44
|
|
|
43
45
|
## Guardrails
|
|
44
46
|
|
|
@@ -31,15 +31,22 @@ echo "$REPORT_JSON" | muggle build-pr-section > /tmp/muggle-pr-section.json
|
|
|
31
31
|
|
|
32
32
|
## Deliver
|
|
33
33
|
|
|
34
|
-
**Mode A (`post`)** —
|
|
34
|
+
**Mode A (`post`)** — deliver `body`, then `comment` only if non-null. Sign each posted body per [`../skills/_shared/vcs/post-signature.md`](../skills/_shared/vcs/post-signature.md) with `--mode plain` — this post is the walkthrough's own, so the command it names is `/muggle-pr-visual-walkthrough`.
|
|
35
|
+
|
|
36
|
+
**Update in place when this PR already carries a walkthrough.** A rerun after a failure must leave the PR with **one** walkthrough reflecting latest state, not a comment per attempt. Resolve which comment to update by reading the PR — never by remembering an id — so the behavior is idempotent across sessions and survives a lost session or a forgotten handle:
|
|
35
37
|
|
|
36
38
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
sign() { bash "${CLAUDE_PLUGIN_ROOT}/scripts/sign-body.sh" --command /muggle-pr-visual-walkthrough --mode plain; }
|
|
40
|
+
existing=$(gh api "repos/<owner>/<repo>/issues/<prNumber>/comments" \
|
|
41
|
+
--jq '[.[] | select(.body | contains("muggle-pr-section")) | .id] | join(" ")')
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
- `existing` empty → post fresh: `jq -r '.body' … | sign | gh pr comment <prNumber> --body-file -`, then the same for `.comment` when non-null.
|
|
45
|
+
- `existing` non-empty → update the first id with `body` via `gh api --method PATCH repos/<owner>/<repo>/issues/comments/<id> -F body=@-`, feeding the same signed text on stdin. Handle `comment` against the second id when both exist; post it fresh when the overflow is new, and delete a now-surplus overflow comment (`gh api --method DELETE …`) so a stale tail never outlives the run it described.
|
|
46
|
+
|
|
47
|
+
Match only comments carrying the sentinel — never every comment the loop user wrote — so an unrelated reply is never overwritten.
|
|
48
|
+
|
|
49
|
+
Report back: PR URL, whether an overflow comment was involved, and whether this was a fresh post or an update.
|
|
43
50
|
|
|
44
51
|
**Modes B/C (`render-for-new-pr` / `embed`)** — do not post, do not touch `gh`. Return the CLI output verbatim as your report:
|
|
45
52
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Real-browser E2E on your changes — no Playwright code, screenshots on the PR (alias for /muggle-test)
|
|
3
3
|
argument-hint: [optional context, e.g. "on staging"]
|
|
4
4
|
allowed-tools: [Skill]
|
|
5
5
|
---
|
|
@@ -26,24 +26,36 @@ Enforcement is reserved for the handoffs that were being skipped: the E2E accept
|
|
|
26
26
|
|
|
27
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.
|
|
28
28
|
|
|
29
|
+
Each wrapper short-circuits in shell first, so the common case never pays Node cold-start. That pre-filter is a second, looser copy of what `guardrails.mjs` matches, and it is the one place a guardrail can fail *silently*: a payload it drops — a skip marker, a reopen line, a comment edit — reaches no recorder, and the gate keeps demanding an action the user already took. Over-matching is free; under-matching is a dead escape hatch. `src/test/guardrails/hook-prefilter.test.ts` pins every payload each subcommand acts on against the wrapper guarding it, and derives the skip-marker tokens from source so a new marker is covered the moment it exists.
|
|
30
|
+
|
|
29
31
|
## Guardrails
|
|
30
32
|
|
|
31
33
|
| Hook event | Wrapper | Strength | Condition | Preference | Effect |
|
|
32
34
|
| :--------- | :------ | :------- | :-------- | :--------- | :----- |
|
|
33
35
|
| `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/skill-emit MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, an E2E run happened (execute/replay call, or the muggle-test skill's own telemetry emit — which registers a clean SKIP verdict too), or an `echo "MUGGLE_E2E_SKIP: <reason>"` marker declared E2E un-runnable | — | set `unitTestsGreen` / `e2eRun` / `e2eSkipped` session state |
|
|
36
|
+
| `PostToolUse` (Bash + muggle execute/replay/skill-emit MCP tools) | `guardrail-record-tests.sh` | record | a unit-test command passed, an E2E run happened (execute/replay call, or the muggle-test skill's own telemetry emit — which registers a clean SKIP verdict too), or an `echo "MUGGLE_E2E_SKIP: <reason>"` marker declared E2E un-runnable, or a walkthrough carrying the `build-pr-section` sentinel was posted / declared un-postable via `echo "MUGGLE_WALKTHROUGH_SKIP: <reason>"` | — | set `unitTestsGreen` / `e2eRun` / `e2eSkipped` / `walkthroughPosted` / `walkthroughSkipped` session state |
|
|
35
37
|
| `PostToolUse` (Bash + Monitor) | `guardrail-pr-terminal.sh` | advise | a PR just went terminal — a `gh pr merge`/`gh pr close` success line or the watch monitor's `TERMINAL pr=N` exit line (never bare `"state":"MERGED"` metadata) | — | record `terminalPending`, direct the post-merge handoff: finalize the watcher slot, tear down per `autoCleanup`, offer next options via AskUserQuestion |
|
|
36
38
|
| `PostToolUse` (AskUserQuestion) | `guardrail-offer-ran.sh` | record | a next-options offer ran while a terminal PR was pending | — | clear `terminalPending` — the only exit for the post-merge Stop gate |
|
|
37
|
-
| `
|
|
39
|
+
| `PostToolUse` (Skill + muggle telemetry-skill-emit) | `guardrail-skill-stages.sh` | advise | a skill this plugin ships was invoked; its `SKILL.md` frontmatter may declare `mandatoryStages`. Registered on the skill's own MCP announcement too, so the declaration still lands if the Skill payload ever stops carrying a name | — | record `lastInvokedSkillName` + `mandatoryStages`, and name the declared files as required reading at the moment of use |
|
|
40
|
+
| `PostToolUse` (Read) | `guardrail-record-stage-read.sh` | record | a markdown file under a `skills/` directory was opened | — | append to `stagesRead` — how the stage gate tells a stage that was read from one that was skipped |
|
|
41
|
+
| `PostToolUse` (Bash + muggle telemetry-event-emit / user-feedback-create) | `guardrail-record-stage-signals.sh` | record | a Step 6f `pre-execution-classification` emit, debug-path evidence for a failed run (a `*-failure-classified\|resolved` emit or feedback naming it), or a `MUGGLE_STAGE_SKIP` / `MUGGLE_CLASSIFY_SKIP` / `MUGGLE_DEBUG_SKIP` marker | — | set `classifiedTestCaseIds` / `debuggedRuns` / the matching skip flag |
|
|
42
|
+
| `PreToolUse` (Bash) | `guardrail-report-format.sh` | **enforce** | a `gh pr comment\|create\|edit` body — or a `gh api … issues/comments/<id>` PATCH editing one — reads like an E2E report but lacks the `build-pr-section` sentinel | — | **deny** — render via `muggle build-pr-section` instead |
|
|
43
|
+
| `PreToolUse` (muggle local execute/replay) | `guardrail-classify-gate.sh` | **enforce** | `muggle-test` is the skill in play and the target test case has no `pre-execution-classification` this session | — | **deny** — run Step 6f first (it calls `muggle-remote-test-script-list`, which is where the run learns the case has never passed) or record a legitimate skip via `MUGGLE_CLASSIFY_SKIP`. Scoped to `muggle-test`, so the single-target skills that legitimately skip classification are untouched; fails open when the test case can't be resolved |
|
|
38
44
|
| `Stop` | `guardrail-e2e-gate.sh` | **enforce** | unit tests passed this session, no E2E ran yet, and no skip was recorded | `autoE2ETest` | **block** the turn until E2E runs via `muggle-test` or a `MUGGLE_E2E_SKIP` marker records a legitimate skip (full message once, one-line reminders after; releases after 3 blocks) |
|
|
39
45
|
| `Stop` | `guardrail-terminal-gate.sh` | **enforce** | a PR went terminal this session and the AskUserQuestion next-options offer hasn't run since | — | **block** the turn until the post-merge handoff runs (full message once, one-line reminders after; releases after 3 blocks; nothing but the offer resets the counter) |
|
|
46
|
+
| `Stop` | `guardrail-watch-gate.sh` | **enforce** | a PR opened this session that no `muggle-do` session slot tracks | `autoWatchPR` | **block** the turn until a slot is seeded via `muggle-pr-followup` or a `MUGGLE_WATCH_SKIP` marker records a legitimate skip (releases after 3 blocks) |
|
|
47
|
+
| `Stop` | `guardrail-walkthrough-gate.sh` | **enforce** | an E2E acceptance run happened this session and a PR in play — opened this session or on the working branch — carries no walkthrough in its body or comments | `postPRVisualWalkthrough` | **block** the turn until the walkthrough is posted via `muggle-pr-visual-walkthrough` or a `MUGGLE_WALKTHROUGH_SKIP` marker records a legitimate skip (releases after 3 blocks; an unreachable PR fails open to not-owed) |
|
|
48
|
+
| `Stop` | `guardrail-stage-gate.sh` | **enforce** | a skill invoked this session declared `mandatoryStages` and one of those files was never opened | — | **block** the turn until the stages are read or a `MUGGLE_STAGE_SKIP` marker records a legitimate skip (releases after 3 blocks) |
|
|
49
|
+
| `Stop` | `guardrail-debug-path-gate.sh` | **enforce** | a local execution returned a non-passing run this session and nothing routed it through `_shared/debug-failed-run.md` | — | **block** the turn until the run is diagnosed (its `*-failure-classified` emit or feedback naming it) or a `MUGGLE_DEBUG_SKIP: <runId> <reason>` marker clears that run (releases after 3 blocks) |
|
|
40
50
|
| `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) |
|
|
41
51
|
|
|
42
52
|
## Session-start reconcile nudge
|
|
43
53
|
|
|
44
54
|
`SessionStart` (`scripts/reconcile-stale-watchers.sh`) — a standalone advisory, not part of the `guardrails.mjs` decision tree above.
|
|
45
55
|
|
|
46
|
-
`muggle-pr-followup` watchers are session-only (a monitor or `/loop` cron); they die with their session, leaving open PRs with no live poller. This is by design — a review is addressed only inside a session that carries the context to address it. The skill's [`reconcile`](../skills/muggle-pr-followup/reconcile.md) procedure recovers them at the next session start — finalizes slots whose PR went terminal, sweeps orphan crons, re-arms silently-stopped open watchers — but re-arming needs Claude tools a shell hook can't call. So this hook nudges rather than acts
|
|
56
|
+
`muggle-pr-followup` watchers are session-only (a monitor or `/loop` cron); they die with their session, leaving open PRs with no live poller. This is by design — a review is addressed only inside a session that carries the context to address it. The skill's [`reconcile`](../skills/muggle-pr-followup/reconcile.md) procedure recovers them at the next session start — finalizes slots whose PR went terminal, sweeps orphan crons, re-arms silently-stopped open watchers — but re-arming needs Claude tools a shell hook can't call. So this hook nudges rather than acts.
|
|
57
|
+
|
|
58
|
+
The nudge counts only slots **this session owns**, and reports the rest as orphans it will not act on — ownership and the recovery rules it gates are defined in [`reconcile`](../skills/muggle-pr-followup/reconcile.md). Nothing owned and nothing orphaned → it emits nothing. A pure directory scan (no `gh`, no writes), so it's cheap enough for every session start.
|
|
47
59
|
|
|
48
60
|
## Session-start state GC
|
|
49
61
|
|
|
@@ -36,6 +36,17 @@
|
|
|
36
36
|
"timeout": 10
|
|
37
37
|
}
|
|
38
38
|
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"matcher": "mcp__.*muggle.*(execute-test-generation|execute-replay)",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-classify-gate.sh\"",
|
|
46
|
+
"async": false,
|
|
47
|
+
"timeout": 10
|
|
48
|
+
}
|
|
49
|
+
]
|
|
39
50
|
}
|
|
40
51
|
],
|
|
41
52
|
"PostToolUse": [
|
|
@@ -59,6 +70,12 @@
|
|
|
59
70
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-pr-terminal.sh\"",
|
|
60
71
|
"async": false,
|
|
61
72
|
"timeout": 10
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "command",
|
|
76
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-stage-signals.sh\"",
|
|
77
|
+
"async": false,
|
|
78
|
+
"timeout": 10
|
|
62
79
|
}
|
|
63
80
|
]
|
|
64
81
|
},
|
|
@@ -73,6 +90,50 @@
|
|
|
73
90
|
}
|
|
74
91
|
]
|
|
75
92
|
},
|
|
93
|
+
{
|
|
94
|
+
"matcher": "mcp__.*muggle.*(telemetry-event-emit|user-feedback-create)",
|
|
95
|
+
"hooks": [
|
|
96
|
+
{
|
|
97
|
+
"type": "command",
|
|
98
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-stage-signals.sh\"",
|
|
99
|
+
"async": false,
|
|
100
|
+
"timeout": 10
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"matcher": "Skill",
|
|
106
|
+
"hooks": [
|
|
107
|
+
{
|
|
108
|
+
"type": "command",
|
|
109
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-skill-stages.sh\"",
|
|
110
|
+
"async": false,
|
|
111
|
+
"timeout": 10
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"matcher": "mcp__.*muggle.*telemetry-skill-emit",
|
|
117
|
+
"hooks": [
|
|
118
|
+
{
|
|
119
|
+
"type": "command",
|
|
120
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-skill-stages.sh\"",
|
|
121
|
+
"async": false,
|
|
122
|
+
"timeout": 10
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"matcher": "Read",
|
|
128
|
+
"hooks": [
|
|
129
|
+
{
|
|
130
|
+
"type": "command",
|
|
131
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-stage-read.sh\"",
|
|
132
|
+
"async": false,
|
|
133
|
+
"timeout": 10
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
76
137
|
{
|
|
77
138
|
"matcher": "Monitor",
|
|
78
139
|
"hooks": [
|
|
@@ -116,6 +177,24 @@
|
|
|
116
177
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-watch-gate.sh\"",
|
|
117
178
|
"async": false,
|
|
118
179
|
"timeout": 10
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "command",
|
|
183
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-walkthrough-gate.sh\"",
|
|
184
|
+
"async": false,
|
|
185
|
+
"timeout": 15
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"type": "command",
|
|
189
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-stage-gate.sh\"",
|
|
190
|
+
"async": false,
|
|
191
|
+
"timeout": 10
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"type": "command",
|
|
195
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-debug-path-gate.sh\"",
|
|
196
|
+
"async": false,
|
|
197
|
+
"timeout": 10
|
|
119
198
|
}
|
|
120
199
|
]
|
|
121
200
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# pre-execution-classification gate (PreToolUse/muggle local execution tools).
|
|
5
|
+
# Denies a generation or replay whose test case was never classified per
|
|
6
|
+
# muggle-test Step 6f. That step calls muggle-remote-test-script-list, which is
|
|
7
|
+
# where the run finds out the test case has never passed or has failed
|
|
8
|
+
# repeatedly — the check is free before dispatch and costs a full browser run
|
|
9
|
+
# once skipped.
|
|
10
|
+
#
|
|
11
|
+
# The pre-filter re-states the execution tool names the gate acts on, so an
|
|
12
|
+
# unrelated payload never pays Node cold-start. It stays cheap by construction:
|
|
13
|
+
# an execution call happens minutes apart and burns a real browser, so the spawn
|
|
14
|
+
# is noise against what it protects. Degrades to {}.
|
|
15
|
+
payload="$(cat)"
|
|
16
|
+
|
|
17
|
+
if ! grep -Eiq 'muggle-local-(execute-test-generation|execute-replay)' <<<"$payload"; then
|
|
18
|
+
printf '{}'
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
23
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" classify-gate 2>/dev/null || printf '{}'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# debug-path gate (Stop). When a run failed this session and never went through
|
|
5
|
+
# _shared/debug-failed-run.md, block the turn end naming the run (or the
|
|
6
|
+
# MUGGLE_DEBUG_SKIP escape hatch). muggle-test Step 7C marks that routing
|
|
7
|
+
# mandatory and it was still routinely skipped, so failures were summarized and
|
|
8
|
+
# dropped — the run a reviewer most needs to see is the one nobody looked at.
|
|
9
|
+
#
|
|
10
|
+
# Mirrors guardrail-watch-gate.sh: synchronous (only a sync Stop hook can block
|
|
11
|
+
# the turn end), fires on EVERY turn end, and pre-filters in shell so Node spawns
|
|
12
|
+
# only when a failed run is recorded and unresolved. On the overwhelming majority
|
|
13
|
+
# of turns nothing failed, so the state file is absent or failedRuns is empty and
|
|
14
|
+
# we return {} in-shell. The evidence join runs in guardrails.mjs. Degrades to {}.
|
|
15
|
+
payload="$(cat)"
|
|
16
|
+
|
|
17
|
+
raw_sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/')"
|
|
18
|
+
[ -n "$raw_sid" ] || raw_sid="unknown"
|
|
19
|
+
sid="$(printf '%s' "$raw_sid" | sed 's/[^A-Za-z0-9_-]/_/g')"
|
|
20
|
+
|
|
21
|
+
# Resolve the same home dir Node's os.homedir() uses. HOME is correct on
|
|
22
|
+
# macOS/Linux and on most Git Bash setups; fall back to converting USERPROFILE
|
|
23
|
+
# when HOME doesn't hold the state dir (some Windows shells point HOME elsewhere).
|
|
24
|
+
home="${HOME:-}"
|
|
25
|
+
if [ ! -d "$home/.muggle-ai" ] && command -v cygpath >/dev/null 2>&1 && [ -n "${USERPROFILE:-}" ]; then
|
|
26
|
+
home="$(cygpath -u "$USERPROFILE" 2>/dev/null || printf '%s' "$home")"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Empty array serializes as `"failedRuns": []` (one line); a non-empty array
|
|
30
|
+
# spans lines, so the empty match reliably tells them apart.
|
|
31
|
+
state_file="$home/.muggle-ai/guardrails/$sid.json"
|
|
32
|
+
if [ ! -f "$state_file" ] \
|
|
33
|
+
|| ! grep -q '"failedRuns"' "$state_file" \
|
|
34
|
+
|| grep -q '"failedRuns": \[\]' "$state_file" \
|
|
35
|
+
|| grep -q '"debugSkipped": true' "$state_file"; then
|
|
36
|
+
printf '{}'
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
41
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" debug-path-gate 2>/dev/null || printf '{}'
|
|
@@ -9,10 +9,14 @@ set -uo pipefail
|
|
|
9
9
|
# AskUserQuestion offer runs. Decision logic lives in the bundled guardrails.mjs.
|
|
10
10
|
#
|
|
11
11
|
# Fires after every Bash call, so a keyword pre-filter for the terminal output
|
|
12
|
-
# shapes keeps Node off the hot path.
|
|
12
|
+
# shapes keeps Node off the hot path. The reopen line belongs here too: it is
|
|
13
|
+
# the one signal that retracts a terminal verdict, and while the pre-filter
|
|
14
|
+
# dropped it a close+reopen — routine, to re-fire a lost workflow trigger — left
|
|
15
|
+
# the handoff armed on a change that is open again. Degrades to {} so it never
|
|
16
|
+
# blocks.
|
|
13
17
|
payload="$(cat)"
|
|
14
18
|
|
|
15
|
-
if ! grep -Eiq 'merged
|
|
19
|
+
if ! grep -Eiq '(merged|closed|reopened) pull request|TERMINAL pr=' <<<"$payload"; then
|
|
16
20
|
printf '{}'
|
|
17
21
|
exit 0
|
|
18
22
|
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# stage-read observer (PostToolUse/Read). Records that a file in the skills tree
|
|
5
|
+
# was opened, which is how the Stop gate tells a mandatory stage that was read
|
|
6
|
+
# from one that was skipped. Emits no directive.
|
|
7
|
+
#
|
|
8
|
+
# Read fires constantly, so only a markdown file living under a `skills/`
|
|
9
|
+
# directory reaches Node — a mandatory stage can be any file in that tree, but
|
|
10
|
+
# never one outside it, so source reads and repo docs return {} in-shell. Both
|
|
11
|
+
# path separators are accepted: a Windows payload carries escaped backslashes.
|
|
12
|
+
# Degrades to {}.
|
|
13
|
+
payload="$(cat)"
|
|
14
|
+
|
|
15
|
+
if ! grep -Eiq '"file_path"[[:space:]]*:[[:space:]]*"[^"]*[/\\]+skills[/\\]+[^"]*\.md"' <<<"$payload"; then
|
|
16
|
+
printf '{}'
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
21
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" record-stage-read 2>/dev/null || printf '{}'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# stage-signal observer (PostToolUse/Bash + muggle telemetry and feedback tools).
|
|
5
|
+
# Records the three things the stage gates settle from: a Step 6f
|
|
6
|
+
# pre-execution-classification emit, debug-path evidence for a failed run (a
|
|
7
|
+
# failure-classified emit or feedback naming it), and the skip markers each gate
|
|
8
|
+
# documents as its way out. Emits no directive — the gates read the state.
|
|
9
|
+
#
|
|
10
|
+
# The marker arm matches the MUGGLE_<GATE>_SKIP shape, never one token: a gate
|
|
11
|
+
# whose marker is missing from a hand-listed set instructs the user to run an
|
|
12
|
+
# echo that can never register, then blocks the turn anyway. Anchoring the
|
|
13
|
+
# marker to a leading `echo` stays in guardrails.mjs; over-matching here only
|
|
14
|
+
# costs a needless spawn. Degrades to {}.
|
|
15
|
+
payload="$(cat)"
|
|
16
|
+
|
|
17
|
+
if ! grep -Eiq 'MUGGLE_[A-Z0-9_]+_SKIP|muggle.*(telemetry-event-emit|user-feedback-create)' <<<"$payload"; then
|
|
18
|
+
printf '{}'
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
23
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" record-stage-signals 2>/dev/null || printf '{}'
|
|
@@ -10,12 +10,19 @@ set -uo pipefail
|
|
|
10
10
|
# pre-filter for test runners and the muggle E2E tool names keeps Node off the
|
|
11
11
|
# hot path. Only a `test` command (npm/pnpm/yarn/jest/vitest/pytest/go/cargo),
|
|
12
12
|
# a muggle execute/replay/test-generation event, a muggle-test skill telemetry
|
|
13
|
-
# emit (registers a clean-SKIP verdict as an E2E run),
|
|
14
|
-
# reaches guardrails.mjs, which then
|
|
15
|
-
# updates state. Degrades to {}.
|
|
13
|
+
# emit (registers a clean-SKIP verdict as an E2E run), a skip marker, or a PR
|
|
14
|
+
# publish carrying a rendered walkthrough reaches guardrails.mjs, which then
|
|
15
|
+
# inspects the output for pass/fail and updates state. Degrades to {}.
|
|
16
|
+
#
|
|
17
|
+
# The marker arm matches the MUGGLE_<GATE>_SKIP shape, never one token: every
|
|
18
|
+
# Stop gate documents its own marker as the escape hatch, and a per-token list
|
|
19
|
+
# left the watcher and walkthrough markers unreachable — the gates instructed
|
|
20
|
+
# the user to run an echo that could never reach the recorder, then blocked the
|
|
21
|
+
# turn anyway. Anchoring the marker to a leading `echo` stays in guardrails.mjs;
|
|
22
|
+
# over-matching here only costs a needless spawn.
|
|
16
23
|
payload="$(cat)"
|
|
17
24
|
|
|
18
|
-
if ! grep -Eiq '(pnpm|npm|yarn)[[:space:]]+(run[[:space:]]+)?test|jest|vitest|pytest|go[[:space:]]+test|cargo[[:space:]]+test|muggle.*(execute|test-generation|replay)|muggle-local-telemetry-skill-emit|
|
|
25
|
+
if ! grep -Eiq '(pnpm|npm|yarn)[[:space:]]+(run[[:space:]]+)?test|jest|vitest|pytest|go[[:space:]]+test|cargo[[:space:]]+test|muggle.*(execute|test-generation|replay)|muggle-local-telemetry-skill-emit|MUGGLE_[A-Z0-9_]+_SKIP|gh[[:space:]]+pr[[:space:]]+(comment|create|edit)|issues/comments/[0-9]' <<<"$payload"; then
|
|
19
26
|
printf '{}'
|
|
20
27
|
exit 0
|
|
21
28
|
fi
|
|
@@ -7,14 +7,18 @@ set -uo pipefail
|
|
|
7
7
|
# deterministic renderer.
|
|
8
8
|
#
|
|
9
9
|
# This must stay synchronous (only a sync PreToolUse hook can deny), and it fires
|
|
10
|
-
# before every Bash call. A keyword pre-filter for the
|
|
10
|
+
# before every Bash call. A keyword pre-filter for the PR-publishing commands
|
|
11
11
|
# keeps Node off the hot path: a plain `ls`/`git status`/build command returns {}
|
|
12
|
-
# in-shell and never pays cold-start. Only a `gh pr comment|create|edit`
|
|
13
|
-
# guardrails.mjs, which reads the body (incl.
|
|
14
|
-
#
|
|
12
|
+
# in-shell and never pays cold-start. Only a `gh pr comment|create|edit` or a
|
|
13
|
+
# `gh api` comment edit reaches guardrails.mjs, which reads the body (incl.
|
|
14
|
+
# --body-file) and decides. The comment-edit arm matters as much as the post
|
|
15
|
+
# arm: guardrails.mjs gates that path precisely so a sanctioned walkthrough
|
|
16
|
+
# can't be overwritten with hand-written markdown afterwards, and a pre-filter
|
|
17
|
+
# on `gh pr` alone left that bypass wide open. Degrades to {} so it never blocks
|
|
18
|
+
# an unrelated command.
|
|
15
19
|
payload="$(cat)"
|
|
16
20
|
|
|
17
|
-
if ! grep -Eiq 'gh[[:space:]]+pr[[:space:]]+(comment|create|edit)' <<<"$payload"; then
|
|
21
|
+
if ! grep -Eiq 'gh[[:space:]]+pr[[:space:]]+(comment|create|edit)|issues/comments/[0-9]' <<<"$payload"; then
|
|
18
22
|
printf '{}'
|
|
19
23
|
exit 0
|
|
20
24
|
fi
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# mandatory-stage recorder (PostToolUse/Skill). Records which skill is running
|
|
5
|
+
# and, when its SKILL.md declares `mandatoryStages`, returns those files as
|
|
6
|
+
# required reading at the moment of use. The Stop gate
|
|
7
|
+
# (guardrail-stage-gate.sh) blocks the turn end while any of them is unread.
|
|
8
|
+
#
|
|
9
|
+
# Skill calls are rare, so the pre-filter is a file test rather than a keyword
|
|
10
|
+
# scan: the invoked name is pulled out of the payload and Node runs only when it
|
|
11
|
+
# resolves to a skill this plugin actually ships. Another plugin's skill, or an
|
|
12
|
+
# input with no skill name, returns {} in-shell. Every key the resolver reads is
|
|
13
|
+
# accepted here — a key matched in one and missed in the other makes the gate
|
|
14
|
+
# dead code on exactly the harness that names it that way. Degrades to {}.
|
|
15
|
+
payload="$(cat)"
|
|
16
|
+
|
|
17
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
18
|
+
|
|
19
|
+
for candidate in $(printf '%s' "$payload" \
|
|
20
|
+
| grep -oE '"(skill|skillName|name|command)"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
21
|
+
| sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/'); do
|
|
22
|
+
# A namespaced invocation (`muggle:muggle-test`) and a slash command
|
|
23
|
+
# (`/muggle-test`) both name the same skill directory.
|
|
24
|
+
skill="${candidate##*:}"
|
|
25
|
+
skill="${skill#/}"
|
|
26
|
+
case "$skill" in
|
|
27
|
+
"" | *[!A-Za-z0-9._-]*) continue ;;
|
|
28
|
+
esac
|
|
29
|
+
if [ -f "$root/skills/$skill/SKILL.md" ]; then
|
|
30
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" skill-stages 2>/dev/null || printf '{}'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
printf '{}'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# mandatory-stage gate (Stop). When a skill invoked this session declared
|
|
5
|
+
# `mandatoryStages` in its SKILL.md and any of those files was never opened,
|
|
6
|
+
# block the turn end naming them (or the MUGGLE_STAGE_SKIP escape hatch). This
|
|
7
|
+
# is the root fix for a skill read as a single page: the SKILL.md links out to
|
|
8
|
+
# steps that are mandatory, nothing forced them open, and the steps were
|
|
9
|
+
# silently dropped.
|
|
10
|
+
#
|
|
11
|
+
# Mirrors guardrail-watch-gate.sh: synchronous (only a sync Stop hook can block
|
|
12
|
+
# the turn end), fires on EVERY turn end, and pre-filters in shell so Node spawns
|
|
13
|
+
# only when a stage is actually owed. On the overwhelming majority of turns no
|
|
14
|
+
# skill declared stages, so the state file is absent or mandatoryStages is empty
|
|
15
|
+
# and we return {} in-shell. The read-vs-declared comparison runs in
|
|
16
|
+
# guardrails.mjs. Degrades to {}.
|
|
17
|
+
payload="$(cat)"
|
|
18
|
+
|
|
19
|
+
raw_sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/')"
|
|
20
|
+
[ -n "$raw_sid" ] || raw_sid="unknown"
|
|
21
|
+
sid="$(printf '%s' "$raw_sid" | sed 's/[^A-Za-z0-9_-]/_/g')"
|
|
22
|
+
|
|
23
|
+
# Resolve the same home dir Node's os.homedir() uses. HOME is correct on
|
|
24
|
+
# macOS/Linux and on most Git Bash setups; fall back to converting USERPROFILE
|
|
25
|
+
# when HOME doesn't hold the state dir (some Windows shells point HOME elsewhere).
|
|
26
|
+
home="${HOME:-}"
|
|
27
|
+
if [ ! -d "$home/.muggle-ai" ] && command -v cygpath >/dev/null 2>&1 && [ -n "${USERPROFILE:-}" ]; then
|
|
28
|
+
home="$(cygpath -u "$USERPROFILE" 2>/dev/null || printf '%s' "$home")"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Empty array serializes as `"mandatoryStages": []` (one line); a non-empty array
|
|
32
|
+
# spans lines, so the empty match reliably tells them apart.
|
|
33
|
+
state_file="$home/.muggle-ai/guardrails/$sid.json"
|
|
34
|
+
if [ ! -f "$state_file" ] \
|
|
35
|
+
|| ! grep -q '"mandatoryStages"' "$state_file" \
|
|
36
|
+
|| grep -q '"mandatoryStages": \[\]' "$state_file" \
|
|
37
|
+
|| grep -q '"stageSkipped": true' "$state_file"; then
|
|
38
|
+
printf '{}'
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
43
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" stage-gate 2>/dev/null || printf '{}'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# acceptance-run → walkthrough gate (Stop). When an E2E acceptance run happened
|
|
5
|
+
# this session and no visual walkthrough has reached the PR, block the turn end
|
|
6
|
+
# until it does or a skip is declared. Fires once per session per PR.
|
|
7
|
+
#
|
|
8
|
+
# This must stay synchronous (only a sync Stop hook can block the turn end), and
|
|
9
|
+
# it fires on EVERY turn end. There is no command payload to key off, so the
|
|
10
|
+
# pre-filter reads the same per-session state file guardrails.mjs uses and only
|
|
11
|
+
# spawns Node when the gate could actually fire — i.e. an acceptance run is
|
|
12
|
+
# recorded and the walkthrough is neither posted nor skipped. On the
|
|
13
|
+
# overwhelming majority of turns (no E2E this session) the state file is absent
|
|
14
|
+
# or e2eRun is unset, so we return {} in-shell and never pay Node cold-start —
|
|
15
|
+
# which also means the gate's `gh` lookups only ever run on turns that could
|
|
16
|
+
# genuinely owe a walkthrough. Degrades to {}.
|
|
17
|
+
payload="$(cat)"
|
|
18
|
+
|
|
19
|
+
raw_sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/')"
|
|
20
|
+
[ -n "$raw_sid" ] || raw_sid="unknown"
|
|
21
|
+
sid="$(printf '%s' "$raw_sid" | sed 's/[^A-Za-z0-9_-]/_/g')"
|
|
22
|
+
|
|
23
|
+
# Resolve the same home dir Node's os.homedir() uses. HOME is correct on
|
|
24
|
+
# macOS/Linux and on most Git Bash setups; fall back to converting USERPROFILE
|
|
25
|
+
# when HOME doesn't hold the state dir (some Windows shells point HOME elsewhere).
|
|
26
|
+
home="${HOME:-}"
|
|
27
|
+
if [ ! -d "$home/.muggle-ai" ] && command -v cygpath >/dev/null 2>&1 && [ -n "${USERPROFILE:-}" ]; then
|
|
28
|
+
home="$(cygpath -u "$USERPROFILE" 2>/dev/null || printf '%s' "$home")"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
state_file="$home/.muggle-ai/guardrails/$sid.json"
|
|
32
|
+
if [ ! -f "$state_file" ] \
|
|
33
|
+
|| ! grep -q '"e2eRun": true' "$state_file" \
|
|
34
|
+
|| grep -q '"walkthroughPosted": true' "$state_file" \
|
|
35
|
+
|| grep -q '"walkthroughSkipped": true' "$state_file"; then
|
|
36
|
+
printf '{}'
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
41
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" walkthrough-gate 2>/dev/null || printf '{}'
|