@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.
- package/config/runtime-targets.json +33 -0
- package/dist/{chunk-MSPMXBO5.js → chunk-FCDHJD6I.js} +441 -347
- package/dist/{chunk-WUMAJOBT.js → chunk-O7LFABOC.js} +39 -35
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/hooks/README.md +4 -1
- package/dist/plugin/hooks/hooks.json +24 -0
- package/dist/plugin/scripts/guardrail-capability-claim.sh +38 -0
- package/dist/plugin/scripts/guardrail-comment-reply-gate.sh +42 -0
- package/dist/plugin/scripts/guardrail-debug-path-gate.sh +1 -1
- package/dist/plugin/scripts/guardrail-e2e-gate.sh +1 -1
- package/dist/plugin/scripts/guardrail-record-comment-replies.sh +28 -0
- package/dist/plugin/scripts/guardrail-resolve-gate.sh +22 -0
- package/dist/plugin/scripts/guardrail-stage-gate.sh +1 -1
- package/dist/plugin/scripts/guardrail-terminal-gate.sh +1 -1
- package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +1 -1
- package/dist/plugin/scripts/guardrail-watch-gate.sh +1 -1
- package/dist/plugin/scripts/guardrails.mjs +506 -32
- package/dist/plugin/scripts/pr-watch-loop.sh +9 -24
- package/dist/plugin/scripts/pr-watch-state.jq +52 -0
- package/dist/plugin/skills/_shared/identity-and-inbox.md +39 -0
- package/dist/plugin/skills/_shared/sync-branch-with-base.md +29 -0
- package/dist/plugin/skills/_shared/telemetry-events/muggle-do-escalation.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/unresolved-threads.md +2 -0
- package/dist/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +3 -1
- package/dist/plugin/skills/_shared/vcs/gitlab.md +0 -1
- package/dist/plugin/skills/do/address-reviews.md +5 -2
- package/dist/plugin/skills/do/build.md +9 -0
- package/dist/plugin/skills/do/e2e-acceptance.md +2 -0
- package/dist/plugin/skills/do/e2e-repair.md +98 -0
- package/dist/plugin/skills/do/open-prs/forward.md +17 -9
- package/dist/plugin/skills/do/open-prs/update.md +9 -2
- package/dist/plugin/skills/do/per-comment-replies.md +13 -1
- package/dist/plugin/skills/do/pre-flight.md +2 -2
- package/dist/plugin/skills/do/resolve-conflicts.md +59 -59
- package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-do/SKILL.md +6 -5
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +1 -1
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoRebase.md +5 -5
- package/dist/plugin/skills/muggle-status/SKILL.md +15 -6
- package/dist/plugin/skills/muggle-test/SKILL.md +1 -0
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +5 -3
- package/dist/plugin/skills/muggle-test-prepare/steps/rebase-check.md +1 -1
- package/dist/release-manifest.json +3 -3
- package/dist/src-TFNPZD5X.js +1 -0
- package/package.json +17 -8
- package/plugin/hooks/README.md +4 -1
- package/plugin/hooks/hooks.json +24 -0
- package/plugin/scripts/guardrail-capability-claim.sh +38 -0
- package/plugin/scripts/guardrail-comment-reply-gate.sh +42 -0
- package/plugin/scripts/guardrail-debug-path-gate.sh +1 -1
- package/plugin/scripts/guardrail-e2e-gate.sh +1 -1
- package/plugin/scripts/guardrail-record-comment-replies.sh +28 -0
- package/plugin/scripts/guardrail-resolve-gate.sh +22 -0
- package/plugin/scripts/guardrail-stage-gate.sh +1 -1
- package/plugin/scripts/guardrail-terminal-gate.sh +1 -1
- package/plugin/scripts/guardrail-walkthrough-gate.sh +1 -1
- package/plugin/scripts/guardrail-watch-gate.sh +1 -1
- package/plugin/scripts/guardrails.mjs +506 -32
- package/plugin/scripts/pr-watch-loop.sh +9 -24
- package/plugin/scripts/pr-watch-state.jq +52 -0
- package/plugin/skills/_shared/identity-and-inbox.md +39 -0
- package/plugin/skills/_shared/sync-branch-with-base.md +29 -0
- package/plugin/skills/_shared/telemetry-events/muggle-do-escalation.md +3 -2
- package/plugin/skills/_shared/vcs/github/unresolved-threads.md +2 -0
- package/plugin/skills/_shared/vcs/gitlab/unresolved-discussions.md +3 -1
- package/plugin/skills/_shared/vcs/gitlab.md +0 -1
- package/plugin/skills/do/address-reviews.md +5 -2
- package/plugin/skills/do/build.md +9 -0
- package/plugin/skills/do/e2e-acceptance.md +2 -0
- package/plugin/skills/do/e2e-repair.md +98 -0
- package/plugin/skills/do/open-prs/forward.md +17 -9
- package/plugin/skills/do/open-prs/update.md +9 -2
- package/plugin/skills/do/per-comment-replies.md +13 -1
- package/plugin/skills/do/pre-flight.md +2 -2
- package/plugin/skills/do/resolve-conflicts.md +59 -59
- package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/plugin/skills/muggle-do/SKILL.md +6 -5
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +1 -1
- package/plugin/skills/muggle-preferences/preference-gates/autoRebase.md +5 -5
- package/plugin/skills/muggle-status/SKILL.md +15 -6
- package/plugin/skills/muggle-test/SKILL.md +1 -0
- package/plugin/skills/muggle-test-feature-local/SKILL.md +5 -3
- package/plugin/skills/muggle-test-prepare/steps/rebase-check.md +1 -1
- package/scripts/postinstall.mjs +59 -2
- package/dist/plugin/skills/_shared/rebase-before-e2e.md +0 -21
- package/dist/plugin/skills/_shared/vcs/gitlab/resolve-discussion.md +0 -10
- package/dist/src-B5OTAZKM.js +0 -1
- package/plugin/skills/_shared/rebase-before-e2e.md +0 -21
- 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
|
-
|
|
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 === "
|
|
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 === "
|
|
747
|
-
|
|
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,
|