@muggleai/works 5.11.1 → 5.12.0-staging.73

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 (90) hide show
  1. package/config/runtime-targets.json +33 -0
  2. package/dist/{chunk-MSPMXBO5.js → chunk-FCDHJD6I.js} +441 -347
  3. package/dist/{chunk-WUMAJOBT.js → chunk-O7LFABOC.js} +39 -35
  4. package/dist/cli.js +2 -2
  5. package/dist/index.js +2 -2
  6. package/dist/plugin/hooks/README.md +4 -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 +506 -32
  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-TFNPZD5X.js +1 -0
  46. package/package.json +17 -8
  47. package/plugin/hooks/README.md +4 -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 +506 -32
  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 +59 -2
  86. package/dist/plugin/skills/_shared/rebase-before-e2e.md +0 -21
  87. package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +0 -10
  88. package/dist/src-B5OTAZKM.js +0 -1
  89. package/plugin/skills/_shared/rebase-before-e2e.md +0 -21
  90. package/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +0 -10
@@ -1,10 +1,108 @@
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
+
44
+ // src/guardrails/store/fileLock.ts
45
+ function isProcessAlive(pid) {
46
+ try {
47
+ process.kill(pid, 0);
48
+ return true;
49
+ } catch (error) {
50
+ return error.code === "EPERM";
51
+ }
52
+ }
53
+ function recordedHolder(lockPath) {
54
+ try {
55
+ return JSON.parse(readFileSync(lockPath, "utf-8"));
56
+ } catch {
57
+ return void 0;
58
+ }
59
+ }
60
+ function breakLockIfHolderIsDead(lockPath) {
61
+ const holder = recordedHolder(lockPath);
62
+ if (!holder) return;
63
+ if (holder.host !== hostname() || isProcessAlive(holder.pid)) return;
64
+ try {
65
+ unlinkSync(lockPath);
66
+ } catch {
67
+ return;
68
+ }
69
+ }
70
+ function pauseBetweenAttempts() {
71
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_POLL_INTERVAL_MS);
72
+ }
73
+ function withFileLock(lockPath, waitMs, run) {
74
+ const deadline = Date.now() + waitMs;
75
+ for (; ; ) {
76
+ let lockFileDescriptor;
77
+ try {
78
+ lockFileDescriptor = openSync(lockPath, "wx");
79
+ writeSync(
80
+ lockFileDescriptor,
81
+ JSON.stringify({
82
+ pid: process.pid,
83
+ host: hostname(),
84
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
85
+ })
86
+ );
87
+ } catch {
88
+ breakLockIfHolderIsDead(lockPath);
89
+ if (Date.now() >= deadline) return void 0;
90
+ pauseBetweenAttempts();
91
+ continue;
92
+ }
93
+ try {
94
+ return run();
95
+ } finally {
96
+ closeSync(lockFileDescriptor);
97
+ try {
98
+ unlinkSync(lockPath);
99
+ } catch {
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ // src/guardrails/sessionState.ts
8
106
  var baseDir = (override) => override ?? join(homedir(), ".muggle-ai", "guardrails");
9
107
  var fileFor = (sessionId2, override) => join(baseDir(override), `${sessionId2.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
10
108
  function readState(sessionId2, dirOverride) {
@@ -19,7 +117,25 @@ function readState(sessionId2, dirOverride) {
19
117
  }
20
118
  function writeState(state, dirOverride) {
21
119
  mkdirSync(baseDir(dirOverride), { recursive: true });
22
- writeFileSync(fileFor(state.sessionId, dirOverride), JSON.stringify(state, null, 2));
120
+ const target = fileFor(state.sessionId, dirOverride);
121
+ const staging = `${target}.${process.pid}.tmp`;
122
+ writeFileSync(staging, JSON.stringify(state, null, 2));
123
+ renameSync(staging, target);
124
+ }
125
+ function updateState(sessionId2, mutate, dirOverride) {
126
+ mkdirSync(baseDir(dirOverride), { recursive: true });
127
+ const committed = withFileLock(
128
+ `${fileFor(sessionId2, dirOverride)}.lock`,
129
+ SESSION_STATE_LOCK_WAIT_MS,
130
+ () => {
131
+ const current = readState(sessionId2, dirOverride);
132
+ const next = mutate(current);
133
+ if (next === current) return true;
134
+ writeState({ ...next, generation: (current.generation ?? 0) + 1 }, dirOverride);
135
+ return true;
136
+ }
137
+ );
138
+ return committed === true;
23
139
  }
24
140
  function markPrHandled(sessionId2, prUrl, dirOverride) {
25
141
  const state = readState(sessionId2, dirOverride);
@@ -42,31 +158,6 @@ ${input2.tool_response?.output ?? ""}`;
42
158
  return m ? m[0] : null;
43
159
  }
44
160
 
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
161
  // src/guardrails/prTerminal.ts
71
162
  function detectPrTerminal(input2) {
72
163
  if (input2.tool_name !== "Bash" && input2.tool_name !== "Monitor") return null;
@@ -132,6 +223,39 @@ function prTerminalGateDecision(state, maxBlocks = MAX_PR_TERMINAL_BLOCKS) {
132
223
  }
133
224
  return { action: "block" /* Block */, blockCount: blockCount + 1 };
134
225
  }
226
+
227
+ // src/guardrails/capabilityClaim.ts
228
+ var SENTENCE = /[^.!?\n]+/g;
229
+ 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;
230
+ 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;
231
+ 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;
232
+ var GENUINELY_UNSUPPORTED = /\b(?:sms|text message|phone (?:number|call)|authenticator app|google authenticator|authy|totp)\b/i;
233
+ function detectFalseCapabilityClaim(text) {
234
+ const sentences = (text ?? "").match(SENTENCE) ?? [];
235
+ return sentences.some(
236
+ (sentence) => IMPOSSIBILITY.test(sentence) && MUGGLE_CLEARS.test(sentence) && TESTING_CONTEXT.test(sentence) && !GENUINELY_UNSUPPORTED.test(sentence)
237
+ );
238
+ }
239
+ function lastAssistantText(transcriptJsonl) {
240
+ const lines = (transcriptJsonl ?? "").split("\n");
241
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
242
+ const line = lines[index]?.trim();
243
+ if (!line) continue;
244
+ let entry;
245
+ try {
246
+ entry = JSON.parse(line);
247
+ } catch {
248
+ continue;
249
+ }
250
+ if (entry.type !== "assistant") continue;
251
+ const content = entry.message?.content;
252
+ if (typeof content === "string") return content;
253
+ if (!Array.isArray(content)) continue;
254
+ const prose = content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
255
+ if (prose) return prose;
256
+ }
257
+ return "";
258
+ }
135
259
  var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
136
260
  var SEQUENCE_ENTRY = /^\s*-\s*(.+?)\s*$/;
137
261
  var FLOW_LIST = /^\s*\[(.*)\]\s*$/;
@@ -533,6 +657,245 @@ function walkthroughGateDecision(state, owedPrUrls, maxBlocks = MAX_WALKTHROUGH_
533
657
  return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedPrUrls };
534
658
  }
535
659
 
660
+ // src/guardrails/ledger/constants.ts
661
+ var LEDGER_FILE_NAME = "comment-ledger.json";
662
+ var LEDGER_VERSION = 1;
663
+ var LEDGER_LOCK_WAIT_MS = 3e4;
664
+ var LEDGER_COMMIT_ATTEMPTS = 5;
665
+ var FOREIGN_CLAIM_EXPIRY_MS = 6 * 60 * 60 * 1e3;
666
+
667
+ // src/guardrails/ledger/store.ts
668
+ var ledgerPath = (slotPath) => join(slotPath, LEDGER_FILE_NAME);
669
+ function newThreadEntry(provider) {
670
+ return {
671
+ provider,
672
+ generation: 0,
673
+ humanCommentIds: [],
674
+ coveredCommentIds: [],
675
+ claim: null,
676
+ lastClaimedBySessionId: null,
677
+ lastReplySha: null,
678
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
679
+ };
680
+ }
681
+ function readLedger(slotPath) {
682
+ const path = ledgerPath(slotPath);
683
+ if (!existsSync(path)) return { version: LEDGER_VERSION, threads: {} };
684
+ try {
685
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
686
+ return { version: parsed.version ?? LEDGER_VERSION, threads: parsed.threads ?? {} };
687
+ } catch {
688
+ return { version: LEDGER_VERSION, threads: {} };
689
+ }
690
+ }
691
+ function writeLedger(slotPath, ledger) {
692
+ const target = ledgerPath(slotPath);
693
+ const staging = `${target}.${process.pid}.tmp`;
694
+ writeFileSync(staging, JSON.stringify(ledger, null, 2));
695
+ renameSync(staging, target);
696
+ }
697
+ function commitThread(slotPath, threadId, mutate) {
698
+ mkdirSync(slotPath, { recursive: true });
699
+ for (let attempt = 0; attempt < LEDGER_COMMIT_ATTEMPTS; attempt += 1) {
700
+ const committed = withFileLock(`${ledgerPath(slotPath)}.lock`, LEDGER_LOCK_WAIT_MS, () => {
701
+ const ledger = readLedger(slotPath);
702
+ const current = ledger.threads[threadId] ?? newThreadEntry("github" /* GitHub */);
703
+ const next = mutate(current);
704
+ ledger.threads[threadId] = {
705
+ ...next,
706
+ generation: current.generation + 1,
707
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
708
+ };
709
+ writeLedger(slotPath, ledger);
710
+ return true;
711
+ });
712
+ if (committed === true) return true;
713
+ }
714
+ return false;
715
+ }
716
+ function isClaimLive(claim, now = Date.now()) {
717
+ if (!claim) return false;
718
+ if (claim.host !== hostname()) {
719
+ return now - Date.parse(claim.claimedAt) < FOREIGN_CLAIM_EXPIRY_MS;
720
+ }
721
+ return isProcessAlive(claim.pid);
722
+ }
723
+ function uncoveredComments(entry) {
724
+ const covered = new Set(entry.coveredCommentIds);
725
+ return entry.humanCommentIds.filter((commentId) => !covered.has(commentId));
726
+ }
727
+ function threadState(entry, now = Date.now()) {
728
+ if (uncoveredComments(entry).length === 0) return "processed" /* Processed */;
729
+ if (isClaimLive(entry.claim, now)) return "processing" /* Processing */;
730
+ return "unprocessed" /* Unprocessed */;
731
+ }
732
+ function claimThread(entry, sessionId2) {
733
+ return {
734
+ ...entry,
735
+ claim: {
736
+ sessionId: sessionId2,
737
+ pid: process.pid,
738
+ host: hostname(),
739
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString()
740
+ },
741
+ lastClaimedBySessionId: sessionId2
742
+ };
743
+ }
744
+ function coverComments(entry, commentIds, replySha) {
745
+ const covered = [...entry.coveredCommentIds];
746
+ for (const commentId of commentIds) if (!covered.includes(commentId)) covered.push(commentId);
747
+ return { ...entry, coveredCommentIds: covered, lastReplySha: replySha ?? entry.lastReplySha };
748
+ }
749
+ function refreshHumanComments(entry, humanCommentIds) {
750
+ return { ...entry, humanCommentIds: [...humanCommentIds] };
751
+ }
752
+
753
+ // src/guardrails/commentReply.ts
754
+ var REPLY_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_REPLY_SKIP\b/;
755
+ var REPLY_SHA_CITATION = /Addressed in ([0-9a-f]{7,40})/;
756
+ function parsedResponse(input2) {
757
+ for (const rendered of [input2.tool_response?.stdout, input2.tool_response?.output]) {
758
+ if (!rendered) continue;
759
+ try {
760
+ return JSON.parse(rendered);
761
+ } catch {
762
+ continue;
763
+ }
764
+ }
765
+ return void 0;
766
+ }
767
+ function collectThreads(value, found) {
768
+ if (Array.isArray(value)) {
769
+ value.forEach((entry) => collectThreads(entry, found));
770
+ return;
771
+ }
772
+ if (!value || typeof value !== "object") return;
773
+ const thread = value;
774
+ if (Array.isArray(thread.comments?.nodes) || Array.isArray(thread.notes)) {
775
+ found.push(thread);
776
+ return;
777
+ }
778
+ Object.values(value).forEach((nested) => collectThreads(nested, found));
779
+ }
780
+ var commentTimestamp = (comment) => comment.createdAt ?? comment.created_at ?? "";
781
+ function inChronologicalOrder(comments) {
782
+ if (comments.some((comment) => commentTimestamp(comment) === "")) return comments;
783
+ return [...comments].sort(
784
+ (earlier, later) => commentTimestamp(earlier).localeCompare(commentTimestamp(later))
785
+ );
786
+ }
787
+ var isLoopAuthored = (comment) => (comment.body ?? "").includes(LOOP_REPLY_MARKER);
788
+ function commentsAwaitingReply(comments) {
789
+ const ordered = inChronologicalOrder(comments);
790
+ let lastLoopIndex = -1;
791
+ ordered.forEach((comment, index) => {
792
+ if (isLoopAuthored(comment)) lastLoopIndex = index;
793
+ });
794
+ return ordered.slice(lastLoopIndex + 1).filter((comment) => !isLoopAuthored(comment));
795
+ }
796
+ function githubThread(thread) {
797
+ if (thread.isResolved === true || !thread.id) return void 0;
798
+ const awaiting = commentsAwaitingReply(thread.comments?.nodes ?? []);
799
+ const humanCommentIds = awaiting.map((comment) => comment.databaseId).filter((databaseId) => databaseId !== void 0).map(String);
800
+ if (humanCommentIds.length === 0) return void 0;
801
+ return {
802
+ threadId: thread.id,
803
+ provider: "github" /* GitHub */,
804
+ humanCommentIds
805
+ };
806
+ }
807
+ function gitlabThread(thread) {
808
+ const notes = thread.notes ?? [];
809
+ if (!notes.some((note) => note.resolved === false) || !thread.id) return void 0;
810
+ const humanCommentIds = commentsAwaitingReply(notes).map((note) => note.id).filter((noteId) => noteId !== void 0).map(String);
811
+ if (humanCommentIds.length === 0) return void 0;
812
+ return {
813
+ threadId: thread.id,
814
+ provider: "gitlab" /* GitLab */,
815
+ humanCommentIds
816
+ };
817
+ }
818
+ function detectUnansweredThreads(input2) {
819
+ if (input2.tool_name !== "Bash") return [];
820
+ if (!REVIEW_THREAD_FETCH_COMMAND.test(input2.tool_input?.command ?? "")) return [];
821
+ const threads = [];
822
+ collectThreads(parsedResponse(input2), threads);
823
+ return threads.map((thread) => Array.isArray(thread.notes) ? gitlabThread(thread) : githubThread(thread)).filter((thread) => thread !== void 0);
824
+ }
825
+ function detectConfirmedReplies(input2) {
826
+ if (input2.tool_name !== "Bash") return [];
827
+ const command = input2.tool_input?.command ?? "";
828
+ const targets = [...command.matchAll(THREADED_REPLY_TARGET)].map(
829
+ ([, githubCommentId, gitlabDiscussionId]) => githubCommentId ?? gitlabDiscussionId
830
+ );
831
+ if (targets.length === 0) return [];
832
+ const created = parsedResponse(input2);
833
+ if (created?.id === void 0) return [];
834
+ const replySha = REPLY_SHA_CITATION.exec(created.body ?? "")?.[1] ?? null;
835
+ return targets.map((targetId) => ({ targetId, replySha }));
836
+ }
837
+ function isReplySkipMarker(command) {
838
+ return REPLY_SKIP_MARKER.test(command);
839
+ }
840
+ function threadForReplyTarget(ledger, targetId) {
841
+ if (ledger.threads[targetId]) return targetId;
842
+ for (const [threadId, entry] of Object.entries(ledger.threads)) {
843
+ if (entry.humanCommentIds.includes(targetId)) return threadId;
844
+ }
845
+ return void 0;
846
+ }
847
+ function resolveSlotForPr(prUrl, sessionsDirOverride) {
848
+ const sessionsDir = join(homedir(), ".muggle-ai", "muggle-do", "sessions");
849
+ if (!existsSync(sessionsDir)) return void 0;
850
+ for (const slug of readdirSync(sessionsDir)) {
851
+ const slotPath = join(sessionsDir, slug);
852
+ try {
853
+ const parsed = JSON.parse(readFileSync(join(slotPath, "prs.json"), "utf-8"));
854
+ const slotPr = Array.isArray(parsed) ? parsed[0] : parsed;
855
+ if (slotPr?.url === prUrl) return slotPath;
856
+ } catch {
857
+ continue;
858
+ }
859
+ }
860
+ return void 0;
861
+ }
862
+ function overdueThreads(slotPath, sessionId2) {
863
+ const overdue = [];
864
+ for (const [threadId, entry] of Object.entries(readLedger(slotPath).threads)) {
865
+ if (entry.lastClaimedBySessionId !== sessionId2) continue;
866
+ if (threadState(entry) === "processed" /* Processed */) continue;
867
+ overdue.push({ threadId, uncovered: uncoveredComments(entry) });
868
+ }
869
+ return overdue;
870
+ }
871
+ function commentReplyGateDecision(state, overdue, maxBlocks = MAX_REPLY_BLOCKS) {
872
+ const blockCount = state.commentReplyBlockCount ?? 0;
873
+ const unanswered = overdue.flatMap((thread) => thread.uncovered);
874
+ if (state.commentReplySkipped === true || unanswered.length === 0) {
875
+ return { action: "none" /* None */, blockCount, unanswered };
876
+ }
877
+ if (blockCount >= maxBlocks) {
878
+ return {
879
+ action: "release" /* Release */,
880
+ blockCount,
881
+ unanswered
882
+ };
883
+ }
884
+ return {
885
+ action: "block" /* Block */,
886
+ blockCount: blockCount + 1,
887
+ unanswered
888
+ };
889
+ }
890
+ function deferredCommentIds(ledger, command) {
891
+ const deferred = [];
892
+ for (const [threadId, entry] of Object.entries(ledger.threads)) {
893
+ const named = uncoveredComments(entry).filter((commentId) => command.includes(commentId));
894
+ if (named.length > 0) deferred.push({ threadId, commentIds: named });
895
+ }
896
+ return deferred;
897
+ }
898
+
536
899
  // src/guardrails/detectBuildIntent.ts
537
900
  var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
538
901
  var DEVCYCLE = /\bresolve\b[^.?!]{0,40}\bconflicts?\b|\bget\b[^.?!]{0,40}\bpr\b[^.?!]{0,40}\b(green|merged?|passing)\b/i;
@@ -567,6 +930,21 @@ function evaluateReportPost(input2, read = defaultFileReader) {
567
930
  };
568
931
  }
569
932
 
933
+ // src/guardrails/reviewThreadResolve.ts
934
+ function detectResolveCall(command) {
935
+ if (!PROVIDER_API_INVOCATION.test(command)) return null;
936
+ if (GITHUB_RESOLVE_THREAD_MUTATION.test(command)) return "github" /* GitHub */;
937
+ if (GITLAB_RESOLVE_DISCUSSION_CALL.test(command)) return "gitlab" /* GitLab */;
938
+ return null;
939
+ }
940
+ 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.";
941
+ function evaluateReviewThreadResolve(input2) {
942
+ if (input2.tool_name !== "Bash") return { deny: false };
943
+ const provider = detectResolveCall(input2.tool_input?.command ?? "");
944
+ if (!provider) return { deny: false };
945
+ return { deny: true, reason: RESOLVE_DENIAL };
946
+ }
947
+
570
948
  // src/guardrails/emit.ts
571
949
  function envelope(eventName, context, host2) {
572
950
  if (!context) return "{}";
@@ -604,6 +982,13 @@ var host = process.env.CURSOR_PLUGIN_ROOT ? "cursor" : "claude";
604
982
  var sub = process.argv[2];
605
983
  var input = readStdin();
606
984
  var sessionId = input.session_id ?? "unknown";
985
+ function releaseGate(field) {
986
+ updateState(
987
+ sessionId,
988
+ (current) => current[field] === true ? current : { ...current, [field]: true }
989
+ );
990
+ return "{}";
991
+ }
607
992
  function prOpened() {
608
993
  const url = detectPrOpened(input);
609
994
  if (!url) return "{}";
@@ -640,6 +1025,7 @@ function offerRan() {
640
1025
  function terminalGate() {
641
1026
  const state = readState(sessionId);
642
1027
  const decision = prTerminalGateDecision(state);
1028
+ if (decision.action === "release" /* Release */) return releaseGate("terminalReleased");
643
1029
  if (decision.action !== "block" /* Block */) return "{}";
644
1030
  state.terminalBlockCount = decision.blockCount;
645
1031
  writeState(state);
@@ -713,6 +1099,7 @@ function classifyGate() {
713
1099
  function stageGate() {
714
1100
  const state = readState(sessionId);
715
1101
  const decision = stageGateDecision(state, unreadMandatoryStages(state));
1102
+ if (decision.action === "release" /* Release */) return releaseGate("stageReleased");
716
1103
  if (decision.action !== "block" /* Block */) return "{}";
717
1104
  state.stageBlockCount = decision.blockCount;
718
1105
  writeState(state);
@@ -723,6 +1110,7 @@ function stageGate() {
723
1110
  function debugPathGate() {
724
1111
  const state = readState(sessionId);
725
1112
  const decision = debugGateDecision(state);
1113
+ if (decision.action === "release" /* Release */) return releaseGate("debugReleased");
726
1114
  if (decision.action !== "block" /* Block */) return "{}";
727
1115
  state.debugBlockCount = decision.blockCount;
728
1116
  writeState(state);
@@ -733,7 +1121,8 @@ function debugPathGate() {
733
1121
  function e2eGate() {
734
1122
  const state = readState(sessionId);
735
1123
  const decision = e2eGateDecision(state);
736
- if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) return "{}";
1124
+ if (decision.action === "release" /* Release */) return releaseGate("e2eReleased");
1125
+ if (decision.action === "none" /* None */) return "{}";
737
1126
  state.e2eBlockCount = decision.blockCount;
738
1127
  writeState(state);
739
1128
  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 +1132,8 @@ function watchGate() {
743
1132
  const state = readState(sessionId);
744
1133
  const untrackedPrUrls = findUntrackedHandledPrs(state.prsHandled);
745
1134
  const decision = watchGateDecision(state, untrackedPrUrls);
746
- if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) {
747
- return "{}";
748
- }
1135
+ if (decision.action === "release" /* Release */) return releaseGate("watchReleased");
1136
+ if (decision.action === "none" /* None */) return "{}";
749
1137
  state.watchBlockCount = decision.blockCount;
750
1138
  writeState(state);
751
1139
  const prList = decision.untracked.join(", ");
@@ -763,6 +1151,7 @@ function walkthroughGate() {
763
1151
  return "{}";
764
1152
  }
765
1153
  const decision = walkthroughGateDecision(state, scan.owed);
1154
+ if (decision.action === "release" /* Release */) return releaseGate("walkthroughReleased");
766
1155
  if (decision.action !== "block" /* Block */) return "{}";
767
1156
  state.walkthroughBlockCount = decision.blockCount;
768
1157
  writeState(state);
@@ -770,11 +1159,73 @@ function walkthroughGate() {
770
1159
  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
1160
  return blockStop(reason, host);
772
1161
  }
1162
+ function slotForSession(state) {
1163
+ for (const prUrl of state.prsHandled) {
1164
+ const slotPath = resolveSlotForPr(prUrl);
1165
+ if (slotPath) return slotPath;
1166
+ }
1167
+ return void 0;
1168
+ }
1169
+ function recordCommentReplies() {
1170
+ const cmd = input.tool_input?.command ?? "";
1171
+ const state = readState(sessionId);
1172
+ const slotPath = slotForSession(state);
1173
+ if (!slotPath) return "{}";
1174
+ if (isReplySkipMarker(cmd)) {
1175
+ const deferred = deferredCommentIds(readLedger(slotPath), cmd);
1176
+ if (deferred.length === 0) {
1177
+ updateState(sessionId, (current) => ({ ...current, commentReplySkipped: true }));
1178
+ return "{}";
1179
+ }
1180
+ for (const { threadId, commentIds } of deferred) {
1181
+ commitThread(slotPath, threadId, (entry) => coverComments(entry, commentIds, null));
1182
+ }
1183
+ return "{}";
1184
+ }
1185
+ for (const thread of detectUnansweredThreads(input)) {
1186
+ commitThread(
1187
+ slotPath,
1188
+ thread.threadId,
1189
+ (entry) => claimThread(
1190
+ refreshHumanComments({ ...entry, provider: thread.provider }, thread.humanCommentIds),
1191
+ sessionId
1192
+ )
1193
+ );
1194
+ }
1195
+ const confirmed = detectConfirmedReplies(input);
1196
+ if (confirmed.length > 0) {
1197
+ const ledger = readLedger(slotPath);
1198
+ for (const reply of confirmed) {
1199
+ const threadId = threadForReplyTarget(ledger, reply.targetId);
1200
+ if (!threadId) continue;
1201
+ const covered = threadId === reply.targetId ? uncoveredComments(ledger.threads[threadId]) : [reply.targetId];
1202
+ commitThread(slotPath, threadId, (entry) => coverComments(entry, covered, reply.replySha));
1203
+ }
1204
+ }
1205
+ return "{}";
1206
+ }
1207
+ function commentReplyGate() {
1208
+ const state = readState(sessionId);
1209
+ const slotPath = slotForSession(state);
1210
+ if (!slotPath) return "{}";
1211
+ const decision = commentReplyGateDecision(state, overdueThreads(slotPath, sessionId));
1212
+ if (decision.action === "release" /* Release */) return releaseGate("commentReplyReleased");
1213
+ if (decision.action !== "block" /* Block */) return "{}";
1214
+ updateState(sessionId, (current) => ({ ...current, commentReplyBlockCount: decision.blockCount }));
1215
+ const commentList = decision.unanswered.join(", ");
1216
+ 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.`;
1217
+ return blockStop(reason, host);
1218
+ }
773
1219
  function reportGate() {
774
1220
  const reportPostVerdict = evaluateReportPost(input);
775
1221
  if (!reportPostVerdict.deny || !reportPostVerdict.reason) return "{}";
776
1222
  return denyTool(reportPostVerdict.reason, host);
777
1223
  }
1224
+ function resolveGate() {
1225
+ const resolveVerdict = evaluateReviewThreadResolve(input);
1226
+ if (!resolveVerdict.deny || !resolveVerdict.reason) return "{}";
1227
+ return denyTool(resolveVerdict.reason, host);
1228
+ }
778
1229
  function buildRouter() {
779
1230
  if (!detectBuildIntent(input.prompt ?? "")) return "{}";
780
1231
  const state = readState(sessionId);
@@ -784,8 +1235,28 @@ function buildRouter() {
784
1235
  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
1236
  return envelope("UserPromptSubmit", ctx, host);
786
1237
  }
1238
+ function capabilityClaimGate() {
1239
+ const state = readState(sessionId);
1240
+ if (state.capabilityClaimNudged) return "{}";
1241
+ const transcriptPath = input.transcript_path;
1242
+ if (!transcriptPath) return "{}";
1243
+ let transcriptTail;
1244
+ try {
1245
+ transcriptTail = readFileSync(transcriptPath, "utf-8").slice(
1246
+ -CAPABILITY_CLAIM_TRANSCRIPT_TAIL_BYTES
1247
+ );
1248
+ } catch {
1249
+ return "{}";
1250
+ }
1251
+ if (!detectFalseCapabilityClaim(lastAssistantText(transcriptTail))) return "{}";
1252
+ state.capabilityClaimNudged = true;
1253
+ writeState(state);
1254
+ 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.`;
1255
+ return blockStop(reason, host);
1256
+ }
787
1257
  var handlers = {
788
1258
  "pr-opened": prOpened,
1259
+ "capability-claim-gate": capabilityClaimGate,
789
1260
  "pr-terminal": prTerminal,
790
1261
  "offer-ran": offerRan,
791
1262
  "record-tests": recordTests,
@@ -793,7 +1264,10 @@ var handlers = {
793
1264
  "terminal-gate": terminalGate,
794
1265
  "watch-gate": watchGate,
795
1266
  "walkthrough-gate": walkthroughGate,
1267
+ "record-comment-replies": recordCommentReplies,
1268
+ "comment-reply-gate": commentReplyGate,
796
1269
  "report-gate": reportGate,
1270
+ "resolve-gate": resolveGate,
797
1271
  "build-router": buildRouter,
798
1272
  "skill-stages": skillStages,
799
1273
  "record-stage-read": recordStageRead,