@muggleai/works 5.11.1 → 5.12.0-staging.74

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.
Files changed (96) hide show
  1. package/config/runtime-targets.json +33 -0
  2. package/dist/{chunk-MSPMXBO5.js → chunk-DJ6W5PZA.js} +486 -351
  3. package/dist/{chunk-WUMAJOBT.js → chunk-VTSS6TGQ.js} +158 -56
  4. package/dist/cli.js +2 -2
  5. package/dist/index.js +2 -2
  6. package/dist/plugin/hooks/README.md +6 -1
  7. package/dist/plugin/hooks/hooks.json +24 -0
  8. package/dist/plugin/scripts/guardrail-capability-claim.sh +38 -0
  9. package/dist/plugin/scripts/guardrail-comment-reply-gate.sh +42 -0
  10. package/dist/plugin/scripts/guardrail-debug-path-gate.sh +1 -1
  11. package/dist/plugin/scripts/guardrail-e2e-gate.sh +1 -1
  12. package/dist/plugin/scripts/guardrail-record-comment-replies.sh +28 -0
  13. package/dist/plugin/scripts/guardrail-resolve-gate.sh +22 -0
  14. package/dist/plugin/scripts/guardrail-stage-gate.sh +1 -1
  15. package/dist/plugin/scripts/guardrail-terminal-gate.sh +1 -1
  16. package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +1 -1
  17. package/dist/plugin/scripts/guardrail-watch-gate.sh +1 -1
  18. package/dist/plugin/scripts/guardrails.mjs +542 -34
  19. package/dist/plugin/scripts/pr-watch-loop.sh +9 -24
  20. package/dist/plugin/scripts/pr-watch-state.jq +52 -0
  21. package/dist/plugin/skills/_shared/identity-and-inbox.md +39 -0
  22. package/dist/plugin/skills/_shared/sync-branch-with-base.md +29 -0
  23. package/dist/plugin/skills/_shared/telemetry-events/muggle-do-escalation.md +3 -2
  24. package/dist/plugin/skills/_shared/vcs/github/unresolved-threads.md +2 -0
  25. package/dist/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +3 -1
  26. package/dist/plugin/skills/_shared/vcs/gitlab.md +0 -1
  27. package/dist/plugin/skills/do/address-reviews.md +5 -2
  28. package/dist/plugin/skills/do/build.md +9 -0
  29. package/dist/plugin/skills/do/e2e-acceptance.md +2 -0
  30. package/dist/plugin/skills/do/e2e-repair.md +98 -0
  31. package/dist/plugin/skills/do/open-prs/forward.md +17 -9
  32. package/dist/plugin/skills/do/open-prs/update.md +9 -2
  33. package/dist/plugin/skills/do/per-comment-replies.md +13 -1
  34. package/dist/plugin/skills/do/pre-flight.md +2 -2
  35. package/dist/plugin/skills/do/resolve-conflicts.md +59 -59
  36. package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
  37. package/dist/plugin/skills/muggle-do/SKILL.md +6 -5
  38. package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +1 -1
  39. package/dist/plugin/skills/muggle-preferences/preference-gates/autoRebase.md +5 -5
  40. package/dist/plugin/skills/muggle-status/SKILL.md +15 -6
  41. package/dist/plugin/skills/muggle-test/SKILL.md +1 -0
  42. package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +5 -3
  43. package/dist/plugin/skills/muggle-test-prepare/steps/rebase-check.md +1 -1
  44. package/dist/release-manifest.json +3 -3
  45. package/dist/src-FRYBTZ6J.js +1 -0
  46. package/package.json +22 -9
  47. package/plugin/hooks/README.md +6 -1
  48. package/plugin/hooks/hooks.json +24 -0
  49. package/plugin/scripts/guardrail-capability-claim.sh +38 -0
  50. package/plugin/scripts/guardrail-comment-reply-gate.sh +42 -0
  51. package/plugin/scripts/guardrail-debug-path-gate.sh +1 -1
  52. package/plugin/scripts/guardrail-e2e-gate.sh +1 -1
  53. package/plugin/scripts/guardrail-record-comment-replies.sh +28 -0
  54. package/plugin/scripts/guardrail-resolve-gate.sh +22 -0
  55. package/plugin/scripts/guardrail-stage-gate.sh +1 -1
  56. package/plugin/scripts/guardrail-terminal-gate.sh +1 -1
  57. package/plugin/scripts/guardrail-walkthrough-gate.sh +1 -1
  58. package/plugin/scripts/guardrail-watch-gate.sh +1 -1
  59. package/plugin/scripts/guardrails.mjs +542 -34
  60. package/plugin/scripts/pr-watch-loop.sh +9 -24
  61. package/plugin/scripts/pr-watch-state.jq +52 -0
  62. package/plugin/skills/_shared/identity-and-inbox.md +39 -0
  63. package/plugin/skills/_shared/sync-branch-with-base.md +29 -0
  64. package/plugin/skills/_shared/telemetry-events/muggle-do-escalation.md +3 -2
  65. package/plugin/skills/_shared/vcs/github/unresolved-threads.md +2 -0
  66. package/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +3 -1
  67. package/plugin/skills/_shared/vcs/gitlab.md +0 -1
  68. package/plugin/skills/do/address-reviews.md +5 -2
  69. package/plugin/skills/do/build.md +9 -0
  70. package/plugin/skills/do/e2e-acceptance.md +2 -0
  71. package/plugin/skills/do/e2e-repair.md +98 -0
  72. package/plugin/skills/do/open-prs/forward.md +17 -9
  73. package/plugin/skills/do/open-prs/update.md +9 -2
  74. package/plugin/skills/do/per-comment-replies.md +13 -1
  75. package/plugin/skills/do/pre-flight.md +2 -2
  76. package/plugin/skills/do/resolve-conflicts.md +59 -59
  77. package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
  78. package/plugin/skills/muggle-do/SKILL.md +6 -5
  79. package/plugin/skills/muggle-pr-followup/arm-watcher.md +1 -1
  80. package/plugin/skills/muggle-preferences/preference-gates/autoRebase.md +5 -5
  81. package/plugin/skills/muggle-status/SKILL.md +15 -6
  82. package/plugin/skills/muggle-test/SKILL.md +1 -0
  83. package/plugin/skills/muggle-test-feature-local/SKILL.md +5 -3
  84. package/plugin/skills/muggle-test-prepare/steps/rebase-check.md +1 -1
  85. package/scripts/postinstall.mjs +94 -6
  86. package/scripts/release-integrity/compareVersions.mjs +24 -0
  87. package/scripts/release-integrity/constants.mjs +8 -0
  88. package/scripts/release-integrity/index.d.mts +21 -0
  89. package/scripts/release-integrity/index.mjs +4 -0
  90. package/scripts/release-integrity/resolveIntegrityPolicy.mjs +37 -0
  91. package/scripts/release-integrity/verifyReleaseSignature.mjs +51 -0
  92. package/dist/plugin/skills/_shared/rebase-before-e2e.md +0 -21
  93. package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +0 -10
  94. package/dist/src-B5OTAZKM.js +0 -1
  95. package/plugin/skills/_shared/rebase-before-e2e.md +0 -21
  96. package/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +0 -10
