@muggleai/works 5.14.0-staging.96 → 5.14.0-staging.97
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/hooks/hooks.json +6 -0
- package/dist/plugin/scripts/guardrail-build-followthrough-gate.sh +44 -0
- package/dist/plugin/scripts/guardrails.mjs +33 -1
- package/dist/release-manifest.json +3 -3
- package/package.json +1 -1
- package/plugin/hooks/hooks.json +6 -0
- package/plugin/scripts/guardrail-build-followthrough-gate.sh +44 -0
- package/plugin/scripts/guardrails.mjs +33 -1
|
@@ -190,6 +190,12 @@
|
|
|
190
190
|
"async": false,
|
|
191
191
|
"timeout": 10
|
|
192
192
|
},
|
|
193
|
+
{
|
|
194
|
+
"type": "command",
|
|
195
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-build-followthrough-gate.sh\"",
|
|
196
|
+
"async": false,
|
|
197
|
+
"timeout": 10
|
|
198
|
+
},
|
|
193
199
|
{
|
|
194
200
|
"type": "command",
|
|
195
201
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-walkthrough-gate.sh\"",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# build-followthrough gate (Stop). When the front-door router took a
|
|
5
|
+
# build/implement/fix prompt this session but no PR was ever opened, block the
|
|
6
|
+
# turn end and point at /muggle-do (or the MUGGLE_BUILD_SKIP escape hatch). The
|
|
7
|
+
# router's offer is advisory and a session that finds the root cause can still
|
|
8
|
+
# end without shipping it — the fix then lives only in a transcript that dies
|
|
9
|
+
# with the session, and no other gate catches it: the watcher gate only fires on
|
|
10
|
+
# a PR that already exists.
|
|
11
|
+
#
|
|
12
|
+
# Mirrors guardrail-watch-gate.sh: synchronous (only a sync Stop hook can block
|
|
13
|
+
# the turn end), fires on EVERY turn end, and pre-filters in shell so Node spawns
|
|
14
|
+
# only when a build request was routed and no PR was handled. On the
|
|
15
|
+
# overwhelming majority of turns no build intent was detected, so the state file
|
|
16
|
+
# is absent or the flag is unset and we return {} in-shell, never paying Node
|
|
17
|
+
# cold-start. Degrades to {}.
|
|
18
|
+
payload="$(cat)"
|
|
19
|
+
|
|
20
|
+
raw_sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/')"
|
|
21
|
+
[ -n "$raw_sid" ] || raw_sid="unknown"
|
|
22
|
+
sid="$(printf '%s' "$raw_sid" | sed 's/[^A-Za-z0-9_-]/_/g')"
|
|
23
|
+
|
|
24
|
+
# Resolve the same home dir Node's os.homedir() uses. HOME is correct on
|
|
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
|
|
31
|
+
|
|
32
|
+
# A non-empty prsHandled array spans lines, so the empty match reliably says no
|
|
33
|
+
# PR was opened. Skip Node unless a build request was routed and nothing shipped.
|
|
34
|
+
state_file="$home/.muggle-ai/guardrails/$sid.json"
|
|
35
|
+
if [ ! -f "$state_file" ] \
|
|
36
|
+
|| ! grep -q '"buildIntentRouted": true' "$state_file" \
|
|
37
|
+
|| ! grep -q '"prsHandled": \[\]' "$state_file" \
|
|
38
|
+
|| grep -q '"buildSkipped": true' "$state_file"; then
|
|
39
|
+
printf '{}'
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
44
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" build-followthrough-gate 2>/dev/null || printf '{}'
|
|
@@ -13,6 +13,7 @@ var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
|
|
|
13
13
|
var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
|
|
14
14
|
var MAX_PR_TERMINAL_BLOCKS = 3;
|
|
15
15
|
var MAX_WATCH_BLOCKS = 3;
|
|
16
|
+
var MAX_BUILD_BLOCKS = 3;
|
|
16
17
|
var MAX_WALKTHROUGH_BLOCKS = 3;
|
|
17
18
|
var GH_LOOKUP_TIMEOUT_MS = 1e4;
|
|
18
19
|
var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
|
|
@@ -583,6 +584,25 @@ function watchGateDecision(state, untrackedPrUrls, maxBlocks = MAX_WATCH_BLOCKS)
|
|
|
583
584
|
untracked: untrackedPrUrls
|
|
584
585
|
};
|
|
585
586
|
}
|
|
587
|
+
|
|
588
|
+
// src/guardrails/buildFollowthrough.ts
|
|
589
|
+
var BUILD_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_BUILD_SKIP\b/;
|
|
590
|
+
function isBuildSkipMarker(cmd) {
|
|
591
|
+
return BUILD_SKIP_MARKER.test(cmd);
|
|
592
|
+
}
|
|
593
|
+
function applyBuildSkip(state, skipped) {
|
|
594
|
+
if (!skipped || state.buildSkipped === true) return state;
|
|
595
|
+
return { ...state, buildSkipped: true };
|
|
596
|
+
}
|
|
597
|
+
function buildFollowthroughDecision(state, maxBlocks = MAX_BUILD_BLOCKS) {
|
|
598
|
+
const blockCount = state.buildBlockCount ?? 0;
|
|
599
|
+
const owed = state.buildIntentRouted === true && state.buildSkipped !== true && state.prsHandled.length === 0;
|
|
600
|
+
if (!owed) return { action: "none" /* None */, blockCount };
|
|
601
|
+
if (blockCount >= maxBlocks) {
|
|
602
|
+
return { action: "release" /* Release */, blockCount };
|
|
603
|
+
}
|
|
604
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1 };
|
|
605
|
+
}
|
|
586
606
|
var REPORT_SENTINEL = "muggle-pr-section";
|
|
587
607
|
var PR_PROSE_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
|
|
588
608
|
var GH_API_CMD = /\bgh\s+api\b/;
|
|
@@ -1080,7 +1100,8 @@ function recordTests() {
|
|
|
1080
1100
|
e2eSkipped: isE2ESkipMarker(cmd)
|
|
1081
1101
|
});
|
|
1082
1102
|
const withWatchSkip = applyWatchSkip(recorded, isWatchSkipMarker(cmd));
|
|
1083
|
-
const
|
|
1103
|
+
const withBuildSkip = applyBuildSkip(withWatchSkip, isBuildSkipMarker(cmd));
|
|
1104
|
+
const withWalkthroughPost = applyWalkthroughPosted(withBuildSkip, detectWalkthroughPost(input));
|
|
1084
1105
|
const withWalkthroughSkip = applyWalkthroughSkip(withWalkthroughPost, isWalkthroughSkipMarker(cmd));
|
|
1085
1106
|
const failedRunId = detectFailedRunId(input);
|
|
1086
1107
|
const next = failedRunId ? applyFailedRun(withWalkthroughSkip, failedRunId) : withWalkthroughSkip;
|
|
@@ -1179,6 +1200,16 @@ function watchGate() {
|
|
|
1179
1200
|
const reason = decision.blockCount === 1 ? `Do not end the turn yet. A PR was opened this session but no muggle-do session slot tracks it: ${prList}. Seed the slot and hand off per muggle-do Stage 8 \u2014 /muggle:muggle-pr-followup ${decision.untracked[0]} does both. Seeding is what matters: once a slot exists, reconcile arms it at the next session start and finalizes it when the PR goes terminal, so an unarmed slot is fine but no slot means nothing ever picks this PR up. If it genuinely should not be tracked (autoWatchPR=never, handed off elsewhere), tell the user why and run \`echo "MUGGLE_WATCH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `PR hand-off still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WATCH_BLOCKS}): seed a slot via /muggle:muggle-pr-followup, or record a legitimate skip via \`echo "MUGGLE_WATCH_SKIP: <reason>"\`.`;
|
|
1180
1201
|
return blockStop(reason, host);
|
|
1181
1202
|
}
|
|
1203
|
+
function buildFollowthroughGate() {
|
|
1204
|
+
const state = readState(sessionId);
|
|
1205
|
+
const decision = buildFollowthroughDecision(state);
|
|
1206
|
+
if (decision.action === "release" /* Release */) return releaseGate("buildSkipped");
|
|
1207
|
+
if (decision.action === "none" /* None */) return "{}";
|
|
1208
|
+
state.buildBlockCount = decision.blockCount;
|
|
1209
|
+
writeState(state);
|
|
1210
|
+
const reason = decision.blockCount === 1 ? `Do not end the turn yet. This session took a build/implement/fix request but no PR was opened. A root cause written into the transcript ships nothing \u2014 once the session ends it is gone, and the watcher gate never fires because it only looks at PRs that exist. Carry the work to a PR via /muggle-do, which runs requirements \u2192 build \u2192 impact \u2192 unit tests \u2192 E2E \u2192 PR \u2192 watcher. If no PR is owed here (the user changed their mind, the fix landed in another repo, the answer was advice rather than a change), tell the user why and run \`echo "MUGGLE_BUILD_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Build request still unanswered \u2014 no PR opened (reminder ${decision.blockCount}/${MAX_BUILD_BLOCKS}): carry it to a PR via /muggle-do, or record a legitimate skip via \`echo "MUGGLE_BUILD_SKIP: <reason>"\`.`;
|
|
1211
|
+
return blockStop(reason, host);
|
|
1212
|
+
}
|
|
1182
1213
|
function walkthroughGate() {
|
|
1183
1214
|
const state = readState(sessionId);
|
|
1184
1215
|
if (state.e2eRun !== true || state.walkthroughPosted === true || state.walkthroughSkipped === true) {
|
|
@@ -1302,6 +1333,7 @@ var handlers = {
|
|
|
1302
1333
|
"e2e-gate": e2eGate,
|
|
1303
1334
|
"terminal-gate": terminalGate,
|
|
1304
1335
|
"watch-gate": watchGate,
|
|
1336
|
+
"build-followthrough-gate": buildFollowthroughGate,
|
|
1305
1337
|
"walkthrough-gate": walkthroughGate,
|
|
1306
1338
|
"record-comment-replies": recordCommentReplies,
|
|
1307
1339
|
"comment-reply-gate": commentReplyGate,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"release": "5.13.0",
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-09-05T00:
|
|
3
|
+
"buildId": "run-97-1",
|
|
4
|
+
"commitSha": "d59f28752c70cf7a5997accb6aabda88235570b7",
|
|
5
|
+
"buildTime": "2026-09-05T00:56:07Z",
|
|
6
6
|
"serviceName": "muggle-ai-works-mcp"
|
|
7
7
|
}
|
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.14.0-staging.
|
|
4
|
+
"version": "5.14.0-staging.97",
|
|
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",
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -190,6 +190,12 @@
|
|
|
190
190
|
"async": false,
|
|
191
191
|
"timeout": 10
|
|
192
192
|
},
|
|
193
|
+
{
|
|
194
|
+
"type": "command",
|
|
195
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-build-followthrough-gate.sh\"",
|
|
196
|
+
"async": false,
|
|
197
|
+
"timeout": 10
|
|
198
|
+
},
|
|
193
199
|
{
|
|
194
200
|
"type": "command",
|
|
195
201
|
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/guardrail-walkthrough-gate.sh\"",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# build-followthrough gate (Stop). When the front-door router took a
|
|
5
|
+
# build/implement/fix prompt this session but no PR was ever opened, block the
|
|
6
|
+
# turn end and point at /muggle-do (or the MUGGLE_BUILD_SKIP escape hatch). The
|
|
7
|
+
# router's offer is advisory and a session that finds the root cause can still
|
|
8
|
+
# end without shipping it — the fix then lives only in a transcript that dies
|
|
9
|
+
# with the session, and no other gate catches it: the watcher gate only fires on
|
|
10
|
+
# a PR that already exists.
|
|
11
|
+
#
|
|
12
|
+
# Mirrors guardrail-watch-gate.sh: synchronous (only a sync Stop hook can block
|
|
13
|
+
# the turn end), fires on EVERY turn end, and pre-filters in shell so Node spawns
|
|
14
|
+
# only when a build request was routed and no PR was handled. On the
|
|
15
|
+
# overwhelming majority of turns no build intent was detected, so the state file
|
|
16
|
+
# is absent or the flag is unset and we return {} in-shell, never paying Node
|
|
17
|
+
# cold-start. Degrades to {}.
|
|
18
|
+
payload="$(cat)"
|
|
19
|
+
|
|
20
|
+
raw_sid="$(printf '%s' "$payload" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)".*/\1/')"
|
|
21
|
+
[ -n "$raw_sid" ] || raw_sid="unknown"
|
|
22
|
+
sid="$(printf '%s' "$raw_sid" | sed 's/[^A-Za-z0-9_-]/_/g')"
|
|
23
|
+
|
|
24
|
+
# Resolve the same home dir Node's os.homedir() uses. HOME is correct on
|
|
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
|
|
31
|
+
|
|
32
|
+
# A non-empty prsHandled array spans lines, so the empty match reliably says no
|
|
33
|
+
# PR was opened. Skip Node unless a build request was routed and nothing shipped.
|
|
34
|
+
state_file="$home/.muggle-ai/guardrails/$sid.json"
|
|
35
|
+
if [ ! -f "$state_file" ] \
|
|
36
|
+
|| ! grep -q '"buildIntentRouted": true' "$state_file" \
|
|
37
|
+
|| ! grep -q '"prsHandled": \[\]' "$state_file" \
|
|
38
|
+
|| grep -q '"buildSkipped": true' "$state_file"; then
|
|
39
|
+
printf '{}'
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
44
|
+
printf '%s' "$payload" | node "${root}/scripts/guardrails.mjs" build-followthrough-gate 2>/dev/null || printf '{}'
|
|
@@ -13,6 +13,7 @@ var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
|
|
|
13
13
|
var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
|
|
14
14
|
var MAX_PR_TERMINAL_BLOCKS = 3;
|
|
15
15
|
var MAX_WATCH_BLOCKS = 3;
|
|
16
|
+
var MAX_BUILD_BLOCKS = 3;
|
|
16
17
|
var MAX_WALKTHROUGH_BLOCKS = 3;
|
|
17
18
|
var GH_LOOKUP_TIMEOUT_MS = 1e4;
|
|
18
19
|
var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
|
|
@@ -583,6 +584,25 @@ function watchGateDecision(state, untrackedPrUrls, maxBlocks = MAX_WATCH_BLOCKS)
|
|
|
583
584
|
untracked: untrackedPrUrls
|
|
584
585
|
};
|
|
585
586
|
}
|
|
587
|
+
|
|
588
|
+
// src/guardrails/buildFollowthrough.ts
|
|
589
|
+
var BUILD_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_BUILD_SKIP\b/;
|
|
590
|
+
function isBuildSkipMarker(cmd) {
|
|
591
|
+
return BUILD_SKIP_MARKER.test(cmd);
|
|
592
|
+
}
|
|
593
|
+
function applyBuildSkip(state, skipped) {
|
|
594
|
+
if (!skipped || state.buildSkipped === true) return state;
|
|
595
|
+
return { ...state, buildSkipped: true };
|
|
596
|
+
}
|
|
597
|
+
function buildFollowthroughDecision(state, maxBlocks = MAX_BUILD_BLOCKS) {
|
|
598
|
+
const blockCount = state.buildBlockCount ?? 0;
|
|
599
|
+
const owed = state.buildIntentRouted === true && state.buildSkipped !== true && state.prsHandled.length === 0;
|
|
600
|
+
if (!owed) return { action: "none" /* None */, blockCount };
|
|
601
|
+
if (blockCount >= maxBlocks) {
|
|
602
|
+
return { action: "release" /* Release */, blockCount };
|
|
603
|
+
}
|
|
604
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1 };
|
|
605
|
+
}
|
|
586
606
|
var REPORT_SENTINEL = "muggle-pr-section";
|
|
587
607
|
var PR_PROSE_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
|
|
588
608
|
var GH_API_CMD = /\bgh\s+api\b/;
|
|
@@ -1080,7 +1100,8 @@ function recordTests() {
|
|
|
1080
1100
|
e2eSkipped: isE2ESkipMarker(cmd)
|
|
1081
1101
|
});
|
|
1082
1102
|
const withWatchSkip = applyWatchSkip(recorded, isWatchSkipMarker(cmd));
|
|
1083
|
-
const
|
|
1103
|
+
const withBuildSkip = applyBuildSkip(withWatchSkip, isBuildSkipMarker(cmd));
|
|
1104
|
+
const withWalkthroughPost = applyWalkthroughPosted(withBuildSkip, detectWalkthroughPost(input));
|
|
1084
1105
|
const withWalkthroughSkip = applyWalkthroughSkip(withWalkthroughPost, isWalkthroughSkipMarker(cmd));
|
|
1085
1106
|
const failedRunId = detectFailedRunId(input);
|
|
1086
1107
|
const next = failedRunId ? applyFailedRun(withWalkthroughSkip, failedRunId) : withWalkthroughSkip;
|
|
@@ -1179,6 +1200,16 @@ function watchGate() {
|
|
|
1179
1200
|
const reason = decision.blockCount === 1 ? `Do not end the turn yet. A PR was opened this session but no muggle-do session slot tracks it: ${prList}. Seed the slot and hand off per muggle-do Stage 8 \u2014 /muggle:muggle-pr-followup ${decision.untracked[0]} does both. Seeding is what matters: once a slot exists, reconcile arms it at the next session start and finalizes it when the PR goes terminal, so an unarmed slot is fine but no slot means nothing ever picks this PR up. If it genuinely should not be tracked (autoWatchPR=never, handed off elsewhere), tell the user why and run \`echo "MUGGLE_WATCH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `PR hand-off still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WATCH_BLOCKS}): seed a slot via /muggle:muggle-pr-followup, or record a legitimate skip via \`echo "MUGGLE_WATCH_SKIP: <reason>"\`.`;
|
|
1180
1201
|
return blockStop(reason, host);
|
|
1181
1202
|
}
|
|
1203
|
+
function buildFollowthroughGate() {
|
|
1204
|
+
const state = readState(sessionId);
|
|
1205
|
+
const decision = buildFollowthroughDecision(state);
|
|
1206
|
+
if (decision.action === "release" /* Release */) return releaseGate("buildSkipped");
|
|
1207
|
+
if (decision.action === "none" /* None */) return "{}";
|
|
1208
|
+
state.buildBlockCount = decision.blockCount;
|
|
1209
|
+
writeState(state);
|
|
1210
|
+
const reason = decision.blockCount === 1 ? `Do not end the turn yet. This session took a build/implement/fix request but no PR was opened. A root cause written into the transcript ships nothing \u2014 once the session ends it is gone, and the watcher gate never fires because it only looks at PRs that exist. Carry the work to a PR via /muggle-do, which runs requirements \u2192 build \u2192 impact \u2192 unit tests \u2192 E2E \u2192 PR \u2192 watcher. If no PR is owed here (the user changed their mind, the fix landed in another repo, the answer was advice rather than a change), tell the user why and run \`echo "MUGGLE_BUILD_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Build request still unanswered \u2014 no PR opened (reminder ${decision.blockCount}/${MAX_BUILD_BLOCKS}): carry it to a PR via /muggle-do, or record a legitimate skip via \`echo "MUGGLE_BUILD_SKIP: <reason>"\`.`;
|
|
1211
|
+
return blockStop(reason, host);
|
|
1212
|
+
}
|
|
1182
1213
|
function walkthroughGate() {
|
|
1183
1214
|
const state = readState(sessionId);
|
|
1184
1215
|
if (state.e2eRun !== true || state.walkthroughPosted === true || state.walkthroughSkipped === true) {
|
|
@@ -1302,6 +1333,7 @@ var handlers = {
|
|
|
1302
1333
|
"e2e-gate": e2eGate,
|
|
1303
1334
|
"terminal-gate": terminalGate,
|
|
1304
1335
|
"watch-gate": watchGate,
|
|
1336
|
+
"build-followthrough-gate": buildFollowthroughGate,
|
|
1305
1337
|
"walkthrough-gate": walkthroughGate,
|
|
1306
1338
|
"record-comment-replies": recordCommentReplies,
|
|
1307
1339
|
"comment-reply-gate": commentReplyGate,
|