@muggleai/works 5.30.0-staging.264 → 5.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.264"
12
+ "version": "5.30.0"
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.264",
4
+ "version": "5.30.0",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.30.0-staging.264",
7
+ "@muggleai/works@5.30.0",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.30.0-staging.264",
3
+ "version": "5.30.0",
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.264",
5
+ "version": "5.30.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.30.0-staging.264",
7
+ "@muggleai/works@5.30.0",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -1,7 +1,7 @@
1
- import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, statSync, writeFileSync, renameSync, writeSync, unlinkSync } from 'fs';
1
+ import { spawnSync, execFileSync, spawn } from 'child_process';
2
+ import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, writeFileSync, renameSync, statSync, writeSync, unlinkSync } from 'fs';
2
3
  import { join, isAbsolute, resolve, basename, dirname } from 'path';
3
4
  import { homedir, tmpdir, hostname } from 'os';
4
- import { execFileSync, spawn } from 'child_process';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { createHash } from 'crypto';
7
7
 
@@ -131,6 +131,12 @@ var REWAKE_ANCHOR_ROLE_BY_EVENT = {
131
131
  Stop: "assistant",
132
132
  UserPromptSubmit: "user"
133
133
  };
134
+ var RECONCILED_HOOK_EVENT = "PostToolUse";
135
+ var RECONCILED_RECORD_SUFFIX = ".reconciled-record";
136
+ var RECONCILE_REPLAY_TIMEOUT_MS = 15e3;
137
+ var GUARDRAIL_SUBCOMMAND_ASSIGNMENT = /^GUARDRAIL_SUBCOMMAND="([\w-]+)"/m;
138
+ var HOOK_COMMAND_SCRIPTS = /^set -- ([^;]+);/;
139
+ var GUARDRAIL_DISPATCH_SCRIPT = "guardrail-dispatch.sh";
134
140
 
135
141
  // src/guardrails/store/fileLock.ts
136
142
  function isProcessAlive(pid) {
@@ -280,6 +286,20 @@ function takeSyncHookOutput(sessionId2, hookEventName, payloadDigest2, dirOverri
280
286
  return null;
281
287
  }
282
288
  }
289
+ function readReconciledRecord(sessionId2, dirOverride) {
290
+ try {
291
+ return readFileSync(fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX), "utf-8").trim() || void 0;
292
+ } catch {
293
+ return void 0;
294
+ }
295
+ }
296
+ function writeReconciledRecord(sessionId2, recordUuid, dirOverride) {
297
+ mkdirSync(baseDir(dirOverride), { recursive: true });
298
+ const target = fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX);
299
+ const staging = `${target}.${process.pid}.tmp`;
300
+ writeFileSync(staging, recordUuid);
301
+ renameSync(staging, target);
302
+ }
283
303
  var lockFor = (sessionId2, dirOverride) => `${fileFor(sessionId2, dirOverride)}.lock`;