@@ -1,10 +1,116 @@
1
- import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, readdirSync, openSync, writeSync, closeSync, unlinkSync } from 'fs';
2
2
  import { join, isAbsolute, resolve, dirname } from 'path';
3
- import { homedir } from 'os';
3
+ import { homedir, hostname } from 'os';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { execFileSync } from 'child_process';
6
6
 
7
7
  // src/guardrails/cli.ts
8
+
9
+ // src/guardrails/constants.ts
10
+ var GH_PR_MERGED_LINE = /\b(?:Merged|Squashed and merged|Rebased and merged) pull request [\w./-]*#(\d+)/;
11
+ var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
12
+ var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
13
+ var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
14
+ var MAX_PR_TERMINAL_BLOCKS = 3;
15
+ var MAX_WATCH_BLOCKS = 3;
16
+ var MAX_WALKTHROUGH_BLOCKS = 3;
17
+ var GH_LOOKUP_TIMEOUT_MS = 1e4;
18
+ var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
19
+ var MUGGLE_TEST_SKILL_NAME = "muggle-test";
20
+ var MANDATORY_STAGES_FRONTMATTER_KEY = "mandatoryStages";
21
+ var SKILL_NAME_INPUT_KEYS = ["skill", "skillName", "name", "command"];
22
+ var MAX_STAGE_BLOCKS = 3;
23
+ var MAX_DEBUG_BLOCKS = 3;
24
+ var ANY_TEST_CASE = "*";
25
+ var MUGGLE_EXECUTION_TOOL = /muggle-local-(execute-test-generation|execute-replay)/i;
26
+ var MUGGLE_EVENT_EMIT_TOOL = /muggle-local-telemetry-event-emit/i;
27
+ var MUGGLE_FEEDBACK_CREATE_TOOL = /muggle-remote-user-feedback-create/i;
28
+ var PRE_EXECUTION_CLASSIFICATION_EVENT = "pre-execution-classification";
29
+ var FAILURE_DIAGNOSIS_EVENT = /-failure-(classified|resolved)$/;
30
+ var MUGGLE_RUN_ID_LINE = /\*\*Run ID:\*\*\s*([^\s*]+)/;
31
+ var MUGGLE_RUN_STATUS_LINE = /\*\*Status:\*\*\s*([A-Za-z_]+)/;
32
+ var MUGGLE_RUN_PASSED_STATUS = "passed";
33
+ var GITHUB_RESOLVE_THREAD_MUTATION = /\bresolveReviewThread\b/;
34
+ var GITLAB_RESOLVE_DISCUSSION_CALL = /discussions\/[^\s"']*[?&]resolved=true/i;
35
+ var PROVIDER_API_INVOCATION = /\b(?:gh|glab)\s+api\b/;
36
+ var LOOP_REPLY_MARKER = "<!-- muggle-do:bot -->";
37
+ var REVIEW_THREAD_FETCH_COMMAND = /reviewThreads|merge_requests\/\d+\/discussions/;
38
+ var THREADED_REPLY_TARGET = /pulls\/\d+\/comments\/(\d+)\/replies|discussions\/([\w-]+)\/notes/g;
39
+ var MAX_REPLY_BLOCKS = 3;
40
+ var CAPABILITY_CLAIM_TRANSCRIPT_TAIL_BYTES = 64e3;
41
+ var SESSION_STATE_LOCK_WAIT_MS = 250;
42
+ var LOCK_POLL_INTERVAL_MS = 10;
43
+ var CALL_FAILURE_SIGNALS = [
44
+ /\bgh:\s/,
45
+ /\bglab:\s/,
46
+ /\bHTTP\s(?:4|5)\d\d\b/,
47
+ /"status"\s*:\s*"?(?:4|5)\d\d/,
48
+ /"isError"\s*:\s*true/
49
+ ];
50
+ var FORGE_TERMINAL_CMD = /\b(?:gh\s+pr\s+(?:merge|close|reopen)|glab\s+mr\s+(?:merge|close|reopen))\b/;
51
+
52
+ // src/guardrails/store/fileLock.ts
53
+ function isProcessAlive(pid) {
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch (error) {
58
+ return error.code === "EPERM";
59
+ }
60
+ }
61
+ function recordedHolder(lockPath) {
62
+ try {
63
+ return JSON.parse(readFileSync(lockPath, "utf-8"));
64
+ } catch {
65
+ return void 0;
66
+ }
67
+ }
68
+ function breakLockIfHolderIsDead(lockPath) {
69
+ const holder = recordedHolder(lockPath);
70
+ if (!holder) return;
71
+ if (holder.host !== hostname() || isProcessAlive(holder.pid)) return;
72
+ try {
73
+ unlinkSync(lockPath);
74
+ } catch {
75
+ return;
76
+ }
77
+ }
78
+ function pauseBetweenAttempts() {
79
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_POLL_INTERVAL_MS);
80
+ }
81
+ function withFileLock(lockPath, waitMs, run) {
82
+ const deadline = Date.now() + waitMs;
83
+ for (; ; ) {
84
+ let lockFileDescriptor;
85
+ try {
86
+ lockFileDescriptor = openSync(lockPath, "wx");
87
+ writeSync(
88
+ lockFileDescriptor,
89
+ JSON.stringify({
90
+ pid: process.pid,
91
+ host: hostname(),
92
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
93
+ })
94
+ );
95
+ } catch {
96
+ breakLockIfHolderIsDead(lockPath);
97
+ if (Date.now() >= deadline) return void 0;
98
+ pauseBetweenAttempts();
99
+ continue;
100
+ }
101
+ try {
102
+ return run();
103
+ } finally {
104
+ closeSync(lockFileDescriptor);
105
+ try {
106
+ unlinkSync(lockPath);
107
+ } catch {
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // src/guardrails/sessionState.ts
8
114
  var baseDir = (override) => override ?? join(homedir(), ".muggle-ai", "guardrails");
9
115
  var fileFor = (sessionId2, override) => join(baseDir(override), `${sessionId2.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
10
116
  function readState(sessionId2, dirOverride) {
@@ -19,7 +125,25 @@ function readState(sessionId2, dirOverride) {
19
125
  }
20
126
  function writeState(state, dirOverride) {
21
127
  mkdirSync(baseDir(dirOverride), { recursive: true });
22
- writeFileSync(fileFor(state.sessionId, dirOverride), JSON.stringify(state, null, 2));
128
+ const target = fileFor(state.sessionId, dirOverride);
129
+ const staging = `${target}.${process.pid}.tmp`;
130
+ writeFileSync(staging, JSON.stringify(state, null, 2));
131
+ renameSync(staging, target);
132
+ }
133
+ function updateState(sessionId2, mutate, dirOverride) {
134
+ mkdirSync(baseDir(dirOverride), { recursive: true });
135
+ const committed = withFileLock(
136
+ `${fileFor(sessionId2, dirOverride)}.lock`,
137
+ SESSION_STATE_LOCK_WAIT_MS,
138
+ () => {
139
+ const current = readState(sessionId2, dirOverride);
140
+ const next = mutate(current);
141
+ if (next === current) return true;
142
+ writeState({ ...next, generation: (current.generation ?? 0) + 1 }, dirOverride);
143
+ return true;
144
+ }
145
+ );
146
+ return committed === true;
23
147
  }
24
148
  function markPrHandled(sessionId2, prUrl, dirOverride) {
25
149
  const state = readState(sessionId2, dirOverride);
@@ -42,41 +166,22 @@ ${input2.tool_response?.output ?? ""}`;
42
166
  return m ? m[0] : null;
43
167
  }
44
168
 
45
- // src/guardrails/constants.ts
46
- var GH_PR_MERGED_LINE = /\b(?:Merged|Squashed and merged|Rebased and merged) pull request [\w./-]*#(\d+)/;
47
- var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
48
- var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
49
- var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
50
- var MAX_PR_TERMINAL_BLOCKS = 3;
51
- var MAX_WATCH_BLOCKS = 3;
52
- var MAX_WALKTHROUGH_BLOCKS = 3;
53
- var GH_LOOKUP_TIMEOUT_MS = 1e4;
54
- var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
55
- var MUGGLE_TEST_SKILL_NAME = "muggle-test";
56
- var MANDATORY_STAGES_FRONTMATTER_KEY = "mandatoryStages";
57
- var SKILL_NAME_INPUT_KEYS = ["skill", "skillName", "name", "command"];
58
- var MAX_STAGE_BLOCKS = 3;
59
- var MAX_DEBUG_BLOCKS = 3;
60
- var ANY_TEST_CASE = "*";
61
- var MUGGLE_EXECUTION_TOOL = /muggle-local-(execute-test-generation|execute-replay)/i;
62
- var MUGGLE_EVENT_EMIT_TOOL = /muggle-local-telemetry-event-emit/i;
63
- var MUGGLE_FEEDBACK_CREATE_TOOL = /muggle-remote-user-feedback-create/i;
64
- var PRE_EXECUTION_CLASSIFICATION_EVENT = "pre-execution-classification";
65
- var FAILURE_DIAGNOSIS_EVENT = /-failure-(classified|resolved)$/;
66
- var MUGGLE_RUN_ID_LINE = /\*\*Run ID:\*\*\s*([^\s*]+)/;
67
- var MUGGLE_RUN_STATUS_LINE = /\*\*Status:\*\*\s*([A-Za-z_]+)/;
68
- var MUGGLE_RUN_PASSED_STATUS = "passed";
69
-
70
169
  // src/guardrails/prTerminal.ts
170
+ function terminalProvenance(input2) {
171
+ const command = input2.tool_input?.command;
172
+ if (command === void 0) return { acceptsForgeLine: true };
173
+ return { acceptsForgeLine: FORGE_TERMINAL_CMD.test(command) };
174
+ }
71
175
  function detectPrTerminal(input2) {
72
176
  if (input2.tool_name !== "Bash" && input2.tool_name !== "Monitor") return null;
73
177
  const response = input2.tool_response;
178
+ const provenance = terminalProvenance(input2);
74
179
  const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
75
- const mergedMatch = haystack.match(GH_PR_MERGED_LINE);
180
+ const mergedMatch = provenance.acceptsForgeLine ? haystack.match(GH_PR_MERGED_LINE) : null;
76
181
  if (mergedMatch) {
77
182
  return { prNumber: Number(mergedMatch[1]), verdict: "merged" /* Merged */ };
78
183
  }
79
- const closedMatch = haystack.match(GH_PR_CLOSED_LINE);
184
+ const closedMatch = provenance.acceptsForgeLine ? haystack.match(GH_PR_CLOSED_LINE) : null;
80
185
  if (closedMatch) {
81
186
  return { prNumber: Number(closedMatch[1]), verdict: "closed" /* Closed */ };
82
187
  }
@@ -91,6 +196,7 @@ function detectPrTerminal(input2) {
91
196
  }
92
197
  function detectPrReopened(input2) {
93
198
  if (input2.tool_name !== "Bash") return null;
199
+ if (!terminalProvenance(input2).acceptsForgeLine) return null;
94
200
  const response = input2.tool_response;
95
201
  const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
96
202
  const reopenedMatch = haystack.match(GH_PR_REOPENED_LINE);
@@ -132,6 +238,39 @@ function prTerminalGateDecision(state, maxBlocks = MAX_PR_TERMINAL_BLOCKS) {
132
238
  }
133
239
  return { action: "block" /* Block */, blockCount: blockCount + 1 };
134
240
  }
241
+
242
+ // src/guardrails/capabilityClaim.ts
243
+ var SENTENCE = /[^.!?\n]+/g;
244
+ var IMPOSSIBILITY = /\b(?:can(?:'|’)?t|cannot|can not|could ?n(?:'|’)?t|unable to|no way to|not able to|impossible|infeasible|untestable|unverifiable|not testable|blocked from|no (?:local )?(?:mail|email|smtp) path)\b/i;
245
+ var MUGGLE_CLEARS = /\b(?:magic[- ]?link|sign[- ]?in link|login link|email link|one[- ]time (?:code|password)|otp|verification (?:code|link|email|mail)|email verification|confirmation (?:code|link|email|mail)|2fa|two[- ]factor|password reset|reset link|inbox|mailbox|mailhog|mailpit|mailtrap|mailcatcher|smtp|transactional email|captcha|recaptcha|hcaptcha|log ?in|logged[- ]?in|sign ?in|signed[- ]in|authenticate|credential)/i;
246
+ var TESTING_CONTEXT = /\b(?:e2e|end[- ]to[- ]end|test(?:s|ed|ing|able)?|verif(?:y|ied|ication)|validat(?:e|ed|ion)|automat(?:e|ed|ion)|coverage|covered|exercis(?:e|ed)|reproduc(?:e|ed)|replay|scripts?|scripted|muggle|playwright|cypress|selenium)\b/i;
247
+ var GENUINELY_UNSUPPORTED = /\b(?:sms|text message|phone (?:number|call)|authenticator app|google authenticator|authy|totp)\b/i;
248
+ function detectFalseCapabilityClaim(text) {
249
+ const sentences = (text ?? "").match(SENTENCE) ?? [];
250
+ return sentences.some(
251
+ (sentence) => IMPOSSIBILITY.test(sentence) && MUGGLE_CLEARS.test(sentence) && TESTING_CONTEXT.test(sentence) && !GENUINELY_UNSUPPORTED.test(sentence)
252
+ );
253
+ }
254
+ function lastAssistantText(transcriptJsonl) {
255
+ const lines = (transcriptJsonl ?? "").split("\n");
256
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
257
+ const line = lines[index]?.trim();
258
+ if (!line) continue;
259
+ let entry;
260
+ try {
261
+ entry = JSON.parse(line);
262
+ } catch {
263
+ continue;
264
+ }
265
+ if (entry.type !== "assistant") continue;
266
+ const content = entry.message?.content;
267
+ if (typeof content === "string") return content;
268
+ if (!Array.isArray(content)) continue;
269
+ const prose = content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
270
+ if (prose) return prose;
271
+ }
272
+ return "";
273
+ }
135
274
  var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
136
275
  var SEQUENCE_ENTRY = /^\s*-\s*(.+?)\s*$/;
137
276
  var FLOW_LIST = /^\s*\[(.*)\]\s*$/;
@@ -238,11 +377,28 @@ function stageGateDecision(state, unreadStagePaths, maxBlocks = MAX_STAGE_BLOCKS
238
377
  return { action: "block" /* Block */, blockCount: blockCount + 1, unread: unreadStagePaths };
239
378
  }
240
379
 
380
+ // src/guardrails/callOutcome.ts
381
+ function renderedResponse(toolResponse) {
382
+ const rendered = [];
383
+ const collect = (value) => {
384
+ if (typeof value === "string") rendered.push(value);
385
+ else if (Array.isArray(value)) value.forEach(collect);
386
+ else if (value && typeof value === "object") Object.values(value).forEach(collect);
387
+ };
388
+ collect(toolResponse);
389
+ return rendered.join("\n");
390
+ }
391
+ function callFailed(input2) {
392
+ const rendered = renderedResponse(input2.tool_response);
393
+ return CALL_FAILURE_SIGNALS.some((signal) => signal.test(rendered));
394
+ }
395
+
241
396
  // src/guardrails/preExecutionClassification.ts
242
397
  var CLASSIFICATION_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_CLASSIFY_SKIP\b/;
243
398
  function detectClassifiedTestCaseId(input2) {
244
399
  if (!MUGGLE_EVENT_EMIT_TOOL.test(input2.tool_name ?? "")) return void 0;
245
400
  if (input2.tool_input?.eventType !== PRE_EXECUTION_CLASSIFICATION_EVENT) return void 0;
401
+ if (callFailed(input2)) return void 0;
246
402
  return input2.tool_input?.testCaseId ?? ANY_TEST_CASE;
247
403
  }
248
404
  function applyClassifiedTestCase(state, testCaseId) {
@@ -299,6 +455,7 @@ function detectDebugEvidenceRunIds(input2, owedRunIds) {
299
455
  const toolName = input2.tool_name ?? "";
300
456
  const isDiagnosisEmit = MUGGLE_EVENT_EMIT_TOOL.test(toolName) && FAILURE_DIAGNOSIS_EVENT.test(input2.tool_input?.eventType ?? "");
301
457
  if (!isDiagnosisEmit && !MUGGLE_FEEDBACK_CREATE_TOOL.test(toolName)) return [];
458
+ if (callFailed(input2)) return [];
302
459
  const payload = serialize(input2);
303
460
  return owedRunIds.filter((runId) => payload.includes(runId));
304
461
  }
@@ -474,6 +631,7 @@ function detectWalkthroughPost(input2, read = defaultFileReader) {
474
631
  if (input2.tool_name !== "Bash") return false;
475
632
  const cmd = input2.tool_input?.command ?? "";
476
633
  if (!isPrReportPostCommand(cmd)) return false;
634
+ if (callFailed(input2)) return false;
477
635
  return collectPrPostText(cmd, input2.cwd, read).includes(REPORT_SENTINEL);
478
636
  }
479
637
  function applyWalkthroughPosted(state, posted) {
@@ -533,6 +691,245 @@ function walkthroughGateDecision(state, owedPrUrls, maxBlocks = MAX_WALKTHROUGH_
533
691
  return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedPrUrls };
534
692
  }
535
693
 
694
+ // src/guardrails/ledger/constants.ts
695
+ var LEDGER_FILE_NAME = "comment-ledger.json";
696
+ var LEDGER_VERSION = 1;
697
+ var LEDGER_LOCK_WAIT_MS = 3e4;
698
+ var LEDGER_COMMIT_ATTEMPTS = 5;
699
+ var FOREIGN_CLAIM_EXPIRY_MS = 6 * 60 * 60 * 1e3;
700
+
701
+ // src/guardrails/ledger/store.ts
702
+ var ledgerPath = (slotPath) => join(slotPath, LEDGER_FILE_NAME);
703
+ function newThreadEntry(provider) {
704
+ return {
705
+ provider,
706
+ generation: 0,
707
+ humanCommentIds: [],
708
+ coveredCommentIds: [],
709
+ claim: null,
710
+ lastClaimedBySessionId: null,
711
+ lastReplySha: null,
712
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
713
+ };
714
+ }
715
+ function readLedger(slotPath) {
716
+ const path = ledgerPath(slotPath);
717
+ if (!existsSync(path)) return { version: LEDGER_VERSION, threads: {} };
718
+ try {
719
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
720
+ return { version: parsed.version ?? LEDGER_VERSION, threads: parsed.threads ?? {} };
721
+ } catch {
722
+ return { version: LEDGER_VERSION, threads: {} };
723
+ }
724
+ }
725
+ function writeLedger(slotPath, ledger) {
726
+ const target = ledgerPath(slotPath);
727
+ const staging = `${target}.${process.pid}.tmp`;
728
+ writeFileSync(staging, JSON.stringify(ledger, null, 2));
729
+ renameSync(staging, target);
730
+ }
731
+ function commitThread(slotPath, threadId, mutate) {
732
+ mkdirSync(slotPath, { recursive: true });
733
+ for (let attempt = 0; attempt < LEDGER_COMMIT_ATTEMPTS; attempt += 1) {
734
+ const committed = withFileLock(`${ledgerPath(slotPath)}.lock`, LEDGER_LOCK_WAIT_MS, () => {
735
+ const ledger = readLedger(slotPath);
736
+ const current = ledger.threads[threadId] ?? newThreadEntry("github" /* GitHub */);
737
+ const next = mutate(current);
738
+ ledger.threads[threadId] = {
739
+ ...next,
740
+ generation: current.generation + 1,
741
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
742
+ };
743
+ writeLedger(slotPath, ledger);
744
+ return true;
745
+ });
746
+ if (committed === true) return true;
747
+ }
748
+ return false;
749
+ }
750
+ function isClaimLive(claim, now = Date.now()) {
751
+ if (!claim) return false;
752
+ if (claim.host !== hostname()) {
753
+ return now - Date.parse(claim.claimedAt) < FOREIGN_CLAIM_EXPIRY_MS;
754
+ }
755
+ return isProcessAlive(claim.pid);
756
+ }
757
+ function uncoveredComments(entry) {
758
+ const covered = new Set(entry.coveredCommentIds);
759
+ return entry.humanCommentIds.filter((commentId) => !covered.has(commentId));
760
+ }
761
+ function threadState(entry, now = Date.now()) {
762
+ if (uncoveredComments(entry).length === 0) return "processed" /* Processed */;
763
+ if (isClaimLive(entry.claim, now)) return "processing" /* Processing */;
764
+ return "unprocessed" /* Unprocessed */;
765
+ }
766
+ function claimThread(entry, sessionId2) {
767
+ return {
768
+ ...entry,
769
+ claim: {
770
+ sessionId: sessionId2,
771
+ pid: process.pid,
772
+ host: hostname(),
773
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString()
774
+ },
775
+ lastClaimedBySessionId: sessionId2
776
+ };
777
+ }
778
+ function coverComments(entry, commentIds, replySha) {
779
+ const covered = [...entry.coveredCommentIds];
780
+ for (const commentId of commentIds) if (!covered.includes(commentId)) covered.push(commentId);
781
+ return { ...entry, coveredCommentIds: covered, lastReplySha: replySha ?? entry.lastReplySha };
782
+ }
783
+ function refreshHumanComments(entry, humanCommentIds) {
784
+ return { ...entry, humanCommentIds: [...humanCommentIds] };
785
+ }
786
+
787
+ // src/guardrails/commentReply.ts
788
+ var REPLY_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_REPLY_SKIP\b/;
789
+ var REPLY_SHA_CITATION = /Addressed in ([0-9a-f]{7,40})/;
790
+ function parsedResponse(input2) {
791
+ for (const rendered of [input2.tool_response?.stdout, input2.tool_response?.output]) {
792
+ if (!rendered) continue;
793
+ try {
794
+ return JSON.parse(rendered);
795
+ } catch {
796
+ continue;
797
+ }
798
+ }
799
+ return void 0;
800
+ }
801
+ function collectThreads(value, found) {
802
+ if (Array.isArray(value)) {
803
+ value.forEach((entry) => collectThreads(entry, found));
804
+ return;
805
+ }
806
+ if (!value || typeof value !== "object") return;
807
+ const thread = value;
808
+ if (Array.isArray(thread.comments?.nodes) || Array.isArray(thread.notes)) {
809
+ found.push(thread);
810
+ return;
811
+ }
812
+ Object.values(value).forEach((nested) => collectThreads(nested, found));
813
+ }
814
+ var commentTimestamp = (comment) => comment.createdAt ?? comment.created_at ?? "";
815
+ function inChronologicalOrder(comments) {
816
+ if (comments.some((comment) => commentTimestamp(comment) === "")) return comments;
817
+ return [...comments].sort(
818
+ (earlier, later) => commentTimestamp(earlier).localeCompare(commentTimestamp(later))
819
+ );
820
+ }
821
+ var isLoopAuthored = (comment) => (comment.body ?? "").includes(LOOP_REPLY_MARKER);
822
+ function commentsAwaitingReply(comments) {
823
+ const ordered = inChronologicalOrder(comments);
824
+ let lastLoopIndex = -1;
825
+ ordered.forEach((comment, index) => {
826
+ if (isLoopAuthored(comment)) lastLoopIndex = index;
827
+ });
828
+ return ordered.slice(lastLoopIndex + 1).filter((comment) => !isLoopAuthored(comment));
829
+ }
830
+ function githubThread(thread) {
831
+ if (thread.isResolved === true || !thread.id) return void 0;
832
+ const awaiting = commentsAwaitingReply(thread.comments?.nodes ?? []);
833
+ const humanCommentIds = awaiting.map((comment) => comment.databaseId).filter((databaseId) => databaseId !== void 0).map(String);
834
+ if (humanCommentIds.length === 0) return void 0;
835
+ return {
836
+ threadId: thread.id,
837
+ provider: "github" /* GitHub */,
838
+ humanCommentIds
839
+ };
840
+ }
841
+ function gitlabThread(thread) {
842
+ const notes = thread.notes ?? [];
843
+ if (!notes.some((note) => note.resolved === false) || !thread.id) return void 0;
844
+ const humanCommentIds = commentsAwaitingReply(notes).map((note) => note.id).filter((noteId) => noteId !== void 0).map(String);
845
+ if (humanCommentIds.length === 0) return void 0;
846
+ return {
847
+ threadId: thread.id,
848
+ provider: "gitlab" /* GitLab */,
849
+ humanCommentIds
850
+ };
851
+ }
852
+ function detectUnansweredThreads(input2) {
853
+ if (input2.tool_name !== "Bash") return [];
854
+ if (!REVIEW_THREAD_FETCH_COMMAND.test(input2.tool_input?.command ?? "")) return [];
855
+ const threads = [];
856
+ collectThreads(parsedResponse(input2), threads);
857
+ return threads.map((thread) => Array.isArray(thread.notes) ? gitlabThread(thread) : githubThread(thread)).filter((thread) => thread !== void 0);
858
+ }
859
+ function detectConfirmedReplies(input2) {
860
+ if (input2.tool_name !== "Bash") return [];
861
+ const command = input2.tool_input?.command ?? "";
862
+ const targets = [...command.matchAll(THREADED_REPLY_TARGET)].map(
863
+ ([, githubCommentId, gitlabDiscussionId]) => githubCommentId ?? gitlabDiscussionId
864
+ );
865
+ if (targets.length === 0) return [];
866
+ const created = parsedResponse(input2);
867
+ if (created?.id === void 0) return [];
868
+ const replySha = REPLY_SHA_CITATION.exec(created.body ?? "")?.[1] ?? null;
869
+ return targets.map((targetId) => ({ targetId, replySha }));
870
+ }
871
+ function isReplySkipMarker(command) {
872
+ return REPLY_SKIP_MARKER.test(command);
873
+ }
874
+ function threadForReplyTarget(ledger, targetId) {
875
+ if (ledger.threads[targetId]) return targetId;
876
+ for (const [threadId, entry] of Object.entries(ledger.threads)) {
877
+ if (entry.humanCommentIds.includes(targetId)) return threadId;
878
+ }
879
+ return void 0;
880
+ }
881
+ function resolveSlotForPr(prUrl, sessionsDirOverride) {
882
+ const sessionsDir = join(homedir(), ".muggle-ai", "muggle-do", "sessions");
883
+ if (!existsSync(sessionsDir)) return void 0;
884
+ for (const slug of readdirSync(sessionsDir)) {
885
+ const slotPath = join(sessionsDir, slug);
886
+ try {
887
+ const parsed = JSON.parse(readFileSync(join(slotPath, "prs.json"), "utf-8"));
888
+ const slotPr = Array.isArray(parsed) ? parsed[0] : parsed;
889
+ if (slotPr?.url === prUrl) return slotPath;
890
+ } catch {
891
+ continue;
892
+ }
893
+ }
894
+ return void 0;
895
+ }
896
+ function overdueThreads(slotPath, sessionId2) {
897
+ const overdue = [];
898
+ for (const [threadId, entry] of Object.entries(readLedger(slotPath).threads)) {
899
+ if (entry.lastClaimedBySessionId !== sessionId2) continue;
900
+ if (threadState(entry) === "processed" /* Processed */) continue;
901
+ overdue.push({ threadId, uncovered: uncoveredComments(entry) });
902
+ }
903
+ return overdue;
904
+ }
905
+ function commentReplyGateDecision(state, overdue, maxBlocks = MAX_REPLY_BLOCKS) {
906
+ const blockCount = state.commentReplyBlockCount ?? 0;
907
+ const unanswered = overdue.flatMap((thread) => thread.uncovered);
908
+ if (state.commentReplySkipped === true || unanswered.length === 0) {
909
+ return { action: "none" /* None */, blockCount, unanswered };
910
+ }
911
+ if (blockCount >= maxBlocks) {
912
+ return {
913
+ action: "release" /* Release */,
914
+ blockCount,
915
+ unanswered
916
+ };
917
+ }
918
+ return {
919
+ action: "block" /* Block */,
920
+ blockCount: blockCount + 1,
921
+ unanswered
922
+ };
923
+ }
924
+ function deferredCommentIds(ledger, command) {
925
+ const deferred = [];
926
+ for (const [threadId, entry] of Object.entries(ledger.threads)) {
927
+ const named = uncoveredComments(entry).filter((commentId) => command.includes(commentId));
928
+ if (named.length > 0) deferred.push({ threadId, commentIds: named });
929
+ }
930
+ return deferred;
931
+ }
932
+
536
933
  // src/guardrails/detectBuildIntent.ts
537
934
  var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
538
935
  var DEVCYCLE = /\bresolve\b[^.?!]{0,40}\bconflicts?\b|\bget\b[^.?!]{0,40}\bpr\b[^.?!]{0,40}\b(green|merged?|passing)\b/i;
@@ -567,6 +964,21 @@ function evaluateReportPost(input2, read = defaultFileReader) {
567
964
  };
568
965
  }
569
966
 
967
+ // src/guardrails/reviewThreadResolve.ts
968
+ function detectResolveCall(command) {
969
+ if (!PROVIDER_API_INVOCATION.test(command)) return null;
970
+ if (GITHUB_RESOLVE_THREAD_MUTATION.test(command)) return "github" /* GitHub */;
971
+ if (GITLAB_RESOLVE_DISCUSSION_CALL.test(command)) return "gitlab" /* GitLab */;
972
+ return null;
973
+ }
974
+ var RESOLVE_DENIAL = "Blocked: resolving a review thread is the reviewer's call, not the loop's. Reply to the thread instead \u2014 the `<!-- muggle-do:bot -->` marker on that reply is what retires it (a thread is actionable only while it is unresolved AND its newest comment is unmarked), so resolving buys no echo protection and costs the reviewer their record of what is still unverified. The loop has twice hidden threads carrying fixes that were partly wrong. If a thread genuinely warrants resolving, say so and let the reviewer close it in the UI; to nudge, run the resolve-reminder stage, which lists addressed-but-open threads without touching them.";
975
+ function evaluateReviewThreadResolve(input2) {
976
+ if (input2.tool_name !== "Bash") return { deny: false };
977
+ const provider = detectResolveCall(input2.tool_input?.command ?? "");
978
+ if (!provider) return { deny: false };
979
+ return { deny: true, reason: RESOLVE_DENIAL };
980
+ }
981
+
570
982
  // src/guardrails/emit.ts
571
983
  function envelope(eventName, context, host2) {
572
984
  if (!context) return "{}";
@@ -604,6 +1016,13 @@ var host = process.env.CURSOR_PLUGIN_ROOT ? "cursor" : "claude";
604
1016
  var sub = process.argv[2];
605
1017
  var input = readStdin();
606
1018
  var sessionId = input.session_id ?? "unknown";
1019
+ function releaseGate(field) {
1020
+ updateState(
1021
+ sessionId,
1022
+ (current) => current[field] === true ? current : { ...current, [field]: true }
1023
+ );
1024
+ return "{}";
1025
+ }
607
1026
  function prOpened() {
608
1027
  const url = detectPrOpened(input);
609
1028
  if (!url) return "{}";
@@ -640,6 +1059,7 @@ function offerRan() {
640
1059
  function terminalGate() {
641
1060
  const state = readState(sessionId);
642
1061
  const decision = prTerminalGateDecision(state);
1062
+ if (decision.action === "release" /* Release */) return releaseGate("terminalReleased");
643
1063
  if (decision.action !== "block" /* Block */) return "{}";
644
1064
  state.terminalBlockCount = decision.blockCount;
645
1065
  writeState(state);
@@ -713,6 +1133,7 @@ function classifyGate() {
713
1133
  function stageGate() {
714
1134
  const state = readState(sessionId);
715
1135
  const decision = stageGateDecision(state, unreadMandatoryStages(state));
1136
+ if (decision.action === "release" /* Release */) return releaseGate("stageReleased");
716
1137
  if (decision.action !== "block" /* Block */) return "{}";
717
1138
  state.stageBlockCount = decision.blockCount;
718
1139
  writeState(state);
@@ -723,6 +1144,7 @@ function stageGate() {
723
1144
  function debugPathGate() {
724
1145
  const state = readState(sessionId);
725
1146
  const decision = debugGateDecision(state);
1147
+ if (decision.action === "release" /* Release */) return releaseGate("debugReleased");
726
1148
  if (decision.action !== "block" /* Block */) return "{}";
727
1149
  state.debugBlockCount = decision.blockCount;
728
1150
  writeState(state);
@@ -733,7 +1155,8 @@ function debugPathGate() {
733
1155
  function e2eGate() {
734
1156
  const state = readState(sessionId);
735
1157
  const decision = e2eGateDecision(state);
736
- if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) return "{}";
1158
+ if (decision.action === "release" /* Release */) return releaseGate("e2eReleased");
1159
+ if (decision.action === "none" /* None */) return "{}";
737
1160
  state.e2eBlockCount = decision.blockCount;
738
1161
  writeState(state);
739
1162
  const reason = decision.blockCount === 1 ? `Do not end the turn yet. Unit tests passed this session but no E2E acceptance run has happened. Per the autoE2ETest preference (default: always), run change-driven E2E now via /muggle:muggle-test, then finish. If E2E genuinely cannot run here (no app to drive, services down, no PR), tell the user why and run \`echo "MUGGLE_E2E_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `E2E acceptance run still owed (reminder ${decision.blockCount}/${MAX_E2E_BLOCKS}): run /muggle:muggle-test, or record a legitimate skip via \`echo "MUGGLE_E2E_SKIP: <reason>"\`.`;
@@ -743,9 +1166,8 @@ function watchGate() {
743
1166
  const state = readState(sessionId);
744
1167
  const untrackedPrUrls = findUntrackedHandledPrs(state.prsHandled);
745
1168
  const decision = watchGateDecision(state, untrackedPrUrls);
746
- if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) {
747
- return "{}";
748
- }
1169
+ if (decision.action === "release" /* Release */) return releaseGate("watchReleased");
1170
+ if (decision.action === "none" /* None */) return "{}";
749
1171
  state.watchBlockCount = decision.blockCount;
750
1172
  writeState(state);
751
1173
  const prList = decision.untracked.join(", ");
@@ -763,6 +1185,7 @@ function walkthroughGate() {
763
1185
  return "{}";
764
1186
  }
765
1187
  const decision = walkthroughGateDecision(state, scan.owed);
1188
+ if (decision.action === "release" /* Release */) return releaseGate("walkthroughReleased");
766
1189
  if (decision.action !== "block" /* Block */) return "{}";
767
1190
  state.walkthroughBlockCount = decision.blockCount;
768
1191
  writeState(state);
@@ -770,11 +1193,73 @@ function walkthroughGate() {
770
1193
  const reason = decision.blockCount === 1 ? `Do not end the turn yet. An E2E acceptance run happened this session but no visual walkthrough has reached ${prList}. Per the postPRVisualWalkthrough preference (default: always), post it now via /muggle:muggle-pr-visual-walkthrough \u2014 include the failed runs, which are the ones reviewers most need to see. If this result genuinely should not be posted (postPRVisualWalkthrough=never, someone else's PR, nothing renderable), tell the user why and run \`echo "MUGGLE_WALKTHROUGH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Walkthrough still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WALKTHROUGH_BLOCKS}): post via /muggle:muggle-pr-visual-walkthrough, or record a legitimate skip via \`echo "MUGGLE_WALKTHROUGH_SKIP: <reason>"\`.`;
771
1194
  return blockStop(reason, host);
772
1195
  }
1196
+ function slotForSession(state) {
1197
+ for (const prUrl of state.prsHandled) {
1198
+ const slotPath = resolveSlotForPr(prUrl);
1199
+ if (slotPath) return slotPath;
1200
+ }
1201
+ return void 0;
1202
+ }
1203
+ function recordCommentReplies() {
1204
+ const cmd = input.tool_input?.command ?? "";
1205
+ const state = readState(sessionId);
1206
+ const slotPath = slotForSession(state);
1207
+ if (!slotPath) return "{}";
1208
+ if (isReplySkipMarker(cmd)) {
1209
+ const deferred = deferredCommentIds(readLedger(slotPath), cmd);
1210
+ if (deferred.length === 0) {
1211
+ updateState(sessionId, (current) => ({ ...current, commentReplySkipped: true }));
1212
+ return "{}";
1213
+ }
1214
+ for (const { threadId, commentIds } of deferred) {
1215
+ commitThread(slotPath, threadId, (entry) => coverComments(entry, commentIds, null));
1216
+ }
1217
+ return "{}";
1218
+ }
1219
+ for (const thread of detectUnansweredThreads(input)) {
1220
+ commitThread(
1221
+ slotPath,
1222
+ thread.threadId,
1223
+ (entry) => claimThread(
1224
+ refreshHumanComments({ ...entry, provider: thread.provider }, thread.humanCommentIds),
1225
+ sessionId
1226
+ )
1227
+ );
1228
+ }
1229
+ const confirmed = detectConfirmedReplies(input);
1230
+ if (confirmed.length > 0) {
1231
+ const ledger = readLedger(slotPath);
1232
+ for (const reply of confirmed) {
1233
+ const threadId = threadForReplyTarget(ledger, reply.targetId);
1234
+ if (!threadId) continue;
1235
+ const covered = threadId === reply.targetId ? uncoveredComments(ledger.threads[threadId]) : [reply.targetId];
1236
+ commitThread(slotPath, threadId, (entry) => coverComments(entry, covered, reply.replySha));
1237
+ }
1238
+ }
1239
+ return "{}";
1240
+ }
1241
+ function commentReplyGate() {
1242
+ const state = readState(sessionId);
1243
+ const slotPath = slotForSession(state);
1244
+ if (!slotPath) return "{}";
1245
+ const decision = commentReplyGateDecision(state, overdueThreads(slotPath, sessionId));
1246
+ if (decision.action === "release" /* Release */) return releaseGate("commentReplyReleased");
1247
+ if (decision.action !== "block" /* Block */) return "{}";
1248
+ updateState(sessionId, (current) => ({ ...current, commentReplyBlockCount: decision.blockCount }));
1249
+ const commentList = decision.unanswered.join(", ");
1250
+ const reason = decision.blockCount === 1 ? `Do not end the turn yet. This session took these review comments as work and never answered them: ${commentList}. Post one reply per comment in its own thread per muggle-do do/per-comment-replies.md \u2014 "Addressed in <short-sha>: <what changed for THIS comment>", signed via sign-body.sh --mode loop. A silent round leaves the reviewer with no answer in the thread, and the loop marker that reply carries is the only thing that stops the watcher re-dispatching the same thread next tick. If a comment was escalated to the user instead of answered, record it as deferred rather than replying.` : `Review comments still owe a threaded reply (reminder ${decision.blockCount}/${MAX_REPLY_BLOCKS}): ${commentList}. Reply per do/per-comment-replies.md, or record the deferral.`;
1251
+ return blockStop(reason, host);
1252
+ }
773
1253
  function reportGate() {
774
1254
  const reportPostVerdict = evaluateReportPost(input);
775
1255
  if (!reportPostVerdict.deny || !reportPostVerdict.reason) return "{}";
776
1256
  return denyTool(reportPostVerdict.reason, host);
777
1257
  }
1258
+ function resolveGate() {
1259
+ const resolveVerdict = evaluateReviewThreadResolve(input);
1260
+ if (!resolveVerdict.deny || !resolveVerdict.reason) return "{}";
1261
+ return denyTool(resolveVerdict.reason, host);
1262
+ }
778
1263
  function buildRouter() {
779
1264
  if (!detectBuildIntent(input.prompt ?? "")) return "{}";
780
1265
  const state = readState(sessionId);
@@ -784,8 +1269,28 @@ function buildRouter() {
784
1269
  const ctx = `This looks like a build/implement/fix request. Per the autoRouteBuildToMuggleDo preference, route it through /muggle-do \u2014 which runs requirements \u2192 build (delegated to superpowers' design\u2192plan\u2192review) \u2192 impact \u2192 unit tests \u2192 E2E \u2192 PR \u2192 watcher. If autoRouteBuildToMuggleDo=always, enter that flow; if =ask, offer it; if =never, proceed normally.`;
785
1270
  return envelope("UserPromptSubmit", ctx, host);
786
1271
  }
1272
+ function capabilityClaimGate() {
1273
+ const state = readState(sessionId);
1274
+ if (state.capabilityClaimNudged) return "{}";
1275
+ const transcriptPath = input.transcript_path;
1276
+ if (!transcriptPath) return "{}";
1277
+ let transcriptTail;
1278
+ try {
1279
+ transcriptTail = readFileSync(transcriptPath, "utf-8").slice(
1280
+ -CAPABILITY_CLAIM_TRANSCRIPT_TAIL_BYTES
1281
+ );
1282
+ } catch {
1283
+ return "{}";
1284
+ }
1285
+ if (!detectFalseCapabilityClaim(lastAssistantText(transcriptTail))) return "{}";
1286
+ state.capabilityClaimNudged = true;
1287
+ writeState(state);
1288
+ const reason = `Do not end the turn yet. This turn tells the user that an email- or login-gated flow cannot be tested, reached, or verified \u2014 the one class of blocker Muggle clears. Every managed login profile owns a live inbox, so magic-link and sign-in-link logins, emailed OTPs, email-based 2FA, verification mail, and password resets run end to end with no mail stub and no test-only backdoor; stored credentials and CAPTCHA solving are covered too. Re-read plugin/skills/_shared/identity-and-inbox.md, then correct the claim or run the flow. If the real blocker is SMS/phone OTP, authenticator-app TOTP, or OAuth-only social login, those limits are genuine \u2014 name the one that applies and move on.`;
1289
+ return blockStop(reason, host);
1290
+ }
787
1291
  var handlers = {
788
1292
  "pr-opened": prOpened,
1293
+ "capability-claim-gate": capabilityClaimGate,
789
1294
  "pr-terminal": prTerminal,
790
1295
  "offer-ran": offerRan,
791
1296
  "record-tests": recordTests,
@@ -793,7 +1298,10 @@ var handlers = {
793
1298
  "terminal-gate": terminalGate,
794
1299
  "watch-gate": watchGate,
795
1300
  "walkthrough-gate": walkthroughGate,
1301
+ "record-comment-replies": recordCommentReplies,
1302
+ "comment-reply-gate": commentReplyGate,
796
1303
  "report-gate": reportGate,
1304
+ "resolve-gate": resolveGate,
797
1305
  "build-router": buildRouter,
798
1306
  "skill-stages": skillStages,
799
1307
  "record-stage-read": recordStageRead,