@muggleai/works 5.30.0-staging.263 → 5.30.0-staging.264
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/.claude-plugin/marketplace.json +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.codex-mcp.json +1 -1
- package/dist/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/.mcp.json +1 -1
- package/dist/plugin/hooks/hooks.json +14 -0
- package/dist/plugin/scripts/guardrail-lib.sh +7 -0
- package/dist/plugin/scripts/guardrail-rewake.sh +40 -0
- package/dist/plugin/scripts/guardrails.mjs +150 -7
- package/dist/release-manifest.json +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-mcp.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/hooks/hooks.json +14 -0
- package/plugin/scripts/guardrail-lib.sh +7 -0
- package/plugin/scripts/guardrail-rewake.sh +40 -0
- package/plugin/scripts/guardrails.mjs +150 -7
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "muggleai",
|
|
10
10
|
"source": "./plugin",
|
|
11
11
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
12
|
-
"version": "5.30.0-staging.
|
|
12
|
+
"version": "5.30.0-staging.264"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
4
|
-
"version": "5.30.0-staging.
|
|
4
|
+
"version": "5.30.0-staging.264",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.30.0-staging.
|
|
3
|
+
"version": "5.30.0-staging.264",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
|
-
"version": "5.30.0-staging.
|
|
5
|
+
"version": "5.30.0-staging.264",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/dist/plugin/.mcp.json
CHANGED
|
@@ -244,6 +244,13 @@
|
|
|
244
244
|
"command": "set -- guardrail-dispatch.sh guardrail-e2e-gate.sh guardrail-terminal-gate.sh guardrail-watch-gate.sh guardrail-watch-follow-through-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; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
245
245
|
"async": false,
|
|
246
246
|
"timeout": 1
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"type": "command",
|
|
250
|
+
"command": "set -- guardrail-rewake.sh guardrail-e2e-gate.sh guardrail-terminal-gate.sh guardrail-watch-gate.sh guardrail-watch-follow-through-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; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
251
|
+
"async": true,
|
|
252
|
+
"asyncRewake": true,
|
|
253
|
+
"timeout": 30
|
|
247
254
|
}
|
|
248
255
|
]
|
|
249
256
|
}
|
|
@@ -256,6 +263,13 @@
|
|
|
256
263
|
"command": "set -- guardrail-build-router.sh; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
257
264
|
"async": false,
|
|
258
265
|
"timeout": 1
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"type": "command",
|
|
269
|
+
"command": "set -- guardrail-rewake.sh guardrail-build-router.sh; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
270
|
+
"async": true,
|
|
271
|
+
"asyncRewake": true,
|
|
272
|
+
"timeout": 30
|
|
259
273
|
}
|
|
260
274
|
]
|
|
261
275
|
}
|
|
@@ -150,6 +150,13 @@ guardrail_run_node() {
|
|
|
150
150
|
node "${root}/scripts/guardrails.mjs" "$@" <<<"$GUARDRAIL_PAYLOAD" 2>/dev/null || printf '{}'
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
# Runs guardrails.mjs for an asyncRewake companion, which answers through its stderr and exit status rather than
|
|
154
|
+
# stdout: exit 2 is what makes Claude Code wake the session with that stderr, so neither is swallowed here.
|
|
155
|
+
guardrail_run_rewake_node() {
|
|
156
|
+
local root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
157
|
+
node "${root}/scripts/guardrails.mjs" "$@" <<<"$GUARDRAIL_PAYLOAD"
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
# Entry for a gate script executed directly: its guardrail_wants decides whether Node runs.
|
|
154
161
|
guardrail_run_standalone() {
|
|
155
162
|
guardrail_read_payload
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# Async companion of a synchronous guardrail hook (Stop, UserPromptSubmit). hooks.json runs it beside that hook with
|
|
5
|
+
# `async` and `asyncRewake`, handing it the same gate scripts:
|
|
6
|
+
#
|
|
7
|
+
# guardrail-rewake.sh guardrail-e2e-gate.sh guardrail-watch-gate.sh ...
|
|
8
|
+
#
|
|
9
|
+
# Claude Code cancels the synchronous hook at its one-second cap and discards what it printed, and on a loaded Windows
|
|
10
|
+
# machine its own Git Bash start can spend that second, so a block or offer decided there can silently never land.
|
|
11
|
+
# Nothing waits on this one. When a gate's pre-filter could fire, or the synchronous run saved an output for the event,
|
|
12
|
+
# guardrails.mjs waits for the transcript to record how the synchronous run ended and, only when it was cancelled,
|
|
13
|
+
# prints what it would have said to stderr and exits 2, which wakes Claude with it. Every other firing ends here in
|
|
14
|
+
# shell, with no process started.
|
|
15
|
+
|
|
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_read_payload
|
|
21
|
+
|
|
22
|
+
firing_subcommands=()
|
|
23
|
+
for gate_script in "$@"; do
|
|
24
|
+
GUARDRAIL_SUBCOMMAND=""
|
|
25
|
+
. "$guardrail_script_dir/$gate_script" || continue
|
|
26
|
+
if [[ -n $GUARDRAIL_SUBCOMMAND ]] && guardrail_wants; then
|
|
27
|
+
firing_subcommands+=("$GUARDRAIL_SUBCOMMAND")
|
|
28
|
+
fi
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
guardrail_payload_field hook_event_name || exit 0
|
|
32
|
+
guardrail_resolve_state_file
|
|
33
|
+
# Named as sessionState.ts names the sidecar SYNC_HOOK_OUTPUT_SUFFIX describes.
|
|
34
|
+
sync_output_file="${GUARDRAIL_STATE_FILE%.json}.${GUARDRAIL_FIELD_VALUE}-sync-output.json"
|
|
35
|
+
if [[ ${#firing_subcommands[@]} -eq 0 && ! -f $sync_output_file ]]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# The expansion guard keeps an empty list from tripping `set -u` on bash 3.2.
|
|
40
|
+
guardrail_run_rewake_node rewake ${firing_subcommands[@]+"${firing_subcommands[@]}"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, statSync, writeFileSync, renameSync, writeSync, unlinkSync
|
|
1
|
+
import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, statSync, writeFileSync, renameSync, writeSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join, isAbsolute, resolve, basename, dirname } from 'path';
|
|
3
3
|
import { homedir, tmpdir, hostname } from 'os';
|
|
4
4
|
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
6
7
|
|
|
7
8
|
// src/guardrails/cli.ts
|
|
8
9
|
|
|
@@ -110,6 +111,26 @@ var E2E_STOP_VERDICT_SUFFIX = ".e2e-stop-verdict";
|
|
|
110
111
|
var E2E_SHELL_BLOCKS_SUFFIX = ".e2e-shell-blocks";
|
|
111
112
|
var AUTO_E2E_TEST_PREFERENCE_KEY = "autoE2ETest";
|
|
112
113
|
var POST_PR_VISUAL_WALKTHROUGH_PREFERENCE_KEY = "postPRVisualWalkthrough";
|
|
114
|
+
var REWAKE_HOOK_EVENTS = ["Stop", "UserPromptSubmit"];
|
|
115
|
+
var REWAKE_SUBCOMMAND = "rewake";
|
|
116
|
+
var REWAKE_HOOK_SCRIPT = "guardrail-rewake.sh";
|
|
117
|
+
var GUARDRAIL_HOOK_COMMAND_MARKER = "guardrail-";
|
|
118
|
+
var SYNC_HOOK_OUTPUT_SUFFIX = "-sync-output.json";
|
|
119
|
+
var TRANSCRIPT_HOOK_CANCELLED_TYPE = "hook_cancelled";
|
|
120
|
+
var TRANSCRIPT_HOOK_RECORD_TYPE_PREFIX = "hook_";
|
|
121
|
+
var REWAKE_OUTCOME_WAIT_MS = 2e4;
|
|
122
|
+
var REWAKE_OUTCOME_POLL_MS = 250;
|
|
123
|
+
var REWAKE_TRANSCRIPT_TAIL_BYTES = 512e3;
|
|
124
|
+
var REWAKE_ANCHOR_CHARS = 120;
|
|
125
|
+
var REWAKE_MESSAGE_PREFIX = {
|
|
126
|
+
Stop: "Muggle Stop check, delivered late because the turn-end hook outlasted its 1 s budget and was cancelled \u2014 act on it now:",
|
|
127
|
+
UserPromptSubmit: "Muggle prompt check, delivered late because the prompt hook outlasted its 1 s budget and was cancelled:"
|
|
128
|
+
};
|
|
129
|
+
var COMMANDLESS_HOOK_DELIVERY_TYPES = ["hook_additional_context", "hook_blocking_error"];
|
|
130
|
+
var REWAKE_ANCHOR_ROLE_BY_EVENT = {
|
|
131
|
+
Stop: "assistant",
|
|
132
|
+
UserPromptSubmit: "user"
|
|
133
|
+
};
|
|
113
134
|
|
|
114
135
|
// src/guardrails/store/fileLock.ts
|
|
115
136
|
function isProcessAlive(pid) {
|
|
@@ -244,6 +265,21 @@ function readE2eShellBlockCount(sessionId2, dirOverride) {
|
|
|
244
265
|
function clearE2eShellBlocks(sessionId2, dirOverride) {
|
|
245
266
|
rmSync(fileFor(sessionId2, dirOverride, E2E_SHELL_BLOCKS_SUFFIX), { force: true });
|
|
246
267
|
}
|
|
268
|
+
var syncHookOutputFile = (sessionId2, hookEventName, dirOverride) => fileFor(sessionId2, dirOverride, `.${hookEventName}${SYNC_HOOK_OUTPUT_SUFFIX}`);
|
|
269
|
+
function writeSyncHookOutput(sessionId2, hookEventName, record, dirOverride) {
|
|
270
|
+
writeAtomically(syncHookOutputFile(sessionId2, hookEventName, dirOverride), record, dirOverride);
|
|
271
|
+
}
|
|
272
|
+
function takeSyncHookOutput(sessionId2, hookEventName, payloadDigest2, dirOverride) {
|
|
273
|
+
const target = syncHookOutputFile(sessionId2, hookEventName, dirOverride);
|
|
274
|
+
try {
|
|
275
|
+
const record = JSON.parse(readFileSync(target, "utf-8"));
|
|
276
|
+
if (record.payloadDigest !== payloadDigest2) return null;
|
|
277
|
+
rmSync(target, { force: true });
|
|
278
|
+
return record;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
247
283
|
var lockFor = (sessionId2, dirOverride) => `${fileFor(sessionId2, dirOverride)}.lock`;
|
|
248
284
|
function commitUnderHeldLock(sessionId2, mutate, dirOverride) {
|
|
249
285
|
const current = readState(sessionId2, dirOverride);
|
|
@@ -2829,6 +2865,63 @@ function normalizeHookInput(raw) {
|
|
|
2829
2865
|
}
|
|
2830
2866
|
return payload;
|
|
2831
2867
|
}
|
|
2868
|
+
function payloadDigest(rawPayload2) {
|
|
2869
|
+
return createHash("sha1").update(rawPayload2).digest("hex");
|
|
2870
|
+
}
|
|
2871
|
+
function transcriptAnchor(input2) {
|
|
2872
|
+
const anchorText = input2.hook_event_name === "Stop" ? input2.last_assistant_message?.trimEnd().slice(-REWAKE_ANCHOR_CHARS) : input2.hook_event_name === "UserPromptSubmit" ? input2.prompt?.trimStart().slice(0, REWAKE_ANCHOR_CHARS) : void 0;
|
|
2873
|
+
if (!anchorText?.trim()) return void 0;
|
|
2874
|
+
return JSON.stringify(anchorText).slice(1, -1);
|
|
2875
|
+
}
|
|
2876
|
+
var jsonFieldPattern = (field, value) => new RegExp(`"${field}"\\s*:\\s*"${value}"`);
|
|
2877
|
+
function parseHookRecord(line, hookEventName) {
|
|
2878
|
+
if (!jsonFieldPattern("hookEvent", hookEventName).test(line)) return void 0;
|
|
2879
|
+
try {
|
|
2880
|
+
const record = JSON.parse(line).attachment;
|
|
2881
|
+
if (!record?.type?.startsWith(TRANSCRIPT_HOOK_RECORD_TYPE_PREFIX) || record.hookEvent !== hookEventName) return void 0;
|
|
2882
|
+
return record;
|
|
2883
|
+
} catch {
|
|
2884
|
+
return void 0;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
function isSyncGuardrailCommand(command) {
|
|
2888
|
+
return command.includes(GUARDRAIL_HOOK_COMMAND_MARKER) && !command.includes(REWAKE_HOOK_SCRIPT);
|
|
2889
|
+
}
|
|
2890
|
+
function findSyncHookOutcome(transcriptTail, hookEventName, anchor) {
|
|
2891
|
+
const lines = transcriptTail.split("\n");
|
|
2892
|
+
const anchorRolePattern = jsonFieldPattern("type", REWAKE_ANCHOR_ROLE_BY_EVENT[hookEventName] ?? "");
|
|
2893
|
+
let anchorIndex = -1;
|
|
2894
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2895
|
+
if (lines[index].includes(anchor) && anchorRolePattern.test(lines[index])) {
|
|
2896
|
+
anchorIndex = index;
|
|
2897
|
+
break;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
if (anchorIndex < 0) return "unrecorded" /* Unrecorded */;
|
|
2901
|
+
for (const line of lines.slice(anchorIndex + 1)) {
|
|
2902
|
+
const record = parseHookRecord(line, hookEventName);
|
|
2903
|
+
if (!record) continue;
|
|
2904
|
+
if (typeof record.command === "string") {
|
|
2905
|
+
if (!isSyncGuardrailCommand(record.command)) continue;
|
|
2906
|
+
return record.type === TRANSCRIPT_HOOK_CANCELLED_TYPE ? "cancelled" /* Cancelled */ : "delivered" /* Delivered */;
|
|
2907
|
+
}
|
|
2908
|
+
if (COMMANDLESS_HOOK_DELIVERY_TYPES.includes(record.type)) return "delivered" /* Delivered */;
|
|
2909
|
+
}
|
|
2910
|
+
return "unrecorded" /* Unrecorded */;
|
|
2911
|
+
}
|
|
2912
|
+
function rewakeMessage(hookEventName, hookOutput) {
|
|
2913
|
+
let parsedOutput;
|
|
2914
|
+
try {
|
|
2915
|
+
parsedOutput = JSON.parse(hookOutput);
|
|
2916
|
+
} catch {
|
|
2917
|
+
return "";
|
|
2918
|
+
}
|
|
2919
|
+
const deliveredText = hookEventName === "Stop" ? parsedOutput.decision === "block" ? parsedOutput.reason : void 0 : parsedOutput.hookSpecificOutput?.additionalContext ?? parsedOutput.additional_context;
|
|
2920
|
+
if (!deliveredText) return "";
|
|
2921
|
+
return `${REWAKE_MESSAGE_PREFIX[hookEventName] ?? ""}
|
|
2922
|
+
|
|
2923
|
+
${deliveredText}`;
|
|
2924
|
+
}
|
|
2832
2925
|
|
|
2833
2926
|
// src/guardrails/hookHost.ts
|
|
2834
2927
|
function resolveHookHost(env = process.env) {
|
|
@@ -2842,17 +2935,25 @@ function skillInvocation(slashCommand, host2) {
|
|
|
2842
2935
|
}
|
|
2843
2936
|
|
|
2844
2937
|
// src/guardrails/cli.ts
|
|
2845
|
-
function
|
|
2938
|
+
function readStdinText() {
|
|
2939
|
+
try {
|
|
2940
|
+
return readFileSync(0, "utf-8");
|
|
2941
|
+
} catch {
|
|
2942
|
+
return "";
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function parseHookInput(rawHookPayload) {
|
|
2846
2946
|
try {
|
|
2847
|
-
return normalizeHookInput(JSON.parse(
|
|
2947
|
+
return normalizeHookInput(JSON.parse(rawHookPayload));
|
|
2848
2948
|
} catch {
|
|
2849
2949
|
return {};
|
|
2850
2950
|
}
|
|
2851
2951
|
}
|
|
2852
2952
|
var host = resolveHookHost();
|
|
2853
2953
|
var sub = process.argv[2];
|
|
2854
|
-
if (!isBackgroundJob(sub)) enforceHookChildProcessDeadline();
|
|
2855
|
-
var
|
|
2954
|
+
if (!isBackgroundJob(sub) && sub !== REWAKE_SUBCOMMAND) enforceHookChildProcessDeadline();
|
|
2955
|
+
var rawPayload = readStdinText();
|
|
2956
|
+
var input = parseHookInput(rawPayload);
|
|
2856
2957
|
var sessionId = input.session_id ?? "unknown";
|
|
2857
2958
|
function releaseGate(field) {
|
|
2858
2959
|
commitStateMutation(sessionId, { kind: "fields" /* Fields */, fields: { [field]: true } });
|
|
@@ -3422,5 +3523,47 @@ var backgroundJobs = {
|
|
|
3422
3523
|
["scan-watch-pr-states" /* ScanWatchPrStates */]: scanWatchPrStatesJob,
|
|
3423
3524
|
["commit-deferred-state" /* CommitDeferredState */]: commitDeferredStateJob
|
|
3424
3525
|
};
|
|
3425
|
-
|
|
3426
|
-
|
|
3526
|
+
function waitSync(milliseconds) {
|
|
3527
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
3528
|
+
}
|
|
3529
|
+
function awaitSyncHookOutcome(hookEventName) {
|
|
3530
|
+
const anchor = transcriptAnchor(
|
|
3531
|
+
hookEventName === "Stop" ? { ...input, last_assistant_message: closingAssistantText() } : input
|
|
3532
|
+
);
|
|
3533
|
+
const transcriptPath = input.transcript_path;
|
|
3534
|
+
if (!anchor || !transcriptPath) return "unrecorded" /* Unrecorded */;
|
|
3535
|
+
const deadline = Date.now() + REWAKE_OUTCOME_WAIT_MS;
|
|
3536
|
+
for (; ; ) {
|
|
3537
|
+
const transcriptTail = readFileTail(transcriptPath, REWAKE_TRANSCRIPT_TAIL_BYTES) ?? "";
|
|
3538
|
+
const outcome = findSyncHookOutcome(transcriptTail, hookEventName, anchor);
|
|
3539
|
+
if (outcome !== "unrecorded" /* Unrecorded */ || Date.now() >= deadline) return outcome;
|
|
3540
|
+
waitSync(REWAKE_OUTCOME_POLL_MS);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
function rewake() {
|
|
3544
|
+
const hookEventName = input.hook_event_name ?? "";
|
|
3545
|
+
const outcome = awaitSyncHookOutcome(hookEventName);
|
|
3546
|
+
const savedOutput = takeSyncHookOutput(sessionId, hookEventName, payloadDigest(rawPayload));
|
|
3547
|
+
if (outcome === "delivered" /* Delivered */) return "";
|
|
3548
|
+
return rewakeMessage(hookEventName, savedOutput ? savedOutput.hookOutput : runFiringGates());
|
|
3549
|
+
}
|
|
3550
|
+
function saveSyncHookOutput(hookOutput) {
|
|
3551
|
+
const hookEventName = input.hook_event_name ?? "";
|
|
3552
|
+
if (!REWAKE_HOOK_EVENTS.includes(hookEventName)) return;
|
|
3553
|
+
try {
|
|
3554
|
+
writeSyncHookOutput(sessionId, hookEventName, { payloadDigest: payloadDigest(rawPayload), hookOutput });
|
|
3555
|
+
} catch {
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
if (sub === REWAKE_SUBCOMMAND) {
|
|
3559
|
+
const rewakeText = rewake();
|
|
3560
|
+
if (rewakeText) {
|
|
3561
|
+
process.stderr.write(rewakeText);
|
|
3562
|
+
process.exitCode = 2;
|
|
3563
|
+
}
|
|
3564
|
+
} else {
|
|
3565
|
+
const runSubcommand = isBackgroundJob(sub) ? backgroundJobs[sub] : handlers[sub] ?? (() => "{}");
|
|
3566
|
+
const hookOutput = runSubcommand();
|
|
3567
|
+
if (!isBackgroundJob(sub)) saveSyncHookOutput(hookOutput);
|
|
3568
|
+
process.stdout.write(hookOutput);
|
|
3569
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"release": "5.26.0",
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-10-
|
|
3
|
+
"buildId": "run-264-1",
|
|
4
|
+
"commitSha": "31c63b7f92a9fbe2a2cf3bfc1bcf4c4828f93e49",
|
|
5
|
+
"buildTime": "2026-10-31T04:22:47Z",
|
|
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.30.0-staging.
|
|
4
|
+
"version": "5.30.0-staging.264",
|
|
5
5
|
"description": "13 years of Microsoft hyperscale engineering, packaged as a harness for your AI coding agent.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
4
|
-
"version": "5.30.0-staging.
|
|
4
|
+
"version": "5.30.0-staging.264",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
package/plugin/.codex-mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.30.0-staging.
|
|
3
|
+
"version": "5.30.0-staging.264",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
|
-
"version": "5.30.0-staging.
|
|
5
|
+
"version": "5.30.0-staging.264",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/plugin/.mcp.json
CHANGED
package/plugin/hooks/hooks.json
CHANGED
|
@@ -244,6 +244,13 @@
|
|
|
244
244
|
"command": "set -- guardrail-dispatch.sh guardrail-e2e-gate.sh guardrail-terminal-gate.sh guardrail-watch-gate.sh guardrail-watch-follow-through-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; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
245
245
|
"async": false,
|
|
246
246
|
"timeout": 1
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"type": "command",
|
|
250
|
+
"command": "set -- guardrail-rewake.sh guardrail-e2e-gate.sh guardrail-terminal-gate.sh guardrail-watch-gate.sh guardrail-watch-follow-through-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; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
251
|
+
"async": true,
|
|
252
|
+
"asyncRewake": true,
|
|
253
|
+
"timeout": 30
|
|
247
254
|
}
|
|
248
255
|
]
|
|
249
256
|
}
|
|
@@ -256,6 +263,13 @@
|
|
|
256
263
|
"command": "set -- guardrail-build-router.sh; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
257
264
|
"async": false,
|
|
258
265
|
"timeout": 1
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"type": "command",
|
|
269
|
+
"command": "set -- guardrail-rewake.sh guardrail-build-router.sh; . \"${CLAUDE_PLUGIN_ROOT}/scripts/hook-entry.sh\"",
|
|
270
|
+
"async": true,
|
|
271
|
+
"asyncRewake": true,
|
|
272
|
+
"timeout": 30
|
|
259
273
|
}
|
|
260
274
|
]
|
|
261
275
|
}
|
|
@@ -150,6 +150,13 @@ guardrail_run_node() {
|
|
|
150
150
|
node "${root}/scripts/guardrails.mjs" "$@" <<<"$GUARDRAIL_PAYLOAD" 2>/dev/null || printf '{}'
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
# Runs guardrails.mjs for an asyncRewake companion, which answers through its stderr and exit status rather than
|
|
154
|
+
# stdout: exit 2 is what makes Claude Code wake the session with that stderr, so neither is swallowed here.
|
|
155
|
+
guardrail_run_rewake_node() {
|
|
156
|
+
local root="${CLAUDE_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}"
|
|
157
|
+
node "${root}/scripts/guardrails.mjs" "$@" <<<"$GUARDRAIL_PAYLOAD"
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
# Entry for a gate script executed directly: its guardrail_wants decides whether Node runs.
|
|
154
161
|
guardrail_run_standalone() {
|
|
155
162
|
guardrail_read_payload
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# Async companion of a synchronous guardrail hook (Stop, UserPromptSubmit). hooks.json runs it beside that hook with
|
|
5
|
+
# `async` and `asyncRewake`, handing it the same gate scripts:
|
|
6
|
+
#
|
|
7
|
+
# guardrail-rewake.sh guardrail-e2e-gate.sh guardrail-watch-gate.sh ...
|
|
8
|
+
#
|
|
9
|
+
# Claude Code cancels the synchronous hook at its one-second cap and discards what it printed, and on a loaded Windows
|
|
10
|
+
# machine its own Git Bash start can spend that second, so a block or offer decided there can silently never land.
|
|
11
|
+
# Nothing waits on this one. When a gate's pre-filter could fire, or the synchronous run saved an output for the event,
|
|
12
|
+
# guardrails.mjs waits for the transcript to record how the synchronous run ended and, only when it was cancelled,
|
|
13
|
+
# prints what it would have said to stderr and exits 2, which wakes Claude with it. Every other firing ends here in
|
|
14
|
+
# shell, with no process started.
|
|
15
|
+
|
|
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_read_payload
|
|
21
|
+
|
|
22
|
+
firing_subcommands=()
|
|
23
|
+
for gate_script in "$@"; do
|
|
24
|
+
GUARDRAIL_SUBCOMMAND=""
|
|
25
|
+
. "$guardrail_script_dir/$gate_script" || continue
|
|
26
|
+
if [[ -n $GUARDRAIL_SUBCOMMAND ]] && guardrail_wants; then
|
|
27
|
+
firing_subcommands+=("$GUARDRAIL_SUBCOMMAND")
|
|
28
|
+
fi
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
guardrail_payload_field hook_event_name || exit 0
|
|
32
|
+
guardrail_resolve_state_file
|
|
33
|
+
# Named as sessionState.ts names the sidecar SYNC_HOOK_OUTPUT_SUFFIX describes.
|
|
34
|
+
sync_output_file="${GUARDRAIL_STATE_FILE%.json}.${GUARDRAIL_FIELD_VALUE}-sync-output.json"
|
|
35
|
+
if [[ ${#firing_subcommands[@]} -eq 0 && ! -f $sync_output_file ]]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# The expansion guard keeps an empty list from tripping `set -u` on bash 3.2.
|
|
40
|
+
guardrail_run_rewake_node rewake ${firing_subcommands[@]+"${firing_subcommands[@]}"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, statSync, writeFileSync, renameSync, writeSync, unlinkSync
|
|
1
|
+
import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, statSync, writeFileSync, renameSync, writeSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join, isAbsolute, resolve, basename, dirname } from 'path';
|
|
3
3
|
import { homedir, tmpdir, hostname } from 'os';
|
|
4
4
|
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
6
7
|
|
|
7
8
|
// src/guardrails/cli.ts
|
|
8
9
|
|
|
@@ -110,6 +111,26 @@ var E2E_STOP_VERDICT_SUFFIX = ".e2e-stop-verdict";
|
|
|
110
111
|
var E2E_SHELL_BLOCKS_SUFFIX = ".e2e-shell-blocks";
|
|
111
112
|
var AUTO_E2E_TEST_PREFERENCE_KEY = "autoE2ETest";
|
|
112
113
|
var POST_PR_VISUAL_WALKTHROUGH_PREFERENCE_KEY = "postPRVisualWalkthrough";
|
|
114
|
+
var REWAKE_HOOK_EVENTS = ["Stop", "UserPromptSubmit"];
|
|
115
|
+
var REWAKE_SUBCOMMAND = "rewake";
|
|
116
|
+
var REWAKE_HOOK_SCRIPT = "guardrail-rewake.sh";
|
|
117
|
+
var GUARDRAIL_HOOK_COMMAND_MARKER = "guardrail-";
|
|
118
|
+
var SYNC_HOOK_OUTPUT_SUFFIX = "-sync-output.json";
|
|
119
|
+
var TRANSCRIPT_HOOK_CANCELLED_TYPE = "hook_cancelled";
|
|
120
|
+
var TRANSCRIPT_HOOK_RECORD_TYPE_PREFIX = "hook_";
|
|
121
|
+
var REWAKE_OUTCOME_WAIT_MS = 2e4;
|
|
122
|
+
var REWAKE_OUTCOME_POLL_MS = 250;
|
|
123
|
+
var REWAKE_TRANSCRIPT_TAIL_BYTES = 512e3;
|
|
124
|
+
var REWAKE_ANCHOR_CHARS = 120;
|
|
125
|
+
var REWAKE_MESSAGE_PREFIX = {
|
|
126
|
+
Stop: "Muggle Stop check, delivered late because the turn-end hook outlasted its 1 s budget and was cancelled \u2014 act on it now:",
|
|
127
|
+
UserPromptSubmit: "Muggle prompt check, delivered late because the prompt hook outlasted its 1 s budget and was cancelled:"
|
|
128
|
+
};
|
|
129
|
+
var COMMANDLESS_HOOK_DELIVERY_TYPES = ["hook_additional_context", "hook_blocking_error"];
|
|
130
|
+
var REWAKE_ANCHOR_ROLE_BY_EVENT = {
|
|
131
|
+
Stop: "assistant",
|
|
132
|
+
UserPromptSubmit: "user"
|
|
133
|
+
};
|
|
113
134
|
|
|
114
135
|
// src/guardrails/store/fileLock.ts
|
|
115
136
|
function isProcessAlive(pid) {
|
|
@@ -244,6 +265,21 @@ function readE2eShellBlockCount(sessionId2, dirOverride) {
|
|
|
244
265
|
function clearE2eShellBlocks(sessionId2, dirOverride) {
|
|
245
266
|
rmSync(fileFor(sessionId2, dirOverride, E2E_SHELL_BLOCKS_SUFFIX), { force: true });
|
|
246
267
|
}
|
|
268
|
+
var syncHookOutputFile = (sessionId2, hookEventName, dirOverride) => fileFor(sessionId2, dirOverride, `.${hookEventName}${SYNC_HOOK_OUTPUT_SUFFIX}`);
|
|
269
|
+
function writeSyncHookOutput(sessionId2, hookEventName, record, dirOverride) {
|
|
270
|
+
writeAtomically(syncHookOutputFile(sessionId2, hookEventName, dirOverride), record, dirOverride);
|
|
271
|
+
}
|
|
272
|
+
function takeSyncHookOutput(sessionId2, hookEventName, payloadDigest2, dirOverride) {
|
|
273
|
+
const target = syncHookOutputFile(sessionId2, hookEventName, dirOverride);
|
|
274
|
+
try {
|
|
275
|
+
const record = JSON.parse(readFileSync(target, "utf-8"));
|
|
276
|
+
if (record.payloadDigest !== payloadDigest2) return null;
|
|
277
|
+
rmSync(target, { force: true });
|
|
278
|
+
return record;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
247
283
|
var lockFor = (sessionId2, dirOverride) => `${fileFor(sessionId2, dirOverride)}.lock`;
|
|
248
284
|
function commitUnderHeldLock(sessionId2, mutate, dirOverride) {
|
|
249
285
|
const current = readState(sessionId2, dirOverride);
|
|
@@ -2829,6 +2865,63 @@ function normalizeHookInput(raw) {
|
|
|
2829
2865
|
}
|
|
2830
2866
|
return payload;
|
|
2831
2867
|
}
|
|
2868
|
+
function payloadDigest(rawPayload2) {
|
|
2869
|
+
return createHash("sha1").update(rawPayload2).digest("hex");
|
|
2870
|
+
}
|
|
2871
|
+
function transcriptAnchor(input2) {
|
|
2872
|
+
const anchorText = input2.hook_event_name === "Stop" ? input2.last_assistant_message?.trimEnd().slice(-REWAKE_ANCHOR_CHARS) : input2.hook_event_name === "UserPromptSubmit" ? input2.prompt?.trimStart().slice(0, REWAKE_ANCHOR_CHARS) : void 0;
|
|
2873
|
+
if (!anchorText?.trim()) return void 0;
|
|
2874
|
+
return JSON.stringify(anchorText).slice(1, -1);
|
|
2875
|
+
}
|
|
2876
|
+
var jsonFieldPattern = (field, value) => new RegExp(`"${field}"\\s*:\\s*"${value}"`);
|
|
2877
|
+
function parseHookRecord(line, hookEventName) {
|
|
2878
|
+
if (!jsonFieldPattern("hookEvent", hookEventName).test(line)) return void 0;
|
|
2879
|
+
try {
|
|
2880
|
+
const record = JSON.parse(line).attachment;
|
|
2881
|
+
if (!record?.type?.startsWith(TRANSCRIPT_HOOK_RECORD_TYPE_PREFIX) || record.hookEvent !== hookEventName) return void 0;
|
|
2882
|
+
return record;
|
|
2883
|
+
} catch {
|
|
2884
|
+
return void 0;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
function isSyncGuardrailCommand(command) {
|
|
2888
|
+
return command.includes(GUARDRAIL_HOOK_COMMAND_MARKER) && !command.includes(REWAKE_HOOK_SCRIPT);
|
|
2889
|
+
}
|
|
2890
|
+
function findSyncHookOutcome(transcriptTail, hookEventName, anchor) {
|
|
2891
|
+
const lines = transcriptTail.split("\n");
|
|
2892
|
+
const anchorRolePattern = jsonFieldPattern("type", REWAKE_ANCHOR_ROLE_BY_EVENT[hookEventName] ?? "");
|
|
2893
|
+
let anchorIndex = -1;
|
|
2894
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
2895
|
+
if (lines[index].includes(anchor) && anchorRolePattern.test(lines[index])) {
|
|
2896
|
+
anchorIndex = index;
|
|
2897
|
+
break;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
if (anchorIndex < 0) return "unrecorded" /* Unrecorded */;
|
|
2901
|
+
for (const line of lines.slice(anchorIndex + 1)) {
|
|
2902
|
+
const record = parseHookRecord(line, hookEventName);
|
|
2903
|
+
if (!record) continue;
|
|
2904
|
+
if (typeof record.command === "string") {
|
|
2905
|
+
if (!isSyncGuardrailCommand(record.command)) continue;
|
|
2906
|
+
return record.type === TRANSCRIPT_HOOK_CANCELLED_TYPE ? "cancelled" /* Cancelled */ : "delivered" /* Delivered */;
|
|
2907
|
+
}
|
|
2908
|
+
if (COMMANDLESS_HOOK_DELIVERY_TYPES.includes(record.type)) return "delivered" /* Delivered */;
|
|
2909
|
+
}
|
|
2910
|
+
return "unrecorded" /* Unrecorded */;
|
|
2911
|
+
}
|
|
2912
|
+
function rewakeMessage(hookEventName, hookOutput) {
|
|
2913
|
+
let parsedOutput;
|
|
2914
|
+
try {
|
|
2915
|
+
parsedOutput = JSON.parse(hookOutput);
|
|
2916
|
+
} catch {
|
|
2917
|
+
return "";
|
|
2918
|
+
}
|
|
2919
|
+
const deliveredText = hookEventName === "Stop" ? parsedOutput.decision === "block" ? parsedOutput.reason : void 0 : parsedOutput.hookSpecificOutput?.additionalContext ?? parsedOutput.additional_context;
|
|
2920
|
+
if (!deliveredText) return "";
|
|
2921
|
+
return `${REWAKE_MESSAGE_PREFIX[hookEventName] ?? ""}
|
|
2922
|
+
|
|
2923
|
+
${deliveredText}`;
|
|
2924
|
+
}
|
|
2832
2925
|
|
|
2833
2926
|
// src/guardrails/hookHost.ts
|
|
2834
2927
|
function resolveHookHost(env = process.env) {
|
|
@@ -2842,17 +2935,25 @@ function skillInvocation(slashCommand, host2) {
|
|
|
2842
2935
|
}
|
|
2843
2936
|
|
|
2844
2937
|
// src/guardrails/cli.ts
|
|
2845
|
-
function
|
|
2938
|
+
function readStdinText() {
|
|
2939
|
+
try {
|
|
2940
|
+
return readFileSync(0, "utf-8");
|
|
2941
|
+
} catch {
|
|
2942
|
+
return "";
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function parseHookInput(rawHookPayload) {
|
|
2846
2946
|
try {
|
|
2847
|
-
return normalizeHookInput(JSON.parse(
|
|
2947
|
+
return normalizeHookInput(JSON.parse(rawHookPayload));
|
|
2848
2948
|
} catch {
|
|
2849
2949
|
return {};
|
|
2850
2950
|
}
|
|
2851
2951
|
}
|
|
2852
2952
|
var host = resolveHookHost();
|
|
2853
2953
|
var sub = process.argv[2];
|
|
2854
|
-
if (!isBackgroundJob(sub)) enforceHookChildProcessDeadline();
|
|
2855
|
-
var
|
|
2954
|
+
if (!isBackgroundJob(sub) && sub !== REWAKE_SUBCOMMAND) enforceHookChildProcessDeadline();
|
|
2955
|
+
var rawPayload = readStdinText();
|
|
2956
|
+
var input = parseHookInput(rawPayload);
|
|
2856
2957
|
var sessionId = input.session_id ?? "unknown";
|
|
2857
2958
|
function releaseGate(field) {
|
|
2858
2959
|
commitStateMutation(sessionId, { kind: "fields" /* Fields */, fields: { [field]: true } });
|
|
@@ -3422,5 +3523,47 @@ var backgroundJobs = {
|
|
|
3422
3523
|
["scan-watch-pr-states" /* ScanWatchPrStates */]: scanWatchPrStatesJob,
|
|
3423
3524
|
["commit-deferred-state" /* CommitDeferredState */]: commitDeferredStateJob
|
|
3424
3525
|
};
|
|
3425
|
-
|
|
3426
|
-
|
|
3526
|
+
function waitSync(milliseconds) {
|
|
3527
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
3528
|
+
}
|
|
3529
|
+
function awaitSyncHookOutcome(hookEventName) {
|
|
3530
|
+
const anchor = transcriptAnchor(
|
|
3531
|
+
hookEventName === "Stop" ? { ...input, last_assistant_message: closingAssistantText() } : input
|
|
3532
|
+
);
|
|
3533
|
+
const transcriptPath = input.transcript_path;
|
|
3534
|
+
if (!anchor || !transcriptPath) return "unrecorded" /* Unrecorded */;
|
|
3535
|
+
const deadline = Date.now() + REWAKE_OUTCOME_WAIT_MS;
|
|
3536
|
+
for (; ; ) {
|
|
3537
|
+
const transcriptTail = readFileTail(transcriptPath, REWAKE_TRANSCRIPT_TAIL_BYTES) ?? "";
|
|
3538
|
+
const outcome = findSyncHookOutcome(transcriptTail, hookEventName, anchor);
|
|
3539
|
+
if (outcome !== "unrecorded" /* Unrecorded */ || Date.now() >= deadline) return outcome;
|
|
3540
|
+
waitSync(REWAKE_OUTCOME_POLL_MS);
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
function rewake() {
|
|
3544
|
+
const hookEventName = input.hook_event_name ?? "";
|
|
3545
|
+
const outcome = awaitSyncHookOutcome(hookEventName);
|
|
3546
|
+
const savedOutput = takeSyncHookOutput(sessionId, hookEventName, payloadDigest(rawPayload));
|
|
3547
|
+
if (outcome === "delivered" /* Delivered */) return "";
|
|
3548
|
+
return rewakeMessage(hookEventName, savedOutput ? savedOutput.hookOutput : runFiringGates());
|
|
3549
|
+
}
|
|
3550
|
+
function saveSyncHookOutput(hookOutput) {
|
|
3551
|
+
const hookEventName = input.hook_event_name ?? "";
|
|
3552
|
+
if (!REWAKE_HOOK_EVENTS.includes(hookEventName)) return;
|
|
3553
|
+
try {
|
|
3554
|
+
writeSyncHookOutput(sessionId, hookEventName, { payloadDigest: payloadDigest(rawPayload), hookOutput });
|
|
3555
|
+
} catch {
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
if (sub === REWAKE_SUBCOMMAND) {
|
|
3559
|
+
const rewakeText = rewake();
|
|
3560
|
+
if (rewakeText) {
|
|
3561
|
+
process.stderr.write(rewakeText);
|
|
3562
|
+
process.exitCode = 2;
|
|
3563
|
+
}
|
|
3564
|
+
} else {
|
|
3565
|
+
const runSubcommand = isBackgroundJob(sub) ? backgroundJobs[sub] : handlers[sub] ?? (() => "{}");
|
|
3566
|
+
const hookOutput = runSubcommand();
|
|
3567
|
+
if (!isBackgroundJob(sub)) saveSyncHookOutput(hookOutput);
|
|
3568
|
+
process.stdout.write(hookOutput);
|
|
3569
|
+
}
|