284
304
  function commitUnderHeldLock(sessionId2, mutate, dirOverride) {
285
305
  const current = readState(sessionId2, dirOverride);
@@ -2923,6 +2943,75 @@ function rewakeMessage(hookEventName, hookOutput) {
2923
2943
  ${deliveredText}`;
2924
2944
  }
2925
2945
 
2946
+ // src/guardrails/reconcile.ts
2947
+ function parseTranscriptLines(transcriptTail) {
2948
+ return transcriptTail.split("\n").flatMap((line) => {
2949
+ try {
2950
+ return [JSON.parse(line)];
2951
+ } catch {
2952
+ return [];
2953
+ }
2954
+ });
2955
+ }
2956
+ function contentBlocks(line) {
2957
+ const content = line.message?.content;
2958
+ return Array.isArray(content) ? content : [];
2959
+ }
2960
+ function toolResponseFor(line, block) {
2961
+ if (line.toolUseResult && typeof line.toolUseResult === "object") return line.toolUseResult;
2962
+ if (typeof block.content === "string") return { output: block.content };
2963
+ return { content: block.content };
2964
+ }
2965
+ function isCancelledSyncRecorderRecord(record) {
2966
+ if (record?.type !== TRANSCRIPT_HOOK_CANCELLED_TYPE || record.hookEvent !== RECONCILED_HOOK_EVENT) return false;
2967
+ const command = record.command ?? "";
2968
+ return command.includes(GUARDRAIL_HOOK_COMMAND_MARKER) && !command.includes(REWAKE_HOOK_SCRIPT);
2969
+ }
2970
+ function findCancelledRecorderEvents(transcriptTail, sessionInput, afterRecordUuid) {
2971
+ const lines = parseTranscriptLines(transcriptTail);
2972
+ const toolCalls = /* @__PURE__ */ new Map();
2973
+ const toolResults = /* @__PURE__ */ new Map();
2974
+ for (const line of lines) {
2975
+ for (const block of contentBlocks(line)) {
2976
+ if (block.type === "tool_use" && block.id) toolCalls.set(block.id, { name: block.name, input: block.input });
2977
+ if (block.type === "tool_result" && block.tool_use_id) toolResults.set(block.tool_use_id, { line, block });
2978
+ }
2979
+ }
2980
+ const watermarkIndex = afterRecordUuid ? lines.findIndex((line) => line.uuid === afterRecordUuid) : -1;
2981
+ return lines.slice(watermarkIndex + 1).flatMap((line) => {
2982
+ const record = line.attachment;
2983
+ if (!line.uuid || !record || !isCancelledSyncRecorderRecord(record) || !record.toolUseID) return [];
2984
+ const toolCall = toolCalls.get(record.toolUseID);
2985
+ const toolResult = toolResults.get(record.toolUseID);
2986
+ if (!toolCall || !toolResult) return [];
2987
+ return [
2988
+ {
2989
+ recordUuid: line.uuid,
2990
+ command: record.command ?? "",
2991
+ replayPayload: {
2992
+ session_id: sessionInput.session_id,
2993
+ transcript_path: sessionInput.transcript_path,
2994
+ cwd: toolResult.line.cwd ?? line.cwd ?? sessionInput.cwd,
2995
+ hook_event_name: RECONCILED_HOOK_EVENT,
2996
+ tool_name: toolCall.name,
2997
+ tool_input: toolCall.input,
2998
+ tool_response: toolResponseFor(toolResult.line, toolResult.block),
2999
+ tool_use_id: record.toolUseID
3000
+ }
3001
+ }
3002
+ ];
3003
+ });
3004
+ }
3005
+ function recorderSubcommands(command, readScript) {
3006
+ const scriptList = HOOK_COMMAND_SCRIPTS.exec(command)?.[1] ?? "";
3007
+ const scriptNames = scriptList.split(/\s+/).filter((scriptName) => scriptName && scriptName !== GUARDRAIL_DISPATCH_SCRIPT);
3008
+ const subcommands = scriptNames.flatMap((scriptName) => {
3009
+ const subcommand = GUARDRAIL_SUBCOMMAND_ASSIGNMENT.exec(readScript(scriptName) ?? "")?.[1];
3010
+ return subcommand ? [subcommand] : [];
3011
+ });
3012
+ return [...new Set(subcommands)];
3013
+ }
3014
+
2926
3015
  // src/guardrails/hookHost.ts
2927
3016
  function resolveHookHost(env = process.env) {
2928
3017
  if (env[HOOK_HOST_ENV_VAR] === "codex") return "codex";
@@ -3540,8 +3629,35 @@ function awaitSyncHookOutcome(hookEventName) {
3540
3629
  waitSync(REWAKE_OUTCOME_POLL_MS);
3541
3630
  }
3542
3631
  }
3632
+ function readGuardrailScript(scriptName) {
3633
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? process.env.CURSOR_PLUGIN_ROOT;
3634
+ const scriptsDir = pluginRoot ? join(pluginRoot, "scripts") : dirname(process.argv[1]);
3635
+ try {
3636
+ return readFileSync(join(scriptsDir, scriptName), "utf-8");
3637
+ } catch {
3638
+ return null;
3639
+ }
3640
+ }
3641
+ function reconcileCancelledRecorders() {
3642
+ const transcriptPath = input.transcript_path;
3643
+ if (!transcriptPath) return;
3644
+ const transcriptTail = readFileTail(transcriptPath, REWAKE_TRANSCRIPT_TAIL_BYTES) ?? "";
3645
+ for (const event of findCancelledRecorderEvents(transcriptTail, input, readReconciledRecord(sessionId))) {
3646
+ const subcommands = recorderSubcommands(event.command, readGuardrailScript);
3647
+ if (subcommands.length > 0) {
3648
+ spawnSync(process.execPath, [...process.execArgv, process.argv[1], "run", ...subcommands], {
3649
+ input: JSON.stringify(event.replayPayload),
3650
+ timeout: RECONCILE_REPLAY_TIMEOUT_MS,
3651
+ windowsHide: true,
3652
+ stdio: ["pipe", "ignore", "ignore"]
3653
+ });
3654
+ }
3655
+ writeReconciledRecord(sessionId, event.recordUuid);
3656
+ }
3657
+ }
3543
3658
  function rewake() {
3544
3659
  const hookEventName = input.hook_event_name ?? "";
3660
+ if (hookEventName === "Stop") reconcileCancelledRecorders();
3545
3661
  const outcome = awaitSyncHookOutcome(hookEventName);
3546
3662
  const savedOutput = takeSyncHookOutput(sessionId, hookEventName, payloadDigest(rawPayload));
3547
3663
  if (outcome === "delivered" /* Delivered */) return "";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "release": "5.26.0",
3
- "buildId": "run-264-1",
4
- "commitSha": "31c63b7f92a9fbe2a2cf3bfc1bcf4c4828f93e49",
5
- "buildTime": "2026-10-31T04:22:47Z",
3
+ "buildId": "run-266-1",
4
+ "commitSha": "c79ba47696868d4d7e5d1d884f4edf77ee3fc393",
5
+ "buildTime": "2026-10-31T06:04:17Z",
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.264",
4
+ "version": "5.30.0",
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",
@@ -64,7 +64,7 @@
64
64
  "muggleConfig": {
65
65
  "electronAppVersion": "1.17.0",
66
66
  "downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
67
- "runtimeTargetDefault": "staging",
67
+ "runtimeTargetDefault": "production",
68
68
  "checksumsByStream": {
69
69
  "production": {
70
70
  "win32-x64": "03687e6c511f407875354954cb1b0f8e95d4ad0602174d76388a52d1aa1554e4",
@@ -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.264",
4
+ "version": "5.30.0",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.30.0-staging.264",
7
+ "@muggleai/works@5.30.0",
8
8
  "serve"
9
9
  ],
10
10
  "env": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muggle",
3
- "version": "5.30.0-staging.264",
3
+ "version": "5.30.0",
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.264",
5
+ "version": "5.30.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
package/plugin/.mcp.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@muggleai/works@5.30.0-staging.264",
7
+ "@muggleai/works@5.30.0",
8
8
  "serve"
9
9
  ]
10
10
  }
@@ -1,7 +1,7 @@
1
- import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, statSync, writeFileSync, renameSync, writeSync, unlinkSync } from 'fs';
1
+ import { spawnSync, execFileSync, spawn } from 'child_process';
2
+ import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, writeFileSync, renameSync, statSync, writeSync, unlinkSync } from 'fs';
2
3
  import { join, isAbsolute, resolve, basename, dirname } from 'path';
3
4
  import { homedir, tmpdir, hostname } from 'os';
4
- import { execFileSync, spawn } from 'child_process';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { createHash } from 'crypto';
7
7
 
@@ -131,6 +131,12 @@ var REWAKE_ANCHOR_ROLE_BY_EVENT = {
131
131
  Stop: "assistant",
132
132
  UserPromptSubmit: "user"
133
133
  };
134
+ var RECONCILED_HOOK_EVENT = "PostToolUse";
135
+ var RECONCILED_RECORD_SUFFIX = ".reconciled-record";
136
+ var RECONCILE_REPLAY_TIMEOUT_MS = 15e3;
137
+ var GUARDRAIL_SUBCOMMAND_ASSIGNMENT = /^GUARDRAIL_SUBCOMMAND="([\w-]+)"/m;
138
+ var HOOK_COMMAND_SCRIPTS = /^set -- ([^;]+);/;
139
+ var GUARDRAIL_DISPATCH_SCRIPT = "guardrail-dispatch.sh";
134
140
 
135
141
  // src/guardrails/store/fileLock.ts
136
142
  function isProcessAlive(pid) {
@@ -280,6 +286,20 @@ function takeSyncHookOutput(sessionId2, hookEventName, payloadDigest2, dirOverri
280
286
  return null;
281
287
  }
282
288
  }
289
+ function readReconciledRecord(sessionId2, dirOverride) {
290
+ try {
291
+ return readFileSync(fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX), "utf-8").trim() || void 0;
292
+ } catch {
293
+ return void 0;
294
+ }
295
+ }
296
+ function writeReconciledRecord(sessionId2, recordUuid, dirOverride) {
297
+ mkdirSync(baseDir(dirOverride), { recursive: true });
298
+ const target = fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX);
299
+ const staging = `${target}.${process.pid}.tmp`;
300
+ writeFileSync(staging, recordUuid);
301
+ renameSync(staging, target);
302
+ }
283
303
  var lockFor = (sessionId2, dirOverride) => `${fileFor(sessionId2, dirOverride)}.lock`;
284
304
  function commitUnderHeldLock(sessionId2, mutate, dirOverride) {
285
305
  const current = readState(sessionId2, dirOverride);
@@ -2923,6 +2943,75 @@ function rewakeMessage(hookEventName, hookOutput) {
2923
2943
  ${deliveredText}`;
2924
2944
  }
2925
2945
 
2946
+ // src/guardrails/reconcile.ts
2947
+ function parseTranscriptLines(transcriptTail) {
2948
+ return transcriptTail.split("\n").flatMap((line) => {
2949
+ try {
2950
+ return [JSON.parse(line)];
2951
+ } catch {
2952
+ return [];
2953
+ }
2954
+ });
2955
+ }
2956
+ function contentBlocks(line) {
2957
+ const content = line.message?.content;
2958
+ return Array.isArray(content) ? content : [];
2959
+ }
2960
+ function toolResponseFor(line, block) {
2961
+ if (line.toolUseResult && typeof line.toolUseResult === "object") return line.toolUseResult;
2962
+ if (typeof block.content === "string") return { output: block.content };
2963
+ return { content: block.content };
2964
+ }
2965
+ function isCancelledSyncRecorderRecord(record) {
2966
+ if (record?.type !== TRANSCRIPT_HOOK_CANCELLED_TYPE || record.hookEvent !== RECONCILED_HOOK_EVENT) return false;
2967
+ const command = record.command ?? "";
2968
+ return command.includes(GUARDRAIL_HOOK_COMMAND_MARKER) && !command.includes(REWAKE_HOOK_SCRIPT);
2969
+ }
2970
+ function findCancelledRecorderEvents(transcriptTail, sessionInput, afterRecordUuid) {
2971
+ const lines = parseTranscriptLines(transcriptTail);
2972
+ const toolCalls = /* @__PURE__ */ new Map();
2973
+ const toolResults = /* @__PURE__ */ new Map();
2974
+ for (const line of lines) {
2975
+ for (const block of contentBlocks(line)) {
2976
+ if (block.type === "tool_use" && block.id) toolCalls.set(block.id, { name: block.name, input: block.input });
2977
+ if (block.type === "tool_result" && block.tool_use_id) toolResults.set(block.tool_use_id, { line, block });
2978
+ }
2979
+ }
2980
+ const watermarkIndex = afterRecordUuid ? lines.findIndex((line) => line.uuid === afterRecordUuid) : -1;
2981
+ return lines.slice(watermarkIndex + 1).flatMap((line) => {
2982
+ const record = line.attachment;
2983
+ if (!line.uuid || !record || !isCancelledSyncRecorderRecord(record) || !record.toolUseID) return [];
2984
+ const toolCall = toolCalls.get(record.toolUseID);
2985
+ const toolResult = toolResults.get(record.toolUseID);
2986
+ if (!toolCall || !toolResult) return [];
2987
+ return [
2988
+ {
2989
+ recordUuid: line.uuid,
2990
+ command: record.command ?? "",
2991
+ replayPayload: {
2992
+ session_id: sessionInput.session_id,
2993
+ transcript_path: sessionInput.transcript_path,
2994
+ cwd: toolResult.line.cwd ?? line.cwd ?? sessionInput.cwd,
2995
+ hook_event_name: RECONCILED_HOOK_EVENT,
2996
+ tool_name: toolCall.name,
2997
+ tool_input: toolCall.input,
2998
+ tool_response: toolResponseFor(toolResult.line, toolResult.block),
2999
+ tool_use_id: record.toolUseID
3000
+ }
3001
+ }
3002
+ ];
3003
+ });
3004
+ }
3005
+ function recorderSubcommands(command, readScript) {
3006
+ const scriptList = HOOK_COMMAND_SCRIPTS.exec(command)?.[1] ?? "";
3007
+ const scriptNames = scriptList.split(/\s+/).filter((scriptName) => scriptName && scriptName !== GUARDRAIL_DISPATCH_SCRIPT);
3008
+ const subcommands = scriptNames.flatMap((scriptName) => {
3009
+ const subcommand = GUARDRAIL_SUBCOMMAND_ASSIGNMENT.exec(readScript(scriptName) ?? "")?.[1];
3010
+ return subcommand ? [subcommand] : [];
3011
+ });
3012
+ return [...new Set(subcommands)];
3013
+ }
3014
+
2926
3015
  // src/guardrails/hookHost.ts
2927
3016
  function resolveHookHost(env = process.env) {
2928
3017
  if (env[HOOK_HOST_ENV_VAR] === "codex") return "codex";
@@ -3540,8 +3629,35 @@ function awaitSyncHookOutcome(hookEventName) {
3540
3629
  waitSync(REWAKE_OUTCOME_POLL_MS);
3541
3630
  }
3542
3631
  }
3632
+ function readGuardrailScript(scriptName) {
3633
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? process.env.CURSOR_PLUGIN_ROOT;
3634
+ const scriptsDir = pluginRoot ? join(pluginRoot, "scripts") : dirname(process.argv[1]);
3635
+ try {
3636
+ return readFileSync(join(scriptsDir, scriptName), "utf-8");
3637
+ } catch {
3638
+ return null;
3639
+ }
3640
+ }
3641
+ function reconcileCancelledRecorders() {
3642
+ const transcriptPath = input.transcript_path;
3643
+ if (!transcriptPath) return;
3644
+ const transcriptTail = readFileTail(transcriptPath, REWAKE_TRANSCRIPT_TAIL_BYTES) ?? "";
3645
+ for (const event of findCancelledRecorderEvents(transcriptTail, input, readReconciledRecord(sessionId))) {
3646
+ const subcommands = recorderSubcommands(event.command, readGuardrailScript);
3647
+ if (subcommands.length > 0) {
3648
+ spawnSync(process.execPath, [...process.execArgv, process.argv[1], "run", ...subcommands], {
3649
+ input: JSON.stringify(event.replayPayload),
3650
+ timeout: RECONCILE_REPLAY_TIMEOUT_MS,
3651
+ windowsHide: true,
3652
+ stdio: ["pipe", "ignore", "ignore"]
3653
+ });
3654
+ }
3655
+ writeReconciledRecord(sessionId, event.recordUuid);
3656
+ }
3657
+ }
3543
3658
  function rewake() {
3544
3659
  const hookEventName = input.hook_event_name ?? "";
3660
+ if (hookEventName === "Stop") reconcileCancelledRecorders();
3545
3661
  const outcome = awaitSyncHookOutcome(hookEventName);
3546
3662
  const savedOutput = takeSyncHookOutput(sessionId, hookEventName, payloadDigest(rawPayload));
3547
3663
  if (outcome === "delivered" /* Delivered */) return "";