@muggleai/works 5.20.0-staging.142 → 5.20.0-staging.143
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/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/README.md +17 -2
- package/dist/plugin/hooks/hooks.json +4 -88
- package/dist/plugin/scripts/guardrail-build-followthrough-gate.sh +13 -23
- package/dist/plugin/scripts/guardrail-build-router.sh +12 -7
- package/dist/plugin/scripts/guardrail-capability-claim.sh +27 -17
- package/dist/plugin/scripts/guardrail-classify-gate.sh +12 -7
- package/dist/plugin/scripts/guardrail-comment-reply-gate.sh +15 -26
- package/dist/plugin/scripts/guardrail-debug-path-gate.sh +14 -24
- package/dist/plugin/scripts/guardrail-dispatch.sh +34 -0
- package/dist/plugin/scripts/guardrail-e2e-gate.sh +13 -21
- package/dist/plugin/scripts/guardrail-lib.sh +91 -0
- package/dist/plugin/scripts/guardrail-offer-ran.sh +12 -20
- package/dist/plugin/scripts/guardrail-pr-opened.sh +12 -7
- package/dist/plugin/scripts/guardrail-pr-terminal.sh +12 -7
- package/dist/plugin/scripts/guardrail-record-comment-replies.sh +12 -7
- package/dist/plugin/scripts/guardrail-record-stage-read.sh +12 -7
- package/dist/plugin/scripts/guardrail-record-stage-signals.sh +12 -7
- package/dist/plugin/scripts/guardrail-record-tests.sh +12 -7
- package/dist/plugin/scripts/guardrail-report-format.sh +12 -7
- package/dist/plugin/scripts/guardrail-resolve-gate.sh +12 -7
- package/dist/plugin/scripts/guardrail-skill-stages.sh +27 -18
- package/dist/plugin/scripts/guardrail-stage-gate.sh +14 -24
- package/dist/plugin/scripts/guardrail-terminal-gate.sh +13 -20
- package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +14 -22
- package/dist/plugin/scripts/guardrail-walkthrough-heading.sh +12 -7
- package/dist/plugin/scripts/guardrail-watch-gate.sh +14 -25
- package/dist/plugin/scripts/guardrails.mjs +29 -0
- package/dist/release-manifest.json +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/hooks/README.md +17 -2
- package/plugin/hooks/hooks.json +4 -88
- package/plugin/scripts/guardrail-build-followthrough-gate.sh +13 -23
- package/plugin/scripts/guardrail-build-router.sh +12 -7
- package/plugin/scripts/guardrail-capability-claim.sh +27 -17
- package/plugin/scripts/guardrail-classify-gate.sh +12 -7
- package/plugin/scripts/guardrail-comment-reply-gate.sh +15 -26
- package/plugin/scripts/guardrail-debug-path-gate.sh +14 -24
- package/plugin/scripts/guardrail-dispatch.sh +34 -0
- package/plugin/scripts/guardrail-e2e-gate.sh +13 -21
- package/plugin/scripts/guardrail-lib.sh +91 -0
- package/plugin/scripts/guardrail-offer-ran.sh +12 -20
- package/plugin/scripts/guardrail-pr-opened.sh +12 -7
- package/plugin/scripts/guardrail-pr-terminal.sh +12 -7
- package/plugin/scripts/guardrail-record-comment-replies.sh +12 -7
- package/plugin/scripts/guardrail-record-stage-read.sh +12 -7
- package/plugin/scripts/guardrail-record-stage-signals.sh +12 -7
- package/plugin/scripts/guardrail-record-tests.sh +12 -7
- package/plugin/scripts/guardrail-report-format.sh +12 -7
- package/plugin/scripts/guardrail-resolve-gate.sh +12 -7
- package/plugin/scripts/guardrail-skill-stages.sh +27 -18
- package/plugin/scripts/guardrail-stage-gate.sh +14 -24
- package/plugin/scripts/guardrail-terminal-gate.sh +13 -20
- package/plugin/scripts/guardrail-walkthrough-gate.sh +14 -22
- package/plugin/scripts/guardrail-walkthrough-heading.sh +12 -7
- package/plugin/scripts/guardrail-watch-gate.sh +14 -25
- package/plugin/scripts/guardrails.mjs +29 -0
|
@@ -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.20.0-staging.
|
|
4
|
+
"version": "5.20.0-staging.143",
|
|
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.20.0-staging.
|
|
5
|
+
"version": "5.20.0-staging.143",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
|
@@ -26,9 +26,9 @@ Enforcement is reserved for the handoffs that were being skipped: the E2E accept
|
|
|
26
26
|
|
|
27
27
|
A recorder that *clears* an obligation — a walkthrough posted, a failure diagnosed, a test case classified — records only when the call did not visibly fail (`callOutcome.ts`). Recording from the request alone let a rejected `gh pr comment` mark the walkthrough posted, so the gate went quiet on a PR that never received it.
|
|
28
28
|
|
|
29
|
-
Each guardrail is a
|
|
29
|
+
Each guardrail is a bash gate script in `../scripts/` holding a `guardrail_wants` pre-filter and the `guardrails.mjs` subcommand it feeds; the bundled `../scripts/guardrails.mjs` holds the decision logic (built from `src/guardrails/`, vitest-covered). The busy events — a shell command before and after it runs, and every turn end — register one `guardrail-dispatch.sh` command listing their gates, which reads the payload once, asks every gate's pre-filter in that one process, and starts Node once (`guardrails.mjs run <gates...>`) for just the gates that could fire, merging their responses. Rarer events call a gate script directly. 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; `guardrail-lib.sh` is the one place that falls back, and `run` isolates gates from each other.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Pre-filters are bash builtins only (`guardrail-lib.sh`), so an irrelevant event starts no process at all. A gate that has spent its block budget stamps a `<gate>Released` flag, which its wrapper then pre-filters on — without it a released gate keeps cold-starting Node on every remaining turn end to answer `{}`, and the walkthrough gate keeps making provider calls to do it. 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.
|
|
32
32
|
|
|
33
33
|
## Guardrails
|
|
34
34
|
|
|
@@ -70,3 +70,18 @@ The nudge counts only slots **this session owns**, and reports the rest as orpha
|
|
|
70
70
|
- Finalized watcher slots (`result.md` present) are pruned 30 days after finalize; their `followup.log` is forensic-only. An **open** slot — a PR watched for any length of time — has no `result.md` and is never touched.
|
|
71
71
|
|
|
72
72
|
Both windows are overridable (`MUGGLE_GUARDRAILS_TTL_DAYS`, `MUGGLE_SLOT_TTL_DAYS`). TTL-gated to once per day via a `~/.cache/muggle/state-gc-checked` marker (the current-session refresh runs every start regardless); silent and best-effort, never blocks session start. Never touches an open slot or the current session's own state.
|
|
73
|
+
|
|
74
|
+
## Footprint caps
|
|
75
|
+
|
|
76
|
+
Every hook here fires on every tool call, prompt or turn end in every open session, so its cost is paid thousands of times a day on the user's machine; on Windows each process start costs 50 ms healthy and seconds under load. The budget is lightweight by construction: one hook command per busy event, and nothing started beyond it when the event is irrelevant. `src/test/guardrails/footprint/` holds CI to it:
|
|
77
|
+
|
|
78
|
+
| Cap | Limit |
|
|
79
|
+
| :-- | :-- |
|
|
80
|
+
| Hook commands per busy slot (PreToolUse/PostToolUse on `Bash` and `Read`, Stop, UserPromptSubmit) | 1 |
|
|
81
|
+
| Programs started beyond each hook's own launch on an irrelevant event | 0 (Stop: 1, a transcript `tail` until the capability-claim gate has nudged; bash cannot seek) |
|
|
82
|
+
| `node`, `gh`, `python` on that path | 0 |
|
|
83
|
+
| Median wall time of one irrelevant firing, Linux/macOS | 100 ms |
|
|
84
|
+
| Hook commands in the whole manifest | 15 |
|
|
85
|
+
| Hook timeout | declared on every hook; 30 s, SessionStart 120 s |
|
|
86
|
+
|
|
87
|
+
Counts are exec counts, so they are identical on every runner. Every count is exact: CI fails when a change exceeds a limit, and also when it beats one until `footprint/constants.ts` is lowered to match, so a gain cannot be given back later.
|
|
@@ -31,19 +31,7 @@
|
|
|
31
31
|
"hooks": [
|
|
32
32
|
{
|
|
33
33
|
"type": "command",
|
|
34
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-report-format.sh
|
|
35
|
-
"async": false,
|
|
36
|
-
"timeout": 10
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
"type": "command",
|
|
40
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-resolve-gate.sh\"",
|
|
41
|
-
"async": false,
|
|
42
|
-
"timeout": 10
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"type": "command",
|
|
46
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-walkthrough-heading.sh\"",
|
|
34
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-dispatch.sh\" guardrail-report-format.sh guardrail-resolve-gate.sh guardrail-walkthrough-heading.sh",
|
|
47
35
|
"async": false,
|
|
48
36
|
"timeout": 10
|
|
49
37
|
}
|
|
@@ -67,33 +55,9 @@
|
|
|
67
55
|
"hooks": [
|
|
68
56
|
{
|
|
69
57
|
"type": "command",
|
|
70
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-pr-opened.sh
|
|
58
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-dispatch.sh\" guardrail-pr-opened.sh guardrail-record-tests.sh guardrail-pr-terminal.sh guardrail-record-stage-signals.sh guardrail-record-comment-replies.sh",
|
|
71
59
|
"async": false,
|
|
72
60
|
"timeout": 25
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"type": "command",
|
|
76
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-tests.sh\"",
|
|
77
|
-
"async": false,
|
|
78
|
-
"timeout": 10
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"type": "command",
|
|
82
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-pr-terminal.sh\"",
|
|
83
|
-
"async": false,
|
|
84
|
-
"timeout": 10
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"type": "command",
|
|
88
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-stage-signals.sh\"",
|
|
89
|
-
"async": false,
|
|
90
|
-
"timeout": 10
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"type": "command",
|
|
94
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-record-comment-replies.sh\"",
|
|
95
|
-
"async": false,
|
|
96
|
-
"timeout": 10
|
|
97
61
|
}
|
|
98
62
|
]
|
|
99
63
|
},
|
|
@@ -180,57 +144,9 @@
|
|
|
180
144
|
"hooks": [
|
|
181
145
|
{
|
|
182
146
|
"type": "command",
|
|
183
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-e2e-gate.sh
|
|
147
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-dispatch.sh\" guardrail-e2e-gate.sh guardrail-terminal-gate.sh guardrail-watch-gate.sh guardrail-build-followthrough-gate.sh guardrail-walkthrough-gate.sh guardrail-comment-reply-gate.sh guardrail-stage-gate.sh guardrail-debug-path-gate.sh guardrail-capability-claim.sh",
|
|
184
148
|
"async": false,
|
|
185
|
-
"timeout":
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
"type": "command",
|
|
189
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-terminal-gate.sh\"",
|
|
190
|
-
"async": false,
|
|
191
|
-
"timeout": 10
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
"type": "command",
|
|
195
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-watch-gate.sh\"",
|
|
196
|
-
"async": false,
|
|
197
|
-
"timeout": 10
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
"type": "command",
|
|
201
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-build-followthrough-gate.sh\"",
|
|
202
|
-
"async": false,
|
|
203
|
-
"timeout": 10
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
"type": "command",
|
|
207
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-walkthrough-gate.sh\"",
|
|
208
|
-
"async": false,
|
|
209
|
-
"timeout": 15
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
"type": "command",
|
|
213
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-comment-reply-gate.sh\"",
|
|
214
|
-
"async": false,
|
|
215
|
-
"timeout": 10
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
"type": "command",
|
|
219
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-stage-gate.sh\"",
|
|
220
|
-
"async": false,
|
|
221
|
-
"timeout": 10
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
"type": "command",
|
|
225
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-debug-path-gate.sh\"",
|
|
226
|
-
"async": false,
|
|
227
|
-
"timeout": 10
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
"type": "command",
|
|
231
|
-
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-capability-claim.sh\"",
|
|
232
|
-
"async": false,
|
|
233
|
-
"timeout": 10
|
|
149
|
+
"timeout": 30
|
|
234
150
|
}
|
|
235
151
|
]
|
|
236
152
|
}
|
|
@@ -15,30 +15,20 @@ set -uo pipefail
|
|
|
15
15
|
# overwhelming majority of turns no build intent was detected, so the state file
|
|
16
16
|
# is absent or the flag is unset and we return {} in-shell, never paying Node
|
|
17
17
|
# cold-start. Degrades to {}.
|
|
18
|
-
payload="$(cat)"
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
[
|
|
22
|
-
|
|
19
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
20
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
21
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
# macOS/Linux and on most Git Bash setups; fall back to converting USERPROFILE
|
|
26
|
-
# when HOME doesn't hold the state dir (some Windows shells point HOME elsewhere).
|
|
27
|
-
home="${HOME:-}"
|
|
28
|
-
if [ ! -d "$home/.muggle-ai" ] && command -v cygpath >/dev/null 2>&1 && [ -n "${USERPROFILE:-}" ]; then
|
|
29
|
-
home="$(cygpath -u "$USERPROFILE" 2>/dev/null || printf '%s' "$home")"
|
|
30
|
-
fi
|
|
23
|
+
GUARDRAIL_SUBCOMMAND="build-followthrough-gate"
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|| grep -q '"buildSkipped": true' "$state_file"; then
|
|
39
|
-
printf '{}'
|
|
40
|
-
exit 0
|
|
41
|
-
fi
|
|
25
|
+
guardrail_wants() {
|
|
26
|
+
guardrail_load_state \
|
|
27
|
+
&& guardrail_state_has '"buildIntentRouted": true' \
|
|
28
|
+
&& guardrail_state_has '"prsHandled": []' \
|
|
29
|
+
&& ! guardrail_state_has '"buildSkipped": true'
|
|
30
|
+
}
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
33
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
34
|
+
fi
|
|
@@ -12,12 +12,17 @@ set -uo pipefail
|
|
|
12
12
|
# then applies the real detectBuildIntent logic (question/slash exclusions,
|
|
13
13
|
# once-per-session dedupe). Over-matching here only costs an occasional needless
|
|
14
14
|
# spawn; it can never emit a spurious offer. Degrades to {} so it never blocks.
|
|
15
|
-
payload="$(cat)"
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
17
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
18
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
19
|
+
|
|
20
|
+
GUARDRAIL_SUBCOMMAND="build-router"
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
guardrail_wants() {
|
|
23
|
+
guardrail_payload_matches '(implement|build|add|create|write|fix|refactor|wire up|hook up|make|change the|conflict|merged|passing|green)'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
27
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
28
|
+
fi
|
|
@@ -15,24 +15,34 @@ set -uo pipefail
|
|
|
15
15
|
# from being "corrected". Over-matching here costs a needless spawn; it can
|
|
16
16
|
# never emit a spurious nudge. Degrades to {} so it never blocks on its own
|
|
17
17
|
# failure.
|
|
18
|
-
payload="$(cat)"
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/' \
|
|
24
|
-
| sed 's/\\\\/\//g')"
|
|
19
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
20
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
21
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
printf '{}'
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
23
|
+
GUARDRAIL_SUBCOMMAND="capability-claim-gate"
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
guardrail_wants() {
|
|
26
|
+
if guardrail_load_state && guardrail_state_has '"capabilityClaimNudged": true'; then
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
local transcript_pattern='"transcript_path"[[:space:]]*:[[:space:]]*"([^"]*)"'
|
|
30
|
+
[[ $GUARDRAIL_PAYLOAD =~ $transcript_pattern ]] || return 1
|
|
31
|
+
local json_escaped_separator='\\'
|
|
32
|
+
local transcript="${BASH_REMATCH[1]//"$json_escaped_separator"//}"
|
|
33
|
+
[[ -f $transcript ]] || return 1
|
|
34
|
+
# The one spawn left on a turn end: bash cannot seek, and reading a multi-MB transcript
|
|
35
|
+
# in-shell costs more than a tail. Gone for the session once the gate has nudged.
|
|
36
|
+
local transcript_tail
|
|
37
|
+
transcript_tail="$(tail -c 20000 "$transcript" 2>/dev/null)"
|
|
38
|
+
local claim_pattern='can.t|cannot|unable to|no way to|untestable|unverifiable|impossible|infeasible'
|
|
39
|
+
local matched=1
|
|
40
|
+
shopt -s nocasematch
|
|
41
|
+
[[ $transcript_tail =~ $claim_pattern ]] && matched=0
|
|
42
|
+
shopt -u nocasematch
|
|
43
|
+
return "$matched"
|
|
44
|
+
}
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
47
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
48
|
+
fi
|
|
@@ -12,12 +12,17 @@ set -uo pipefail
|
|
|
12
12
|
# unrelated payload never pays Node cold-start. It stays cheap by construction:
|
|
13
13
|
# an execution call happens minutes apart and burns a real browser, so the spawn
|
|
14
14
|
# is noise against what it protects. Degrades to {}.
|
|
15
|
-
payload="$(cat)"
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
17
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
18
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
19
|
+
|
|
20
|
+
GUARDRAIL_SUBCOMMAND="classify-gate"
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
guardrail_wants() {
|
|
23
|
+
guardrail_payload_matches 'muggle-local-(execute-test-generation|execute-replay)'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
27
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
28
|
+
fi
|
|
@@ -10,33 +10,22 @@ set -uo pipefail
|
|
|
10
10
|
# than the session state file, so the pre-filter keys on a ledger existing at
|
|
11
11
|
# all: with no ledger anywhere there is nothing this gate could owe, and we
|
|
12
12
|
# return {} in-shell without paying Node cold-start. Degrades to {}.
|
|
13
|
-
payload="$(cat)"
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
|
|
14
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
15
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
16
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
# macOS/Linux and on most Git Bash setups; fall back to converting USERPROFILE
|
|
21
|
-
# when HOME doesn't hold the state dir (some Windows shells point HOME elsewhere).
|
|
22
|
-
home="${HOME:-}"
|
|
23
|
-
if [ ! -d "$home/.muggle-ai" ] && command -v cygpath >/dev/null 2>&1 && [ -n "${USERPROFILE:-}" ]; then
|
|
24
|
-
home="$(cygpath -u "$USERPROFILE" 2>/dev/null || printf '%s' "$home")"
|
|
25
|
-
fi
|
|
18
|
+
GUARDRAIL_SUBCOMMAND="comment-reply-gate"
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
fi
|
|
36
|
-
if [ -f "$state_file" ] && grep -q '"commentReplyReleased": true' "$state_file"; then
|
|
37
|
-
printf '{}'
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
20
|
+
guardrail_wants() {
|
|
21
|
+
guardrail_resolve_home
|
|
22
|
+
local ledgers=("$GUARDRAIL_HOME"/.muggle-ai/muggle-do/sessions/*/comment-ledger.json)
|
|
23
|
+
[[ -e ${ledgers[0]} ]] || return 1
|
|
24
|
+
if guardrail_load_state; then
|
|
25
|
+
! guardrail_state_has '"commentReplySkipped": true' && ! guardrail_state_has '"commentReplyReleased": true'
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
30
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
31
|
+
fi
|
|
@@ -12,31 +12,21 @@ set -uo pipefail
|
|
|
12
12
|
# only when a failed run is recorded and unresolved. On the overwhelming majority
|
|
13
13
|
# of turns nothing failed, so the state file is absent or failedRuns is empty and
|
|
14
14
|
# we return {} in-shell. The evidence join runs in guardrails.mjs. Degrades to {}.
|
|
15
|
-
payload="$(cat)"
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
[
|
|
19
|
-
|
|
16
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
17
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
18
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
20
19
|
|
|
21
|
-
|
|
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
|
|
20
|
+
GUARDRAIL_SUBCOMMAND="debug-path-gate"
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|| grep -q '"debugSkipped": true' "$state_file"; then
|
|
37
|
-
printf '{}'
|
|
38
|
-
exit 0
|
|
39
|
-
fi
|
|
22
|
+
guardrail_wants() {
|
|
23
|
+
guardrail_load_state \
|
|
24
|
+
&& guardrail_state_has '"failedRuns"' \
|
|
25
|
+
&& ! guardrail_state_has '"failedRuns": []' \
|
|
26
|
+
&& ! guardrail_state_has '"debugReleased": true' \
|
|
27
|
+
&& ! guardrail_state_has '"debugSkipped": true'
|
|
28
|
+
}
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
31
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
32
|
+
fi
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# One hook command per event instead of one per gate. hooks.json calls this with the gate
|
|
5
|
+
# scripts that event feeds, in order:
|
|
6
|
+
#
|
|
7
|
+
# guardrail-dispatch.sh guardrail-e2e-gate.sh guardrail-watch-gate.sh ...
|
|
8
|
+
#
|
|
9
|
+
# It reads the payload once, sources each gate (defining its guardrail_wants pre-filter and
|
|
10
|
+
# GUARDRAIL_SUBCOMMAND without running it), and asks every pre-filter in this one process.
|
|
11
|
+
# Irrelevant events — nearly all of them — end here with {} and no process started. When any
|
|
12
|
+
# gate could fire, Node runs once for all of them (`guardrails.mjs run <gates...>`), merging
|
|
13
|
+
# their outputs the way the harness merged separate hooks.
|
|
14
|
+
|
|
15
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
16
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
17
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
18
|
+
|
|
19
|
+
guardrail_read_payload
|
|
20
|
+
|
|
21
|
+
firing_subcommands=()
|
|
22
|
+
for gate_script in "$@"; do
|
|
23
|
+
GUARDRAIL_SUBCOMMAND=""
|
|
24
|
+
. "$guardrail_script_dir/$gate_script" || continue
|
|
25
|
+
if [[ -n $GUARDRAIL_SUBCOMMAND ]] && guardrail_wants; then
|
|
26
|
+
firing_subcommands+=("$GUARDRAIL_SUBCOMMAND")
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
if [[ ${#firing_subcommands[@]} -eq 0 ]]; then
|
|
31
|
+
printf '{}'
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
guardrail_run_node run "${firing_subcommands[@]}"
|
|
@@ -14,28 +14,20 @@ set -uo pipefail
|
|
|
14
14
|
# overwhelming majority of turns (no test run, no PR) the state file is absent,
|
|
15
15
|
# or unitTestsGreen is unset while prsHandled is empty, so we return {} in-shell
|
|
16
16
|
# and never pay Node cold-start. Degrades to {}.
|
|
17
|
-
payload="$(cat)"
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
[
|
|
21
|
-
|
|
18
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
19
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
20
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
22
21
|
|
|
23
|
-
|
|
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
|
|
22
|
+
GUARDRAIL_SUBCOMMAND="e2e-gate"
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
exit 0
|
|
38
|
-
fi
|
|
24
|
+
guardrail_wants() {
|
|
25
|
+
guardrail_load_state \
|
|
26
|
+
&& { guardrail_state_has '"unitTestsGreen": true' || ! guardrail_state_has '"prsHandled": []'; } \
|
|
27
|
+
&& ! guardrail_state_has '"e2eReleased": true' \
|
|
28
|
+
&& ! guardrail_state_has '"e2eRun": true'
|
|
29
|
+
}
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
32
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
33
|
+
fi
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Spawn-free plumbing shared by every guardrail hook, sourced by the gate scripts and by
|
|
3
|
+
# guardrail-dispatch.sh. Hooks fire on every tool call, prompt and turn end in every open
|
|
4
|
+
# session, and on Windows each process start costs 50ms healthy and seconds under load, so
|
|
5
|
+
# everything on the "this event is irrelevant" path is a bash builtin: payload and state are
|
|
6
|
+
# read with `read`, matched with [[ =~ ]] and glob tests, and the session id is sanitised with
|
|
7
|
+
# parameter expansion. Node starts only once a gate's pre-filter says it could fire.
|
|
8
|
+
# src/test/guardrails/footprint caps this in CI.
|
|
9
|
+
|
|
10
|
+
[[ -n ${GUARDRAIL_LIB_LOADED:-} ]] && return 0
|
|
11
|
+
GUARDRAIL_LIB_LOADED=1
|
|
12
|
+
|
|
13
|
+
GUARDRAIL_PAYLOAD=""
|
|
14
|
+
GUARDRAIL_STATE=""
|
|
15
|
+
GUARDRAIL_STATE_LOADED=""
|
|
16
|
+
GUARDRAIL_HAS_STATE=""
|
|
17
|
+
|
|
18
|
+
guardrail_read_payload() {
|
|
19
|
+
IFS= read -r -d '' GUARDRAIL_PAYLOAD || true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Case-insensitive ERE over the whole payload. Unanchored, so it matches everything the
|
|
23
|
+
# per-line `grep -Eiq` it replaced did; an over-match only costs a Node start.
|
|
24
|
+
guardrail_payload_matches() {
|
|
25
|
+
local pattern="$1"
|
|
26
|
+
local matched=1
|
|
27
|
+
shopt -s nocasematch
|
|
28
|
+
[[ $GUARDRAIL_PAYLOAD =~ $pattern ]] && matched=0
|
|
29
|
+
shopt -u nocasematch
|
|
30
|
+
return "$matched"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# The home Node's os.homedir() resolves. HOME is right on macOS/Linux and most Git Bash
|
|
34
|
+
# setups; when it doesn't hold the state dir, USERPROFILE (C:\Users\x) is converted to its
|
|
35
|
+
# Git Bash form (/c/Users/x) in-shell rather than through cygpath.
|
|
36
|
+
guardrail_resolve_home() {
|
|
37
|
+
GUARDRAIL_HOME="${HOME:-}"
|
|
38
|
+
local profile="${USERPROFILE:-}"
|
|
39
|
+
if [[ ! -d $GUARDRAIL_HOME/.muggle-ai && $profile == [A-Za-z]:* ]]; then
|
|
40
|
+
local drive="${profile:0:1}"
|
|
41
|
+
local rest="${profile:2}"
|
|
42
|
+
# Lower-cased by lookup, not ${drive,,}: macOS ships bash 3.2.
|
|
43
|
+
local upper_letters="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
44
|
+
local lower_letters="abcdefghijklmnopqrstuvwxyz"
|
|
45
|
+
local letters_before="${upper_letters%%"$drive"*}"
|
|
46
|
+
if [[ $letters_before != "$upper_letters" ]]; then
|
|
47
|
+
drive="${lower_letters:${#letters_before}:1}"
|
|
48
|
+
fi
|
|
49
|
+
GUARDRAIL_HOME="/${drive}${rest//\\//}"
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Loads this session's guardrail state once per process; returns 1 when the session has none.
|
|
54
|
+
guardrail_load_state() {
|
|
55
|
+
if [[ -z $GUARDRAIL_STATE_LOADED ]]; then
|
|
56
|
+
GUARDRAIL_STATE_LOADED=1
|
|
57
|
+
local session_id_pattern='"session_id"[[:space:]]*:[[:space:]]*"([^"]*)"'
|
|
58
|
+
local raw_session_id="unknown"
|
|
59
|
+
if [[ $GUARDRAIL_PAYLOAD =~ $session_id_pattern ]] && [[ -n ${BASH_REMATCH[1]} ]]; then
|
|
60
|
+
raw_session_id="${BASH_REMATCH[1]}"
|
|
61
|
+
fi
|
|
62
|
+
guardrail_resolve_home
|
|
63
|
+
local state_file="$GUARDRAIL_HOME/.muggle-ai/guardrails/${raw_session_id//[^A-Za-z0-9_-]/_}.json"
|
|
64
|
+
if [[ -f $state_file ]]; then
|
|
65
|
+
GUARDRAIL_HAS_STATE=1
|
|
66
|
+
IFS= read -r -d '' GUARDRAIL_STATE <"$state_file" || true
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
[[ -n $GUARDRAIL_HAS_STATE ]]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Fixed-string test against the loaded state (which sessionState writes pretty-printed).
|
|
73
|
+
guardrail_state_has() {
|
|
74
|
+
[[ $GUARDRAIL_STATE == *"$1"* ]]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Runs guardrails.mjs on the payload. Never blocks a turn: any failure degrades to {}.
|
|
78
|
+
guardrail_run_node() {
|
|
79
|
+
local root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
80
|
+
printf '%s' "$GUARDRAIL_PAYLOAD" | node "${root}/scripts/guardrails.mjs" "$@" 2>/dev/null || printf '{}'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Entry for a gate script executed directly: its guardrail_wants decides whether Node runs.
|
|
84
|
+
guardrail_run_standalone() {
|
|
85
|
+
guardrail_read_payload
|
|
86
|
+
if guardrail_wants; then
|
|
87
|
+
guardrail_run_node "$1"
|
|
88
|
+
else
|
|
89
|
+
printf '{}'
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
@@ -9,27 +9,19 @@ set -uo pipefail
|
|
|
9
9
|
# Pre-filter: only spawn Node when the per-session state actually has a
|
|
10
10
|
# pending terminal PR, so the ordinary AskUserQuestion (no PR merged this
|
|
11
11
|
# session) never pays Node cold-start. Degrades to {}.
|
|
12
|
-
payload="$(cat)"
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
[
|
|
16
|
-
|
|
13
|
+
guardrail_script_dir="${BASH_SOURCE[0]%/*}"
|
|
14
|
+
[[ $guardrail_script_dir == "${BASH_SOURCE[0]}" ]] && guardrail_script_dir=.
|
|
15
|
+
. "$guardrail_script_dir/guardrail-lib.sh"
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
# macOS/Linux and on most Git Bash setups; fall back to converting USERPROFILE
|
|
20
|
-
# when HOME doesn't hold the state dir (some Windows shells point HOME elsewhere).
|
|
21
|
-
home="${HOME:-}"
|
|
22
|
-
if [ ! -d "$home/.muggle-ai" ] && command -v cygpath >/dev/null 2>&1 && [ -n "${USERPROFILE:-}" ]; then
|
|
23
|
-
home="$(cygpath -u "$USERPROFILE" 2>/dev/null || printf '%s' "$home")"
|
|
24
|
-
fi
|
|
17
|
+
GUARDRAIL_SUBCOMMAND="offer-ran"
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
exit 0
|
|
32
|
-
fi
|
|
19
|
+
guardrail_wants() {
|
|
20
|
+
guardrail_load_state \
|
|
21
|
+
&& guardrail_state_has '"terminalPending"' \
|
|
22
|
+
&& ! guardrail_state_has '"terminalPending": []'
|
|
23
|
+
}
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
|
26
|
+
guardrail_run_standalone "$GUARDRAIL_SUBCOMMAND"
|
|
27
|
+
fi
|