@muggleai/works 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/{chunk-LVUJ4E62.js → chunk-MSPMXBO5.js} +638 -468
- package/dist/{chunk-5PYK7DWB.js → chunk-WUMAJOBT.js} +17 -6
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/README.md +19 -13
- package/dist/plugin/agents/test-prepare-runner.md +4 -1
- package/dist/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/dist/plugin/commands/mtest.md +1 -1
- package/dist/plugin/hooks/README.md +15 -3
- package/dist/plugin/hooks/hooks.json +79 -0
- package/dist/plugin/scripts/ensure-electron-app.sh +62 -47
- package/dist/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/dist/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/dist/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/dist/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/dist/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/dist/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/dist/plugin/scripts/guardrail-report-format.sh +9 -5
- package/dist/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/dist/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/dist/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/dist/plugin/scripts/guardrails.mjs +474 -80
- package/dist/plugin/scripts/pr-watch-events.sh +116 -0
- package/dist/plugin/scripts/pr-watch-guards.sh +8 -2
- package/dist/plugin/scripts/pr-watch-loop.sh +254 -0
- package/dist/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/dist/plugin/scripts/sign-body.sh +88 -0
- package/dist/plugin/skills/CLAUDE.md +16 -0
- package/dist/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/dist/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/dist/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/dist/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
- package/dist/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/dist/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/dist/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/dist/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/dist/plugin/skills/do/open-prs/forward.md +2 -2
- package/dist/plugin/skills/do/per-comment-replies.md +2 -15
- package/dist/plugin/skills/muggle/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/dist/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/dist/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/dist/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/dist/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/dist/plugin/skills/muggle-preferences/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-preferences/evals/evals.json +45 -0
- package/dist/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
- package/dist/plugin/skills/muggle-preferences/ops/configure.md +4 -5
- package/dist/plugin/skills/muggle-preferences/ops/list.md +1 -1
- package/dist/plugin/skills/muggle-preferences/ops/set.md +3 -4
- package/dist/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
- package/dist/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
- package/dist/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/dist/plugin/skills/muggle-test/SKILL.md +23 -3
- package/dist/plugin/skills/muggle-test/execute-local.md +2 -0
- package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +6 -1
- package/dist/plugin/skills/muggle-test-import/SKILL.md +1 -1
- package/dist/plugin/skills/muggle-test-prepare/SKILL.md +28 -15
- package/dist/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +111 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/dist/plugin/skills/muggle-test-prepare/steps/readiness-report.md +12 -5
- package/dist/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/dist/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +9 -6
- package/dist/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/dist/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/dist/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
- package/dist/release-manifest.json +4 -4
- package/dist/src-B5OTAZKM.js +1 -0
- package/package.json +7 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/README.md +19 -13
- package/plugin/agents/test-prepare-runner.md +4 -1
- package/plugin/agents/visual-walkthrough-builder.md +12 -5
- package/plugin/commands/mtest.md +1 -1
- package/plugin/hooks/README.md +15 -3
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/scripts/ensure-electron-app.sh +62 -47
- package/plugin/scripts/guardrail-classify-gate.sh +23 -0
- package/plugin/scripts/guardrail-debug-path-gate.sh +41 -0
- package/plugin/scripts/guardrail-pr-terminal.sh +6 -2
- package/plugin/scripts/guardrail-record-stage-read.sh +21 -0
- package/plugin/scripts/guardrail-record-stage-signals.sh +23 -0
- package/plugin/scripts/guardrail-record-tests.sh +11 -4
- package/plugin/scripts/guardrail-report-format.sh +9 -5
- package/plugin/scripts/guardrail-skill-stages.sh +35 -0
- package/plugin/scripts/guardrail-stage-gate.sh +43 -0
- package/plugin/scripts/guardrail-walkthrough-gate.sh +41 -0
- package/plugin/scripts/guardrails.mjs +474 -80
- package/plugin/scripts/pr-watch-events.sh +116 -0
- package/plugin/scripts/pr-watch-guards.sh +8 -2
- package/plugin/scripts/pr-watch-loop.sh +254 -0
- package/plugin/scripts/reconcile-stale-watchers.sh +66 -18
- package/plugin/scripts/sign-body.sh +88 -0
- package/plugin/skills/CLAUDE.md +16 -0
- package/plugin/skills/_shared/post-merge-cleanup.md +83 -3
- package/plugin/skills/_shared/pr-followup-helpers/loop-signature.md +6 -8
- package/plugin/skills/_shared/pr-followup-helpers/reply-routing.md +10 -18
- package/plugin/skills/_shared/resolve-e2e-validation-context.md +1 -0
- package/plugin/skills/_shared/vcs/CLAUDE.md +1 -0
- package/plugin/skills/_shared/vcs/github/pr-edit.md +3 -2
- package/plugin/skills/_shared/vcs/github/reply-line-comment.md +3 -2
- package/plugin/skills/_shared/vcs/github/top-level-comment.md +3 -2
- package/plugin/skills/_shared/vcs/gitlab/mr-create.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-edit.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/mr-note.md +4 -1
- package/plugin/skills/_shared/vcs/gitlab/reply-discussion.md +4 -1
- package/plugin/skills/_shared/vcs/post-signature.md +23 -12
- package/plugin/skills/do/open-prs/forward.md +2 -2
- package/plugin/skills/do/per-comment-replies.md +2 -15
- package/plugin/skills/muggle/SKILL.md +1 -1
- package/plugin/skills/muggle-browser-task/SKILL.md +1 -1
- package/plugin/skills/muggle-pr-followup/CLAUDE.md +2 -1
- package/plugin/skills/muggle-pr-followup/SKILL.md +6 -3
- package/plugin/skills/muggle-pr-followup/adopt.md +60 -0
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +10 -14
- package/plugin/skills/muggle-pr-followup/auto-track.md +2 -0
- package/plugin/skills/muggle-pr-followup/evals/evals.json +63 -1
- package/plugin/skills/muggle-pr-followup/output-templates/help.md +11 -1
- package/plugin/skills/muggle-pr-followup/output-templates/inline-reply.md +1 -7
- package/plugin/skills/muggle-pr-followup/output-templates/resolve-reminder.md +1 -4
- package/plugin/skills/muggle-pr-followup/reconcile.md +32 -6
- package/plugin/skills/muggle-pr-followup/state-schemas.md +18 -0
- package/plugin/skills/muggle-preferences/SKILL.md +1 -1
- package/plugin/skills/muggle-preferences/evals/evals.json +45 -0
- package/plugin/skills/muggle-preferences/ops/change-one.md +1 -1
- package/plugin/skills/muggle-preferences/ops/configure.md +4 -5
- package/plugin/skills/muggle-preferences/ops/list.md +1 -1
- package/plugin/skills/muggle-preferences/ops/set.md +3 -4
- package/plugin/skills/muggle-preferences/preference-gates/README.md +3 -3
- package/plugin/skills/muggle-preferences/preference-gates/autoSelectLocalHost.md +2 -2
- package/plugin/skills/muggle-preferences/preference-gates/autoSelectProject.md +1 -1
- package/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +23 -0
- package/plugin/skills/muggle-test/SKILL.md +23 -3
- package/plugin/skills/muggle-test/execute-local.md +2 -0
- package/plugin/skills/muggle-test-feature-local/SKILL.md +6 -1
- package/plugin/skills/muggle-test-import/SKILL.md +1 -1
- package/plugin/skills/muggle-test-prepare/SKILL.md +28 -15
- package/plugin/skills/muggle-test-prepare/steps/check-running.md +1 -1
- package/plugin/skills/muggle-test-prepare/steps/confirm-recipe.md +56 -0
- package/plugin/skills/muggle-test-prepare/steps/derive-service-graph.md +51 -0
- package/plugin/skills/muggle-test-prepare/steps/e2e-instructions.md +111 -0
- package/plugin/skills/muggle-test-prepare/steps/identify-services.md +4 -2
- package/plugin/skills/muggle-test-prepare/steps/readiness-report.md +12 -5
- package/plugin/skills/muggle-test-prepare/steps/record-resolution.md +32 -0
- package/plugin/skills/muggle-test-prepare/steps/replay-or-learn.md +20 -0
- package/plugin/skills/muggle-test-prepare/steps/reuse-plan.md +9 -6
- package/plugin/skills/muggle-test-prepare/steps/smoke-test.md +5 -1
- package/plugin/skills/muggle-test-prepare/steps/start-services.md +3 -1
- package/plugin/skills/muggle-test-regenerate-missing/SKILL.md +1 -1
- package/dist/src-FDK2KQSZ.js +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync
|
|
2
|
-
import { isAbsolute, resolve,
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, isAbsolute, resolve, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
4
6
|
|
|
5
7
|
// src/guardrails/cli.ts
|
|
6
8
|
var baseDir = (override) => override ?? join(homedir(), ".muggle-ai", "guardrails");
|
|
@@ -43,11 +45,27 @@ ${input2.tool_response?.output ?? ""}`;
|
|
|
43
45
|
// src/guardrails/constants.ts
|
|
44
46
|
var GH_PR_MERGED_LINE = /\b(?:Merged|Squashed and merged|Rebased and merged) pull request [\w./-]*#(\d+)/;
|
|
45
47
|
var GH_PR_CLOSED_LINE = /\bClosed pull request [\w./-]*#(\d+)/;
|
|
48
|
+
var GH_PR_REOPENED_LINE = /\bReopened pull request [\w./-]*#(\d+)/;
|
|
46
49
|
var PR_MONITOR_TERMINAL_LINE = /\bTERMINAL pr=(\d+): (MERGED|CLOSED)\b/;
|
|
47
50
|
var MAX_PR_TERMINAL_BLOCKS = 3;
|
|
48
51
|
var MAX_WATCH_BLOCKS = 3;
|
|
52
|
+
var MAX_WALKTHROUGH_BLOCKS = 3;
|
|
53
|
+
var GH_LOOKUP_TIMEOUT_MS = 1e4;
|
|
49
54
|
var MUGGLE_SKILL_EMIT_TOOL = /muggle-local-telemetry-skill-emit/i;
|
|
50
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";
|
|
51
69
|
|
|
52
70
|
// src/guardrails/prTerminal.ts
|
|
53
71
|
function detectPrTerminal(input2) {
|
|
@@ -71,6 +89,23 @@ function detectPrTerminal(input2) {
|
|
|
71
89
|
}
|
|
72
90
|
return null;
|
|
73
91
|
}
|
|
92
|
+
function detectPrReopened(input2) {
|
|
93
|
+
if (input2.tool_name !== "Bash") return null;
|
|
94
|
+
const response = input2.tool_response;
|
|
95
|
+
const haystack = [response?.stdout, response?.stderr, response?.output, response?.content].filter((part) => typeof part === "string").join("\n");
|
|
96
|
+
const reopenedMatch = haystack.match(GH_PR_REOPENED_LINE);
|
|
97
|
+
return reopenedMatch ? Number(reopenedMatch[1]) : null;
|
|
98
|
+
}
|
|
99
|
+
function applyPrReopened(state, prNumber) {
|
|
100
|
+
const pending = state.terminalPending ?? [];
|
|
101
|
+
const handled = state.terminalHandled ?? [];
|
|
102
|
+
if (!pending.includes(prNumber) && !handled.includes(prNumber)) return state;
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
terminalPending: pending.filter((number) => number !== prNumber),
|
|
106
|
+
terminalHandled: handled.filter((number) => number !== prNumber)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
74
109
|
function applyPrTerminalDetected(state, prNumber) {
|
|
75
110
|
const pending = state.terminalPending ?? [];
|
|
76
111
|
const handled = state.terminalHandled ?? [];
|
|
@@ -97,6 +132,207 @@ function prTerminalGateDecision(state, maxBlocks = MAX_PR_TERMINAL_BLOCKS) {
|
|
|
97
132
|
}
|
|
98
133
|
return { action: "block" /* Block */, blockCount: blockCount + 1 };
|
|
99
134
|
}
|
|
135
|
+
var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
136
|
+
var SEQUENCE_ENTRY = /^\s*-\s*(.+?)\s*$/;
|
|
137
|
+
var FLOW_LIST = /^\s*\[(.*)\]\s*$/;
|
|
138
|
+
var STAGE_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_STAGE_SKIP\b/;
|
|
139
|
+
var SKILLS_DIR_NAME = "skills";
|
|
140
|
+
var SKILLS_PATH_SEGMENT = `/${SKILLS_DIR_NAME}/`;
|
|
141
|
+
var SINGLE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
142
|
+
var unquote = (raw) => raw.trim().replace(/^["']|["']$/g, "").trim();
|
|
143
|
+
function parseDeclaredStages(skillMarkdown) {
|
|
144
|
+
const frontmatter = FRONTMATTER.exec(skillMarkdown)?.[1];
|
|
145
|
+
if (!frontmatter) return [];
|
|
146
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
147
|
+
const keyIndex = lines.findIndex(
|
|
148
|
+
(line) => line.startsWith(`${MANDATORY_STAGES_FRONTMATTER_KEY}:`)
|
|
149
|
+
);
|
|
150
|
+
if (keyIndex < 0) return [];
|
|
151
|
+
const inlineValue = lines[keyIndex].slice(MANDATORY_STAGES_FRONTMATTER_KEY.length + 1).trim();
|
|
152
|
+
const flowEntries = FLOW_LIST.exec(inlineValue)?.[1];
|
|
153
|
+
if (flowEntries !== void 0) {
|
|
154
|
+
return flowEntries.split(",").map(unquote).filter((entry) => entry.length > 0);
|
|
155
|
+
}
|
|
156
|
+
const declared = [];
|
|
157
|
+
for (const line of lines.slice(keyIndex + 1)) {
|
|
158
|
+
const entry = SEQUENCE_ENTRY.exec(line);
|
|
159
|
+
if (!entry) break;
|
|
160
|
+
declared.push(unquote(entry[1]));
|
|
161
|
+
}
|
|
162
|
+
return declared;
|
|
163
|
+
}
|
|
164
|
+
function normalizeStagePath(filePath) {
|
|
165
|
+
return filePath.replaceAll("\\", "/").toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
function stageLabel(normalizedStagePath) {
|
|
168
|
+
const insideSkills = normalizedStagePath.lastIndexOf(SKILLS_PATH_SEGMENT);
|
|
169
|
+
if (insideSkills < 0) return normalizedStagePath;
|
|
170
|
+
return normalizedStagePath.slice(insideSkills + SKILLS_PATH_SEGMENT.length);
|
|
171
|
+
}
|
|
172
|
+
function resolvePluginSkillsRoot() {
|
|
173
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || process.env.CURSOR_PLUGIN_ROOT;
|
|
174
|
+
if (pluginRoot) return join(pluginRoot, SKILLS_DIR_NAME);
|
|
175
|
+
return join(dirname(dirname(fileURLToPath(import.meta.url))), SKILLS_DIR_NAME);
|
|
176
|
+
}
|
|
177
|
+
function resolveSkillNameFromToolInput(toolInput) {
|
|
178
|
+
if (!toolInput) return void 0;
|
|
179
|
+
const indexed = toolInput;
|
|
180
|
+
for (const inputKey of SKILL_NAME_INPUT_KEYS) {
|
|
181
|
+
const value = indexed[inputKey];
|
|
182
|
+
if (typeof value !== "string" || value.trim().length === 0) continue;
|
|
183
|
+
const skillName = value.trim().split(":").pop()?.replace(/^\//, "") ?? "";
|
|
184
|
+
return SINGLE_PATH_SEGMENT.test(skillName) ? skillName : void 0;
|
|
185
|
+
}
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
function resolveSkillStagePaths(skillName, skillsRootDir) {
|
|
189
|
+
const skillDir = join(skillsRootDir, skillName);
|
|
190
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
191
|
+
if (!existsSync(skillFile)) return [];
|
|
192
|
+
const normalizedRoot = normalizeStagePath(skillsRootDir);
|
|
193
|
+
const staysInsideSkillsTree = (candidate) => normalizeStagePath(candidate).startsWith(`${normalizedRoot}/`);
|
|
194
|
+
const resolved = [];
|
|
195
|
+
for (const declared of parseDeclaredStages(readFileSync(skillFile, "utf-8"))) {
|
|
196
|
+
if (isAbsolute(declared)) continue;
|
|
197
|
+
const stagePath = [resolve(skillDir, declared), resolve(skillsRootDir, declared)].find(
|
|
198
|
+
(candidate) => staysInsideSkillsTree(candidate) && existsSync(candidate)
|
|
199
|
+
);
|
|
200
|
+
if (!stagePath) continue;
|
|
201
|
+
const normalized = normalizeStagePath(stagePath);
|
|
202
|
+
if (!resolved.includes(normalized)) resolved.push(normalized);
|
|
203
|
+
}
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
function applySkillInvocation(state, skillName, stagePaths) {
|
|
207
|
+
const merged = [...state.mandatoryStages ?? []];
|
|
208
|
+
for (const stagePath of stagePaths) if (!merged.includes(stagePath)) merged.push(stagePath);
|
|
209
|
+
if (state.lastInvokedSkillName === skillName && merged.length === (state.mandatoryStages?.length ?? 0)) {
|
|
210
|
+
return state;
|
|
211
|
+
}
|
|
212
|
+
return { ...state, lastInvokedSkillName: skillName, mandatoryStages: merged };
|
|
213
|
+
}
|
|
214
|
+
function applyStageRead(state, filePath) {
|
|
215
|
+
const stagePath = normalizeStagePath(filePath);
|
|
216
|
+
if ((state.stagesRead ?? []).includes(stagePath)) return state;
|
|
217
|
+
return { ...state, stagesRead: [...state.stagesRead ?? [], stagePath] };
|
|
218
|
+
}
|
|
219
|
+
function unreadMandatoryStages(state) {
|
|
220
|
+
const read = new Set(state.stagesRead ?? []);
|
|
221
|
+
return (state.mandatoryStages ?? []).filter((stagePath) => !read.has(stagePath));
|
|
222
|
+
}
|
|
223
|
+
function isStageSkipMarker(command) {
|
|
224
|
+
return STAGE_SKIP_MARKER.test(command);
|
|
225
|
+
}
|
|
226
|
+
function applyStageSkip(state, skipped) {
|
|
227
|
+
if (!skipped || state.stageSkipped === true) return state;
|
|
228
|
+
return { ...state, stageSkipped: true };
|
|
229
|
+
}
|
|
230
|
+
function stageGateDecision(state, unreadStagePaths, maxBlocks = MAX_STAGE_BLOCKS) {
|
|
231
|
+
const blockCount = state.stageBlockCount ?? 0;
|
|
232
|
+
if (state.stageSkipped === true || unreadStagePaths.length === 0) {
|
|
233
|
+
return { action: "none" /* None */, blockCount, unread: unreadStagePaths };
|
|
234
|
+
}
|
|
235
|
+
if (blockCount >= maxBlocks) {
|
|
236
|
+
return { action: "release" /* Release */, blockCount, unread: unreadStagePaths };
|
|
237
|
+
}
|
|
238
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1, unread: unreadStagePaths };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/guardrails/preExecutionClassification.ts
|
|
242
|
+
var CLASSIFICATION_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_CLASSIFY_SKIP\b/;
|
|
243
|
+
function detectClassifiedTestCaseId(input2) {
|
|
244
|
+
if (!MUGGLE_EVENT_EMIT_TOOL.test(input2.tool_name ?? "")) return void 0;
|
|
245
|
+
if (input2.tool_input?.eventType !== PRE_EXECUTION_CLASSIFICATION_EVENT) return void 0;
|
|
246
|
+
return input2.tool_input?.testCaseId ?? ANY_TEST_CASE;
|
|
247
|
+
}
|
|
248
|
+
function applyClassifiedTestCase(state, testCaseId) {
|
|
249
|
+
if ((state.classifiedTestCaseIds ?? []).includes(testCaseId)) return state;
|
|
250
|
+
return { ...state, classifiedTestCaseIds: [...state.classifiedTestCaseIds ?? [], testCaseId] };
|
|
251
|
+
}
|
|
252
|
+
function resolveExecutionTargetTestCaseId(input2) {
|
|
253
|
+
if (!MUGGLE_EXECUTION_TOOL.test(input2.tool_name ?? "")) return void 0;
|
|
254
|
+
return input2.tool_input?.testCase?.id ?? input2.tool_input?.testScript?.testCaseId;
|
|
255
|
+
}
|
|
256
|
+
function isClassificationSkipMarker(command) {
|
|
257
|
+
return CLASSIFICATION_SKIP_MARKER.test(command);
|
|
258
|
+
}
|
|
259
|
+
function applyClassificationSkip(state, skipped) {
|
|
260
|
+
if (!skipped || state.classificationSkipped === true) return state;
|
|
261
|
+
return { ...state, classificationSkipped: true };
|
|
262
|
+
}
|
|
263
|
+
function classificationGateDecision(state, input2) {
|
|
264
|
+
if (state.lastInvokedSkillName !== MUGGLE_TEST_SKILL_NAME) return { deny: false };
|
|
265
|
+
if (state.classificationSkipped === true) return { deny: false };
|
|
266
|
+
const testCaseId = resolveExecutionTargetTestCaseId(input2);
|
|
267
|
+
if (!testCaseId) return { deny: false };
|
|
268
|
+
const classified = state.classifiedTestCaseIds ?? [];
|
|
269
|
+
if (classified.includes(ANY_TEST_CASE) || classified.includes(testCaseId)) return { deny: false };
|
|
270
|
+
return { deny: true, testCaseId };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/guardrails/debugPath.ts
|
|
274
|
+
var DEBUG_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_DEBUG_SKIP\b/;
|
|
275
|
+
var serialize = (input2) => `${JSON.stringify(input2.tool_input ?? {})}
|
|
276
|
+
${JSON.stringify(input2.tool_response ?? {})}`;
|
|
277
|
+
function renderedResult(toolResponse) {
|
|
278
|
+
const rendered = [];
|
|
279
|
+
const collect = (value) => {
|
|
280
|
+
if (typeof value === "string") rendered.push(value);
|
|
281
|
+
else if (Array.isArray(value)) value.forEach(collect);
|
|
282
|
+
else if (value && typeof value === "object") Object.values(value).forEach(collect);
|
|
283
|
+
};
|
|
284
|
+
collect(toolResponse);
|
|
285
|
+
return rendered.join("\n");
|
|
286
|
+
}
|
|
287
|
+
function detectFailedRunId(input2) {
|
|
288
|
+
if (!MUGGLE_EXECUTION_TOOL.test(input2.tool_name ?? "")) return void 0;
|
|
289
|
+
const rendered = renderedResult(input2.tool_response);
|
|
290
|
+
const status = MUGGLE_RUN_STATUS_LINE.exec(rendered)?.[1];
|
|
291
|
+
if (!status || status.toLowerCase() === MUGGLE_RUN_PASSED_STATUS) return void 0;
|
|
292
|
+
return MUGGLE_RUN_ID_LINE.exec(rendered)?.[1];
|
|
293
|
+
}
|
|
294
|
+
function applyFailedRun(state, runId) {
|
|
295
|
+
if ((state.failedRuns ?? []).includes(runId)) return state;
|
|
296
|
+
return { ...state, failedRuns: [...state.failedRuns ?? [], runId] };
|
|
297
|
+
}
|
|
298
|
+
function detectDebugEvidenceRunIds(input2, owedRunIds) {
|
|
299
|
+
const toolName = input2.tool_name ?? "";
|
|
300
|
+
const isDiagnosisEmit = MUGGLE_EVENT_EMIT_TOOL.test(toolName) && FAILURE_DIAGNOSIS_EVENT.test(input2.tool_input?.eventType ?? "");
|
|
301
|
+
if (!isDiagnosisEmit && !MUGGLE_FEEDBACK_CREATE_TOOL.test(toolName)) return [];
|
|
302
|
+
const payload = serialize(input2);
|
|
303
|
+
return owedRunIds.filter((runId) => payload.includes(runId));
|
|
304
|
+
}
|
|
305
|
+
function applyDebugEvidence(state, runIds) {
|
|
306
|
+
const debugged = [...state.debuggedRuns ?? []];
|
|
307
|
+
for (const runId of runIds) if (!debugged.includes(runId)) debugged.push(runId);
|
|
308
|
+
if (debugged.length === (state.debuggedRuns ?? []).length) return state;
|
|
309
|
+
return { ...state, debuggedRuns: debugged };
|
|
310
|
+
}
|
|
311
|
+
function isDebugSkipMarker(command) {
|
|
312
|
+
return DEBUG_SKIP_MARKER.test(command);
|
|
313
|
+
}
|
|
314
|
+
function applyDebugSkip(state, command) {
|
|
315
|
+
if (!isDebugSkipMarker(command)) return state;
|
|
316
|
+
const namedRuns = (state.failedRuns ?? []).filter((runId) => command.includes(runId));
|
|
317
|
+
if (namedRuns.length > 0) return applyDebugEvidence(state, namedRuns);
|
|
318
|
+
if (state.debugSkipped === true) return state;
|
|
319
|
+
return { ...state, debugSkipped: true };
|
|
320
|
+
}
|
|
321
|
+
function undebuggedFailedRuns(state) {
|
|
322
|
+
const debugged = new Set(state.debuggedRuns ?? []);
|
|
323
|
+
return (state.failedRuns ?? []).filter((runId) => !debugged.has(runId));
|
|
324
|
+
}
|
|
325
|
+
function debugGateDecision(state, maxBlocks = MAX_DEBUG_BLOCKS) {
|
|
326
|
+
const blockCount = state.debugBlockCount ?? 0;
|
|
327
|
+
const undebugged = undebuggedFailedRuns(state);
|
|
328
|
+
if (state.debugSkipped === true || undebugged.length === 0) {
|
|
329
|
+
return { action: "none" /* None */, blockCount, undebugged };
|
|
330
|
+
}
|
|
331
|
+
if (blockCount >= maxBlocks) {
|
|
332
|
+
return { action: "release" /* Release */, blockCount, undebugged };
|
|
333
|
+
}
|
|
334
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1, undebugged };
|
|
335
|
+
}
|
|
100
336
|
|
|
101
337
|
// src/guardrails/testsGreen.ts
|
|
102
338
|
var TEST_CMD = /\b(pnpm|npm|yarn)\s+(run\s+)?test\b|\b(jest|vitest|pytest)\b|\bgo\s+test\b|\bcargo\s+test\b/;
|
|
@@ -144,7 +380,6 @@ function e2eGateDecision(state, maxBlocks = MAX_E2E_BLOCKS) {
|
|
|
144
380
|
if (blockCount >= maxBlocks) return { action: "release" /* Release */, blockCount };
|
|
145
381
|
return { action: "block" /* Block */, blockCount: blockCount + 1 };
|
|
146
382
|
}
|
|
147
|
-
var HEARTBEAT_FRESH_MS = 15 * 60 * 1e3;
|
|
148
383
|
var WATCH_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_WATCH_SKIP\b/;
|
|
149
384
|
function isWatchSkipMarker(cmd) {
|
|
150
385
|
return WATCH_SKIP_MARKER.test(cmd);
|
|
@@ -153,38 +388,13 @@ function applyWatchSkip(state, skipped) {
|
|
|
153
388
|
if (!skipped || state.watchSkipped === true) return state;
|
|
154
389
|
return { ...state, watchSkipped: true };
|
|
155
390
|
}
|
|
156
|
-
function
|
|
157
|
-
if (existsSync(join(slotDir, "result.md"))) return true;
|
|
158
|
-
const pidFile = join(slotDir, "watch.pid");
|
|
159
|
-
if (existsSync(pidFile)) {
|
|
160
|
-
const pid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
161
|
-
if (Number.isInteger(pid) && pid > 0) {
|
|
162
|
-
try {
|
|
163
|
-
process.kill(pid, 0);
|
|
164
|
-
return true;
|
|
165
|
-
} catch (err) {
|
|
166
|
-
if (err.code === "EPERM") return true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const beat = join(slotDir, "watch-heartbeat");
|
|
171
|
-
if (existsSync(beat)) {
|
|
172
|
-
try {
|
|
173
|
-
if (Date.now() - statSync(beat).mtimeMs < HEARTBEAT_FRESH_MS) return true;
|
|
174
|
-
} catch {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
function findUnarmedHandledPrs(handledUrls, sessionsDirOverride) {
|
|
391
|
+
function findUntrackedHandledPrs(handledUrls, sessionsDirOverride) {
|
|
181
392
|
if (handledUrls.length === 0) return [];
|
|
182
393
|
const sessionsDir = join(homedir(), ".muggle-ai", "muggle-do", "sessions");
|
|
183
394
|
if (!existsSync(sessionsDir)) return [...handledUrls];
|
|
184
|
-
const
|
|
395
|
+
const trackedUrls = /* @__PURE__ */ new Set();
|
|
185
396
|
for (const slug of readdirSync(sessionsDir)) {
|
|
186
|
-
const
|
|
187
|
-
const prsFile = join(slotDir, "prs.json");
|
|
397
|
+
const prsFile = join(sessionsDir, slug, "prs.json");
|
|
188
398
|
if (!existsSync(prsFile)) continue;
|
|
189
399
|
let slotUrl;
|
|
190
400
|
try {
|
|
@@ -194,36 +404,30 @@ function findUnarmedHandledPrs(handledUrls, sessionsDirOverride) {
|
|
|
194
404
|
} catch {
|
|
195
405
|
continue;
|
|
196
406
|
}
|
|
197
|
-
if (slotUrl
|
|
198
|
-
watchedUrls.add(slotUrl);
|
|
199
|
-
}
|
|
407
|
+
if (slotUrl) trackedUrls.add(slotUrl);
|
|
200
408
|
}
|
|
201
|
-
return handledUrls.filter((url) => !
|
|
409
|
+
return handledUrls.filter((url) => !trackedUrls.has(url));
|
|
202
410
|
}
|
|
203
|
-
function watchGateDecision(state,
|
|
411
|
+
function watchGateDecision(state, untrackedPrUrls, maxBlocks = MAX_WATCH_BLOCKS) {
|
|
204
412
|
const blockCount = state.watchBlockCount ?? 0;
|
|
205
|
-
if (state.watchSkipped === true ||
|
|
206
|
-
return { action: "none" /* None */, blockCount,
|
|
413
|
+
if (state.watchSkipped === true || untrackedPrUrls.length === 0) {
|
|
414
|
+
return { action: "none" /* None */, blockCount, untracked: untrackedPrUrls };
|
|
207
415
|
}
|
|
208
416
|
if (blockCount >= maxBlocks) {
|
|
209
|
-
return { action: "release" /* Release */, blockCount,
|
|
417
|
+
return { action: "release" /* Release */, blockCount, untracked: untrackedPrUrls };
|
|
210
418
|
}
|
|
211
|
-
return {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
var DEVCYCLE = /\bresolve\b[^.?!]{0,40}\bconflicts?\b|\bget\b[^.?!]{0,40}\bpr\b[^.?!]{0,40}\b(green|merged?|passing)\b/i;
|
|
217
|
-
var QUESTION = /^\s*(why|what|how|when|where|who|is|are|does|do|can you (explain|tell)|explain)\b/i;
|
|
218
|
-
function detectBuildIntent(prompt) {
|
|
219
|
-
const p = (prompt ?? "").trim();
|
|
220
|
-
if (!p || p.startsWith("/")) return false;
|
|
221
|
-
if (QUESTION.test(p)) return false;
|
|
222
|
-
return BUILD.test(p) || DEVCYCLE.test(p);
|
|
419
|
+
return {
|
|
420
|
+
action: "block" /* Block */,
|
|
421
|
+
blockCount: blockCount + 1,
|
|
422
|
+
untracked: untrackedPrUrls
|
|
423
|
+
};
|
|
223
424
|
}
|
|
224
425
|
var REPORT_SENTINEL = "muggle-pr-section";
|
|
225
|
-
var
|
|
226
|
-
var
|
|
426
|
+
var PR_PROSE_CMD = /\bgh\s+pr\s+(comment|create|edit)\b/;
|
|
427
|
+
var GH_API_CMD = /\bgh\s+api\b/;
|
|
428
|
+
var ISSUE_COMMENT_PATH = /\bissues\/comments\/\d+/;
|
|
429
|
+
var PATCH_METHOD = /(?:--method|-X)[=\s]+PATCH\b/;
|
|
430
|
+
var defaultFileReader = (path, cwd) => {
|
|
227
431
|
try {
|
|
228
432
|
const abs = isAbsolute(path) ? path : resolve(cwd ?? process.cwd(), path);
|
|
229
433
|
if (!existsSync(abs)) return null;
|
|
@@ -232,13 +436,115 @@ var defaultReader = (path, cwd) => {
|
|
|
232
436
|
return null;
|
|
233
437
|
}
|
|
234
438
|
};
|
|
235
|
-
function
|
|
439
|
+
function unquote2(s) {
|
|
236
440
|
const t = s.trim();
|
|
237
441
|
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
238
442
|
return t.slice(1, -1);
|
|
239
443
|
}
|
|
240
444
|
return t;
|
|
241
445
|
}
|
|
446
|
+
function isCommentEditCommand(cmd) {
|
|
447
|
+
return GH_API_CMD.test(cmd) && ISSUE_COMMENT_PATH.test(cmd) && PATCH_METHOD.test(cmd);
|
|
448
|
+
}
|
|
449
|
+
function isPrReportPostCommand(cmd) {
|
|
450
|
+
return PR_PROSE_CMD.test(cmd) || isCommentEditCommand(cmd);
|
|
451
|
+
}
|
|
452
|
+
function collectPrPostText(cmd, cwd, read) {
|
|
453
|
+
let text = cmd;
|
|
454
|
+
for (const match of cmd.matchAll(/(?:--body-file|--input)[=\s]+("[^"]+"|'[^']+'|\S+)/g)) {
|
|
455
|
+
const path = unquote2(match[1]);
|
|
456
|
+
if (path && path !== "-") {
|
|
457
|
+
const contents = read(path, cwd);
|
|
458
|
+
if (contents) text += "\n" + contents;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
for (const match of cmd.matchAll(/jq\b[^|]*?("[^"]+\.json"|'[^']+\.json'|\S+\.json)/g)) {
|
|
462
|
+
const contents = read(unquote2(match[1]), cwd);
|
|
463
|
+
if (contents) text += "\n" + contents;
|
|
464
|
+
}
|
|
465
|
+
return text;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/guardrails/walkthroughPosted.ts
|
|
469
|
+
var WALKTHROUGH_SKIP_MARKER = /^\s*echo\s+["']?MUGGLE_WALKTHROUGH_SKIP\b/;
|
|
470
|
+
function isWalkthroughSkipMarker(cmd) {
|
|
471
|
+
return WALKTHROUGH_SKIP_MARKER.test(cmd);
|
|
472
|
+
}
|
|
473
|
+
function detectWalkthroughPost(input2, read = defaultFileReader) {
|
|
474
|
+
if (input2.tool_name !== "Bash") return false;
|
|
475
|
+
const cmd = input2.tool_input?.command ?? "";
|
|
476
|
+
if (!isPrReportPostCommand(cmd)) return false;
|
|
477
|
+
return collectPrPostText(cmd, input2.cwd, read).includes(REPORT_SENTINEL);
|
|
478
|
+
}
|
|
479
|
+
function applyWalkthroughPosted(state, posted) {
|
|
480
|
+
if (!posted || state.walkthroughPosted === true) return state;
|
|
481
|
+
return { ...state, walkthroughPosted: true };
|
|
482
|
+
}
|
|
483
|
+
function applyWalkthroughSkip(state, skipped) {
|
|
484
|
+
if (!skipped || state.walkthroughSkipped === true) return state;
|
|
485
|
+
return { ...state, walkthroughSkipped: true };
|
|
486
|
+
}
|
|
487
|
+
function runGh(args) {
|
|
488
|
+
try {
|
|
489
|
+
return execFileSync("gh", args, {
|
|
490
|
+
encoding: "utf-8",
|
|
491
|
+
timeout: GH_LOOKUP_TIMEOUT_MS,
|
|
492
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
493
|
+
});
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
var defaultPrWalkthroughLookup = {
|
|
499
|
+
branchPrUrl: () => {
|
|
500
|
+
const url = runGh(["pr", "view", "--json", "url", "-q", ".url"])?.trim();
|
|
501
|
+
return url && url.startsWith("http") ? url : null;
|
|
502
|
+
},
|
|
503
|
+
// Scans the description as well as the comments: muggle-do embeds the
|
|
504
|
+
// walkthrough in the body at PR-creation time, and a comments-only check
|
|
505
|
+
// would report that PR as owing one it already carries.
|
|
506
|
+
prCarriesWalkthrough: (prUrl) => {
|
|
507
|
+
const rendered = runGh(["pr", "view", prUrl, "--json", "body,comments"]);
|
|
508
|
+
if (rendered === null) return true;
|
|
509
|
+
return rendered.includes(REPORT_SENTINEL);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
function scanForOwedWalkthroughs(state, lookup = defaultPrWalkthroughLookup) {
|
|
513
|
+
const candidates = new Set(state.prsHandled);
|
|
514
|
+
const branchPrUrl = lookup.branchPrUrl();
|
|
515
|
+
if (branchPrUrl) candidates.add(branchPrUrl);
|
|
516
|
+
const owed = [];
|
|
517
|
+
const verified = [];
|
|
518
|
+
for (const prUrl of candidates) {
|
|
519
|
+
if (lookup.prCarriesWalkthrough(prUrl)) verified.push(prUrl);
|
|
520
|
+
else owed.push(prUrl);
|
|
521
|
+
}
|
|
522
|
+
return { owed, verified };
|
|
523
|
+
}
|
|
524
|
+
function walkthroughGateDecision(state, owedPrUrls, maxBlocks = MAX_WALKTHROUGH_BLOCKS) {
|
|
525
|
+
const blockCount = state.walkthroughBlockCount ?? 0;
|
|
526
|
+
const alreadySettled = state.walkthroughPosted === true || state.walkthroughSkipped === true || state.e2eRun !== true;
|
|
527
|
+
if (alreadySettled || owedPrUrls.length === 0) {
|
|
528
|
+
return { action: "none" /* None */, blockCount, owed: owedPrUrls };
|
|
529
|
+
}
|
|
530
|
+
if (blockCount >= maxBlocks) {
|
|
531
|
+
return { action: "release" /* Release */, blockCount, owed: owedPrUrls };
|
|
532
|
+
}
|
|
533
|
+
return { action: "block" /* Block */, blockCount: blockCount + 1, owed: owedPrUrls };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/guardrails/detectBuildIntent.ts
|
|
537
|
+
var BUILD = /\b(implement|build|add|create|write|fix|refactor|wire up|hook up|make (a|the|it)|change the)\b/i;
|
|
538
|
+
var DEVCYCLE = /\bresolve\b[^.?!]{0,40}\bconflicts?\b|\bget\b[^.?!]{0,40}\bpr\b[^.?!]{0,40}\b(green|merged?|passing)\b/i;
|
|
539
|
+
var QUESTION = /^\s*(why|what|how|when|where|who|is|are|does|do|can you (explain|tell)|explain)\b/i;
|
|
540
|
+
function detectBuildIntent(prompt) {
|
|
541
|
+
const p = (prompt ?? "").trim();
|
|
542
|
+
if (!p || p.startsWith("/")) return false;
|
|
543
|
+
if (QUESTION.test(p)) return false;
|
|
544
|
+
return BUILD.test(p) || DEVCYCLE.test(p);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/guardrails/reportGate.ts
|
|
242
548
|
function looksLikeE2EReport(text) {
|
|
243
549
|
const t = text.toLowerCase();
|
|
244
550
|
const statusEmojis = (text.match(/[✅❌⚠]/gu) ?? []).length;
|
|
@@ -248,26 +554,11 @@ function looksLikeE2EReport(text) {
|
|
|
248
554
|
const muggleContext = /\bmuggle\b/.test(t) || /muggle-ai\.com/.test(t) || /\be2e\b/.test(t) || /\bacceptance\b/.test(t);
|
|
249
555
|
return resultsStructure && muggleContext;
|
|
250
556
|
}
|
|
251
|
-
function
|
|
252
|
-
let text = cmd;
|
|
253
|
-
for (const m of cmd.matchAll(/--body-file[=\s]+("[^"]+"|'[^']+'|\S+)/g)) {
|
|
254
|
-
const p = unquote(m[1]);
|
|
255
|
-
if (p && p !== "-") {
|
|
256
|
-
const c = read(p, cwd);
|
|
257
|
-
if (c) text += "\n" + c;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
for (const m of cmd.matchAll(/jq\b[^|]*?("[^"]+\.json"|'[^']+\.json'|\S+\.json)/g)) {
|
|
261
|
-
const c = read(unquote(m[1]), cwd);
|
|
262
|
-
if (c) text += "\n" + c;
|
|
263
|
-
}
|
|
264
|
-
return text;
|
|
265
|
-
}
|
|
266
|
-
function evaluateReportPost(input2, read = defaultReader) {
|
|
557
|
+
function evaluateReportPost(input2, read = defaultFileReader) {
|
|
267
558
|
if (input2.tool_name !== "Bash") return { deny: false };
|
|
268
559
|
const cmd = input2.tool_input?.command ?? "";
|
|
269
|
-
if (!
|
|
270
|
-
const text =
|
|
560
|
+
if (!isPrReportPostCommand(cmd)) return { deny: false };
|
|
561
|
+
const text = collectPrPostText(cmd, input2.cwd, read);
|
|
271
562
|
if (text.includes(REPORT_SENTINEL)) return { deny: false };
|
|
272
563
|
if (!looksLikeE2EReport(text)) return { deny: false };
|
|
273
564
|
return {
|
|
@@ -323,6 +614,13 @@ Per the autoWatchPR preference, a muggle-pr-followup watcher should handle its i
|
|
|
323
614
|
return envelope("PostToolUse", ctx, host);
|
|
324
615
|
}
|
|
325
616
|
function prTerminal() {
|
|
617
|
+
const reopenedPrNumber = detectPrReopened(input);
|
|
618
|
+
if (reopenedPrNumber !== null) {
|
|
619
|
+
const state2 = readState(sessionId);
|
|
620
|
+
const next2 = applyPrReopened(state2, reopenedPrNumber);
|
|
621
|
+
if (next2 !== state2) writeState(next2);
|
|
622
|
+
return "{}";
|
|
623
|
+
}
|
|
326
624
|
const terminalEvent = detectPrTerminal(input);
|
|
327
625
|
if (!terminalEvent) return "{}";
|
|
328
626
|
const state = readState(sessionId);
|
|
@@ -357,10 +655,81 @@ function recordTests() {
|
|
|
357
655
|
e2eRan: isE2ERun(input),
|
|
358
656
|
e2eSkipped: isE2ESkipMarker(cmd)
|
|
359
657
|
});
|
|
360
|
-
const
|
|
658
|
+
const withWatchSkip = applyWatchSkip(recorded, isWatchSkipMarker(cmd));
|
|
659
|
+
const withWalkthroughPost = applyWalkthroughPosted(withWatchSkip, detectWalkthroughPost(input));
|
|
660
|
+
const withWalkthroughSkip = applyWalkthroughSkip(withWalkthroughPost, isWalkthroughSkipMarker(cmd));
|
|
661
|
+
const failedRunId = detectFailedRunId(input);
|
|
662
|
+
const next = failedRunId ? applyFailedRun(withWalkthroughSkip, failedRunId) : withWalkthroughSkip;
|
|
361
663
|
if (next !== state) writeState(next);
|
|
362
664
|
return "{}";
|
|
363
665
|
}
|
|
666
|
+
function skillStages() {
|
|
667
|
+
const skillName = resolveSkillNameFromToolInput(input.tool_input);
|
|
668
|
+
if (!skillName) return "{}";
|
|
669
|
+
const state = readState(sessionId);
|
|
670
|
+
const next = applySkillInvocation(
|
|
671
|
+
state,
|
|
672
|
+
skillName,
|
|
673
|
+
resolveSkillStagePaths(skillName, resolvePluginSkillsRoot())
|
|
674
|
+
);
|
|
675
|
+
if (next !== state) writeState(next);
|
|
676
|
+
const unread = unreadMandatoryStages(next);
|
|
677
|
+
if (unread.length === 0) return "{}";
|
|
678
|
+
const ctx = `The ${skillName} skill declares required reading: ${unread.map(stageLabel).join(", ")}. Read those files now, before working through the skill's steps \u2014 they carry mandatory steps SKILL.md only links to, and treating them as optional elaboration is how those steps get silently dropped. A Stop gate holds the turn open until they are opened.`;
|
|
679
|
+
return envelope("PostToolUse", ctx, host);
|
|
680
|
+
}
|
|
681
|
+
function recordStageRead() {
|
|
682
|
+
const filePath = input.tool_input?.file_path;
|
|
683
|
+
if (!filePath) return "{}";
|
|
684
|
+
const state = readState(sessionId);
|
|
685
|
+
const next = applyStageRead(state, filePath);
|
|
686
|
+
if (next !== state) writeState(next);
|
|
687
|
+
return "{}";
|
|
688
|
+
}
|
|
689
|
+
function recordStageSignals() {
|
|
690
|
+
const cmd = input.tool_input?.command ?? "";
|
|
691
|
+
const state = readState(sessionId);
|
|
692
|
+
const withStageSkip = applyStageSkip(state, isStageSkipMarker(cmd));
|
|
693
|
+
const withClassificationSkip = applyClassificationSkip(
|
|
694
|
+
withStageSkip,
|
|
695
|
+
isClassificationSkipMarker(cmd)
|
|
696
|
+
);
|
|
697
|
+
const withDebugSkip = applyDebugSkip(withClassificationSkip, cmd);
|
|
698
|
+
const classifiedTestCaseId = detectClassifiedTestCaseId(input);
|
|
699
|
+
const withClassification = classifiedTestCaseId ? applyClassifiedTestCase(withDebugSkip, classifiedTestCaseId) : withDebugSkip;
|
|
700
|
+
const next = applyDebugEvidence(
|
|
701
|
+
withClassification,
|
|
702
|
+
detectDebugEvidenceRunIds(input, undebuggedFailedRuns(withClassification))
|
|
703
|
+
);
|
|
704
|
+
if (next !== state) writeState(next);
|
|
705
|
+
return "{}";
|
|
706
|
+
}
|
|
707
|
+
function classifyGate() {
|
|
708
|
+
const decision = classificationGateDecision(readState(sessionId), input);
|
|
709
|
+
if (!decision.deny) return "{}";
|
|
710
|
+
const reason = `Test case ${decision.testCaseId} has no pre-execution classification this session. Run muggle-test Step 6f for it first: classify replay-vs-regen per _shared/failure-mode-handling.md \xA7A, then emit one muggle-local-telemetry-event-emit with eventType "pre-execution-classification" for this test case. That step is what calls muggle-remote-test-script-list, which is the only place this run learns the test case has never passed or has failed repeatedly \u2014 cheap now, ~5 minutes of browser time to rediscover after the fact. If this execution genuinely has no classification step (a single user-picked target), say why and run \`echo "MUGGLE_CLASSIFY_SKIP: <reason>"\` \u2014 that records the skip for the rest of the session.`;
|
|
711
|
+
return denyTool(reason, host);
|
|
712
|
+
}
|
|
713
|
+
function stageGate() {
|
|
714
|
+
const state = readState(sessionId);
|
|
715
|
+
const decision = stageGateDecision(state, unreadMandatoryStages(state));
|
|
716
|
+
if (decision.action !== "block" /* Block */) return "{}";
|
|
717
|
+
state.stageBlockCount = decision.blockCount;
|
|
718
|
+
writeState(state);
|
|
719
|
+
const stageList = decision.unread.map(stageLabel).join(", ");
|
|
720
|
+
const reason = decision.blockCount === 1 ? `Do not end the turn yet. A skill invoked this session declares mandatory stages that were never opened: ${stageList}. Read them and carry out what they require \u2014 they are steps, not background reading, and SKILL.md only links to them. If they genuinely do not apply to this run, say why and run \`echo "MUGGLE_STAGE_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `Mandatory stages still unread (reminder ${decision.blockCount}/${MAX_STAGE_BLOCKS}): ${stageList}. Read them, or record a legitimate skip via \`echo "MUGGLE_STAGE_SKIP: <reason>"\`.`;
|
|
721
|
+
return blockStop(reason, host);
|
|
722
|
+
}
|
|
723
|
+
function debugPathGate() {
|
|
724
|
+
const state = readState(sessionId);
|
|
725
|
+
const decision = debugGateDecision(state);
|
|
726
|
+
if (decision.action !== "block" /* Block */) return "{}";
|
|
727
|
+
state.debugBlockCount = decision.blockCount;
|
|
728
|
+
writeState(state);
|
|
729
|
+
const runList = decision.undebugged.join(", ");
|
|
730
|
+
const reason = decision.blockCount === 1 ? `Do not end the turn yet. These runs failed and never went through the debug path: ${runList}. muggle-test Step 7C makes that mandatory \u2014 route each through _shared/debug-failed-run.md: gather the attempted steps and the failing screenshot, diagnose the bucket per _shared/failure-mode-handling.md \xA7B/\xA7C with its classified telemetry emit, and present the offer in which "give feedback & rerun" is always available. A summarized-and-dropped failure is the run a reviewer most needs to see. If a run genuinely cannot be debugged, run \`echo "MUGGLE_DEBUG_SKIP: <runId> <reason>"\` \u2014 that clears just that run.` : `Failed runs still owe the debug path (reminder ${decision.blockCount}/${MAX_DEBUG_BLOCKS}): ${runList}. Route each through _shared/debug-failed-run.md, or record a legitimate skip via \`echo "MUGGLE_DEBUG_SKIP: <runId> <reason>"\`.`;
|
|
731
|
+
return blockStop(reason, host);
|
|
732
|
+
}
|
|
364
733
|
function e2eGate() {
|
|
365
734
|
const state = readState(sessionId);
|
|
366
735
|
const decision = e2eGateDecision(state);
|
|
@@ -372,21 +741,39 @@ function e2eGate() {
|
|
|
372
741
|
}
|
|
373
742
|
function watchGate() {
|
|
374
743
|
const state = readState(sessionId);
|
|
375
|
-
const
|
|
376
|
-
const decision = watchGateDecision(state,
|
|
744
|
+
const untrackedPrUrls = findUntrackedHandledPrs(state.prsHandled);
|
|
745
|
+
const decision = watchGateDecision(state, untrackedPrUrls);
|
|
377
746
|
if (decision.action === "none" /* None */ || decision.action === "release" /* Release */) {
|
|
378
747
|
return "{}";
|
|
379
748
|
}
|
|
380
749
|
state.watchBlockCount = decision.blockCount;
|
|
381
750
|
writeState(state);
|
|
751
|
+
const prList = decision.untracked.join(", ");
|
|
752
|
+
const reason = decision.blockCount === 1 ? `Do not end the turn yet. A PR was opened this session but no muggle-do session slot tracks it: ${prList}. Seed the slot and hand off per muggle-do Stage 8 \u2014 /muggle:muggle-pr-followup ${decision.untracked[0]} does both. Seeding is what matters: once a slot exists, reconcile arms it at the next session start and finalizes it when the PR goes terminal, so an unarmed slot is fine but no slot means nothing ever picks this PR up. If it genuinely should not be tracked (autoWatchPR=never, handed off elsewhere), tell the user why and run \`echo "MUGGLE_WATCH_SKIP: <reason>"\` \u2014 that records the skip and keeps this gate quiet for the rest of the session.` : `PR hand-off still owed for ${prList} (reminder ${decision.blockCount}/${MAX_WATCH_BLOCKS}): seed a slot via /muggle:muggle-pr-followup, or record a legitimate skip via \`echo "MUGGLE_WATCH_SKIP: <reason>"\`.`;
|
|
753
|
+
return blockStop(reason, host);
|
|
754
|
+
}
|
|
755
|
+
function walkthroughGate() {
|
|
756
|
+
const state = readState(sessionId);
|
|
757
|
+
if (state.e2eRun !== true || state.walkthroughPosted === true || state.walkthroughSkipped === true) {
|
|
758
|
+
return "{}";
|
|
759
|
+
}
|
|
760
|
+
const scan = scanForOwedWalkthroughs(state);
|
|
761
|
+
if (scan.owed.length === 0) {
|
|
762
|
+
if (scan.verified.length > 0) writeState({ ...state, walkthroughPosted: true });
|
|
763
|
+
return "{}";
|
|
764
|
+
}
|
|
765
|
+
const decision = walkthroughGateDecision(state, scan.owed);
|
|
766
|
+
if (decision.action !== "block" /* Block */) return "{}";
|
|
767
|
+
state.walkthroughBlockCount = decision.blockCount;
|
|
768
|
+
writeState(state);
|
|
382
769
|
const prList = decision.owed.join(", ");
|
|
383
|
-
const reason = decision.blockCount === 1 ? `Do not end the turn yet.
|
|
770
|
+
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>"\`.`;
|
|
384
771
|
return blockStop(reason, host);
|
|
385
772
|
}
|
|
386
773
|
function reportGate() {
|
|
387
|
-
const
|
|
388
|
-
if (!
|
|
389
|
-
return denyTool(
|
|
774
|
+
const reportPostVerdict = evaluateReportPost(input);
|
|
775
|
+
if (!reportPostVerdict.deny || !reportPostVerdict.reason) return "{}";
|
|
776
|
+
return denyTool(reportPostVerdict.reason, host);
|
|
390
777
|
}
|
|
391
778
|
function buildRouter() {
|
|
392
779
|
if (!detectBuildIntent(input.prompt ?? "")) return "{}";
|
|
@@ -405,7 +792,14 @@ var handlers = {
|
|
|
405
792
|
"e2e-gate": e2eGate,
|
|
406
793
|
"terminal-gate": terminalGate,
|
|
407
794
|
"watch-gate": watchGate,
|
|
795
|
+
"walkthrough-gate": walkthroughGate,
|
|
408
796
|
"report-gate": reportGate,
|
|
409
|
-
"build-router": buildRouter
|
|
797
|
+
"build-router": buildRouter,
|
|
798
|
+
"skill-stages": skillStages,
|
|
799
|
+
"record-stage-read": recordStageRead,
|
|
800
|
+
"record-stage-signals": recordStageSignals,
|
|
801
|
+
"classify-gate": classifyGate,
|
|
802
|
+
"stage-gate": stageGate,
|
|
803
|
+
"debug-path-gate": debugPathGate
|
|
410
804
|
};
|
|
411
805
|
process.stdout.write((handlers[sub] ?? (() => "{}"))());
|