@muggleai/works 5.27.0-staging.245 → 5.27.0-staging.247
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/.claude-plugin/marketplace.json +1 -1
- package/dist/{chunk-FI6VHHZN.js → chunk-6BAB7IVG.js} +25 -2
- package/dist/{chunk-SJBYMBC2.js → chunk-B5BWBZZ5.js} +142 -28
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.codex-mcp.json +1 -1
- package/dist/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/.mcp.json +1 -1
- package/dist/plugin/hooks/README.md +2 -2
- package/dist/plugin/scripts/pr-watch-arm.sh +27 -10
- package/dist/plugin/scripts/pr-watch-floors.sh +62 -0
- package/dist/plugin/scripts/pr-watch-guards.sh +11 -11
- package/dist/plugin/scripts/pr-watch-loop.sh +63 -50
- package/dist/plugin/scripts/reconcile-stale-watchers.sh +7 -30
- package/dist/plugin/skills/_shared/host-tool-equivalents.md +1 -1
- package/dist/plugin/skills/do/e2e-repair.md +2 -0
- package/dist/plugin/skills/do/next-step.md +1 -1
- package/dist/plugin/skills/do/open-prs/forward.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/SKILL.md +5 -5
- package/dist/plugin/skills/muggle-pr-followup/adopt.md +2 -2
- package/dist/plugin/skills/muggle-pr-followup/arm-watcher.md +21 -16
- package/dist/plugin/skills/muggle-pr-followup/blocked-tick.md +3 -3
- package/dist/plugin/skills/muggle-pr-followup/bootstrap.md +1 -1
- package/dist/plugin/skills/muggle-pr-followup/contract.md +9 -7
- package/dist/plugin/skills/muggle-pr-followup/evals/evals.json +16 -16
- package/dist/plugin/skills/muggle-pr-followup/reconcile.md +4 -4
- package/dist/plugin/skills/muggle-pr-followup/state-schemas.md +17 -6
- package/dist/plugin/skills/muggle-pr-followup/stop.md +3 -3
- package/dist/plugin/skills/muggle-pr-visual-walkthrough/e2e-report-assembly.md +1 -0
- package/dist/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +2 -2
- package/dist/release-manifest.json +3 -3
- package/dist/{src-33XQK7NM.js → src-MCJFH7AB.js} +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-mcp.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/hooks/README.md +2 -2
- package/plugin/scripts/pr-watch-arm.sh +27 -10
- package/plugin/scripts/pr-watch-floors.sh +62 -0
- package/plugin/scripts/pr-watch-guards.sh +11 -11
- package/plugin/scripts/pr-watch-loop.sh +63 -50
- package/plugin/scripts/reconcile-stale-watchers.sh +7 -30
- package/plugin/skills/_shared/host-tool-equivalents.md +1 -1
- package/plugin/skills/do/e2e-repair.md +2 -0
- package/plugin/skills/do/next-step.md +1 -1
- package/plugin/skills/do/open-prs/forward.md +1 -1
- package/plugin/skills/muggle-pr-followup/SKILL.md +5 -5
- package/plugin/skills/muggle-pr-followup/adopt.md +2 -2
- package/plugin/skills/muggle-pr-followup/arm-watcher.md +21 -16
- package/plugin/skills/muggle-pr-followup/blocked-tick.md +3 -3
- package/plugin/skills/muggle-pr-followup/bootstrap.md +1 -1
- package/plugin/skills/muggle-pr-followup/contract.md +9 -7
- package/plugin/skills/muggle-pr-followup/evals/evals.json +16 -16
- package/plugin/skills/muggle-pr-followup/reconcile.md +4 -4
- package/plugin/skills/muggle-pr-followup/state-schemas.md +17 -6
- package/plugin/skills/muggle-pr-followup/stop.md +3 -3
- package/plugin/skills/muggle-pr-visual-walkthrough/e2e-report-assembly.md +1 -0
- package/plugin/skills/muggle-preferences/preference-gates/watcherLifetime.md +2 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "muggleai",
|
|
10
10
|
"source": "./plugin",
|
|
11
11
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
12
|
-
"version": "5.27.0-staging.
|
|
12
|
+
"version": "5.27.0-staging.247"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -5705,7 +5705,7 @@ async function checkServiceStack(params) {
|
|
|
5705
5705
|
const snapshot = await readGenerationStore({ directory: ledgerDir(), emptyRecord: EMPTY_STACK_RECORD });
|
|
5706
5706
|
const swept = sweepStackRecord({ record: snapshot.record, now: Date.now(), isAlive: isProcessAlive });
|
|
5707
5707
|
const ownLease = swept.leases.find((lease) => lease.cwd === cwd) ?? null;
|
|
5708
|
-
const base = { cwd, generation: snapshot.generation, mismatches: [], peerLease: null };
|
|
5708
|
+
const base = { cwd, generation: snapshot.generation, mismatches: [], peerLease: null, servedNeeds: [] };
|
|
5709
5709
|
const isClaimedByCaller = ownLease?.needs.some((need) => need.port === params.port) ?? false;
|
|
5710
5710
|
const peerLease = params.port === void 0 || isClaimedByCaller ? void 0 : swept.leases.find((lease) => lease.cwd !== cwd && lease.needs.some((need) => need.port === params.port));
|
|
5711
5711
|
if (peerLease !== void 0) {
|
|
@@ -5734,7 +5734,12 @@ async function checkServiceStack(params) {
|
|
|
5734
5734
|
mismatches
|
|
5735
5735
|
};
|
|
5736
5736
|
}
|
|
5737
|
-
return {
|
|
5737
|
+
return {
|
|
5738
|
+
...base,
|
|
5739
|
+
verdict: "VALID" /* Valid */,
|
|
5740
|
+
reason: "every claimed port serves the claimed code",
|
|
5741
|
+
servedNeeds: ownLease.needs
|
|
5742
|
+
};
|
|
5738
5743
|
}
|
|
5739
5744
|
async function startStackRun(params) {
|
|
5740
5745
|
const cwd = await resolveCheckoutRoot({ cwd: params.cwd, runGit: params.runGit ?? runGitCommand });
|
|
@@ -9596,6 +9601,20 @@ async function findRunStackRefusal(params) {
|
|
|
9596
9601
|
});
|
|
9597
9602
|
return REFUSING_VERDICTS.has(report.verdict) ? report : null;
|
|
9598
9603
|
}
|
|
9604
|
+
async function readServedStack(params) {
|
|
9605
|
+
if (!isLoopbackUrl(params.localUrl)) return [];
|
|
9606
|
+
const report = await checkServiceStack({ cwd: params.cwd, runGit: params.runGit, readListener: params.readListener });
|
|
9607
|
+
return report.servedNeeds;
|
|
9608
|
+
}
|
|
9609
|
+
function recordServedStack(params) {
|
|
9610
|
+
if (params.servedStack.length === 0 || params.stackChange !== null) return;
|
|
9611
|
+
const storage = getRunResultStorageService();
|
|
9612
|
+
const runResult = storage.getRunResult(params.runId);
|
|
9613
|
+
if (!runResult) return;
|
|
9614
|
+
storage.updateRunResult(params.runId, {
|
|
9615
|
+
localExecutionContext: { ...runResult.localExecutionContext, servedStack: params.servedStack }
|
|
9616
|
+
});
|
|
9617
|
+
}
|
|
9599
9618
|
async function runUnderStackLease(params) {
|
|
9600
9619
|
if (!isLoopbackUrl(params.localUrl)) return await params.execute();
|
|
9601
9620
|
const runId = randomUUID();
|
|
@@ -9985,6 +10004,7 @@ var executeTestGenerationTool = {
|
|
|
9985
10004
|
logger7.warn("Test generation refused: service stack", { verdict: stackRefusal.verdict, cwd: input.cwd });
|
|
9986
10005
|
return { content: formatRunStackRefusal(stackRefusal, input.cwd), isError: true, data: stackRefusal };
|
|
9987
10006
|
}
|
|
10007
|
+
const servedStack = await readServedStack({ cwd: input.cwd, localUrl: input.localUrl });
|
|
9988
10008
|
try {
|
|
9989
10009
|
const generationRun = await runUnderStackLease({
|
|
9990
10010
|
cwd: input.cwd,
|
|
@@ -10012,6 +10032,7 @@ var executeTestGenerationTool = {
|
|
|
10012
10032
|
generationRun.errorMessage ? `**Error:** ${generationRun.errorMessage}` : ""
|
|
10013
10033
|
].filter(Boolean).join("\n");
|
|
10014
10034
|
const stackChange = await findStackChangedDuringRun({ cwd: input.cwd, localUrl: input.localUrl });
|
|
10035
|
+
recordServedStack({ runId: generationRun.id, servedStack, stackChange });
|
|
10015
10036
|
const reportedContent = stackChange ? content + formatStackChangedDuringRun(stackChange) : content;
|
|
10016
10037
|
return {
|
|
10017
10038
|
content: reportedContent,
|
|
@@ -10051,6 +10072,7 @@ var executeReplayTool = {
|
|
|
10051
10072
|
logger7.warn("Test replay refused: service stack", { verdict: stackRefusal.verdict, cwd: input.cwd });
|
|
10052
10073
|
return { content: formatRunStackRefusal(stackRefusal, input.cwd), isError: true, data: stackRefusal };
|
|
10053
10074
|
}
|
|
10075
|
+
const servedStack = await readServedStack({ cwd: input.cwd, localUrl: input.localUrl });
|
|
10054
10076
|
try {
|
|
10055
10077
|
const replayRun = await runUnderStackLease({
|
|
10056
10078
|
cwd: input.cwd,
|
|
@@ -10079,6 +10101,7 @@ var executeReplayTool = {
|
|
|
10079
10101
|
replayRun.errorMessage ? `**Error:** ${replayRun.errorMessage}` : ""
|
|
10080
10102
|
].filter(Boolean).join("\n");
|
|
10081
10103
|
const stackChange = await findStackChangedDuringRun({ cwd: input.cwd, localUrl: input.localUrl });
|
|
10104
|
+
recordServedStack({ runId: replayRun.id, servedStack, stackChange });
|
|
10082
10105
|
const reportedContent = stackChange ? content + formatStackChangedDuringRun(stackChange) : content;
|
|
10083
10106
|
return {
|
|
10084
10107
|
content: reportedContent,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir2, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, resolveElectronAppExecutablePath, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, getActiveElectronAppReleaseStream, buildToolInputJsonSchema, toolRequiresAuth, getCallerCredentials, parseElectronAppDirName, getElectronAppReleaseTagPrefix, hasShownDisclosure, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, ElectronAppReleaseStream, buildElectronAppChecksumsUrl, __require, getDisclosureCopy, buildOnboardingPlan, applyOnboardingAnswers, needsOnboarding, buildLlmSourcePlan, describeLlmSourceStatus, resolveLlmSourceStatus, readRunView, RunViewError, renderRunViewText, writeRunViewHtml, openBrowserUrl, evaluateCheckoutFreshness, runGitCommand, BLOCKING_FRESHNESS_VERDICTS, claimServiceStack, BUSY_EXIT_CODE, checkServiceStack, releaseServiceStack, readServiceStackStatus, resolvePreferences, OnboardingBlanketChoice, PreferenceKey, LlmSource, applyLlmSourceSelection, describeStackMismatch, getDataDir, DEFAULT_LOCAL_LLM_BASE_URL, isValidLocalBaseUrl } from './chunk-
|
|
1
|
+
import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getActiveRuntimeTarget, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, reconcileProjectPreferences, getDataDir2, PREFERENCES_FILE_NAME, isFirstRun, writePreferences, DEFAULT_PREFERENCES, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, getElectronAppSignedFromVersion, getReleaseSignerIdentityUri, verifyFileChecksum, resolveElectronAppExecutablePath, calculateFileChecksum, initTelemetry, Surface, ServiceName, track, EventName, getQaTools, getLocalQaTools, performLogout, assertDeviceCodeClientProvisioned, performLogin, getActiveElectronAppReleaseStream, buildToolInputJsonSchema, toolRequiresAuth, getCallerCredentials, parseElectronAppDirName, getElectronAppReleaseTagPrefix, hasShownDisclosure, markDisclosureShown, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, ElectronAppReleaseStream, buildElectronAppChecksumsUrl, __require, getDisclosureCopy, buildOnboardingPlan, applyOnboardingAnswers, needsOnboarding, buildLlmSourcePlan, describeLlmSourceStatus, resolveLlmSourceStatus, readRunView, RunViewError, renderRunViewText, writeRunViewHtml, openBrowserUrl, evaluateCheckoutFreshness, runGitCommand, BLOCKING_FRESHNESS_VERDICTS, claimServiceStack, BUSY_EXIT_CODE, checkServiceStack, releaseServiceStack, readServiceStackStatus, resolvePreferences, OnboardingBlanketChoice, PreferenceKey, LlmSource, applyLlmSourceSelection, describeStackMismatch, getDataDir, DEFAULT_LOCAL_LLM_BASE_URL, isValidLocalBaseUrl, local_exports } from './chunk-6BAB7IVG.js';
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
@@ -356,6 +356,16 @@ function parentIsAlive(pid) {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
// src/pr-walkthrough/constants.ts
|
|
360
|
+
var REPORT_SENTINEL = "muggle-pr-section";
|
|
361
|
+
var WALKTHROUGH_SLOT_MARKER = "<!-- muggle-pr-walkthrough:v1 -->";
|
|
362
|
+
var WALKTHROUGH_SKIPPED_MARKER = "<!-- muggle-pr-walkthrough-status:skipped -->";
|
|
363
|
+
var WALKTHROUGH_COMMENT_HEADING = "### Muggle AI \u2014 PR visual walkthrough";
|
|
364
|
+
var RERUN_NOTICE_OPENING = "<!-- muggle-pr-walkthrough-rerun -->";
|
|
365
|
+
var SHORT_SHA_LENGTH = 7;
|
|
366
|
+
var GH_COMMENT_TIMEOUT_MS = 1e4;
|
|
367
|
+
var DEPENDENCY_BOT_LOGINS = ["dependabot[bot]", "renovate[bot]"];
|
|
368
|
+
|
|
359
369
|
// src/cli/pr-section/constants.ts
|
|
360
370
|
var DETAIL_IMAGE_WIDTH = 720;
|
|
361
371
|
var STEP_IMAGE_WIDTH = 640;
|
|
@@ -369,6 +379,84 @@ var SHORT_ID_LABEL_SEPARATOR = " \xB7 ";
|
|
|
369
379
|
var TEST_DETAILS_ANCHOR_PREFIX = "muggle-test-";
|
|
370
380
|
var STEP_PAIR_IMAGE_WIDTH = 420;
|
|
371
381
|
var MAX_COMMENT_BYTES = 64e3;
|
|
382
|
+
var FULL_COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
383
|
+
var BASELINE_FAILED_STATUS = "failed";
|
|
384
|
+
|
|
385
|
+
// src/cli/pr-section/baseline.ts
|
|
386
|
+
var refused = (refusalReason) => ({ isCredited: false, refusalReason });
|
|
387
|
+
async function resolveBaseRange(deps) {
|
|
388
|
+
const baseRef = deps.baseBranch ? `refs/remotes/origin/${deps.baseBranch}` : await deps.runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], deps.cwd);
|
|
389
|
+
if (!baseRef) return null;
|
|
390
|
+
const baseTipSha = await deps.runGit(["rev-parse", "--verify", `${baseRef}^{commit}`], deps.cwd);
|
|
391
|
+
if (!baseTipSha) return null;
|
|
392
|
+
const mergeBaseSha = await deps.runGit(["merge-base", "HEAD", baseTipSha], deps.cwd);
|
|
393
|
+
return mergeBaseSha ? { mergeBaseSha, baseTipSha } : null;
|
|
394
|
+
}
|
|
395
|
+
async function isOnBaseSinceFork(sha, range, deps) {
|
|
396
|
+
const isAncestor = async (ancestor, descendant) => await deps.runGit(["merge-base", "--is-ancestor", ancestor, descendant], deps.cwd) !== null;
|
|
397
|
+
return await isAncestor(range.mergeBaseSha, sha) && await isAncestor(sha, range.baseTipSha);
|
|
398
|
+
}
|
|
399
|
+
function servedCommitsOf(baselineRun) {
|
|
400
|
+
const servedStack = baselineRun.localExecutionContext?.servedStack;
|
|
401
|
+
if (!Array.isArray(servedStack)) return [];
|
|
402
|
+
return servedStack.map((servedNeed) => servedNeed?.headSha).filter((sha) => typeof sha === "string" && FULL_COMMIT_SHA_PATTERN.test(sha));
|
|
403
|
+
}
|
|
404
|
+
async function judgeBaseline(test, baselineRunId, range, deps) {
|
|
405
|
+
if (range === null) return refused("the base branch could not be resolved in this checkout");
|
|
406
|
+
const baselineRun = deps.readRunResult(baselineRunId);
|
|
407
|
+
if (!baselineRun) return refused("no such run in the local run store");
|
|
408
|
+
if (baselineRun.id === test.runId) return refused("it is the failing run itself");
|
|
409
|
+
if (baselineRun.cloudTestCaseId !== test.testCaseId) return refused(`it ran test case ${baselineRun.cloudTestCaseId}`);
|
|
410
|
+
if (baselineRun.status !== BASELINE_FAILED_STATUS) return refused(`its status is ${baselineRun.status}`);
|
|
411
|
+
if (test.runSettingsId && baselineRun.cloudRunSettingsId && baselineRun.cloudRunSettingsId !== test.runSettingsId) {
|
|
412
|
+
return refused("it ran under a different runSettings record");
|
|
413
|
+
}
|
|
414
|
+
const servedCommits = servedCommitsOf(baselineRun);
|
|
415
|
+
if (servedCommits.length === 0) return refused("it recorded no stack it served");
|
|
416
|
+
for (const servedSha of servedCommits) {
|
|
417
|
+
if (!await isOnBaseSinceFork(servedSha, range, deps)) continue;
|
|
418
|
+
return {
|
|
419
|
+
isCredited: true,
|
|
420
|
+
failsOnBase: {
|
|
421
|
+
baselineRunId: baselineRun.id,
|
|
422
|
+
baselineSha: servedSha,
|
|
423
|
+
...baselineRun.viewUrl && { viewUrl: baselineRun.viewUrl }
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const span = `${range.mergeBaseSha.slice(0, SHORT_SHA_LENGTH)}..${range.baseTipSha.slice(0, SHORT_SHA_LENGTH)}`;
|
|
428
|
+
return refused(`none of the commits it served is on the base between ${span}`);
|
|
429
|
+
}
|
|
430
|
+
function withoutFailsOnBase(test) {
|
|
431
|
+
const uncredited = { ...test };
|
|
432
|
+
delete uncredited.failsOnBase;
|
|
433
|
+
return uncredited;
|
|
434
|
+
}
|
|
435
|
+
async function verifyBaselineFailures(report, deps) {
|
|
436
|
+
let range;
|
|
437
|
+
const tests = [];
|
|
438
|
+
for (const test of report.tests) {
|
|
439
|
+
if (test.status !== "failed") {
|
|
440
|
+
tests.push(test);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const uncredited = withoutFailsOnBase(test);
|
|
444
|
+
if (!test.baselineRunId) {
|
|
445
|
+
tests.push(uncredited);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (range === void 0) range = await resolveBaseRange(deps);
|
|
449
|
+
const judgment = await judgeBaseline(uncredited, test.baselineRunId, range, deps);
|
|
450
|
+
if (judgment.isCredited) {
|
|
451
|
+
tests.push({ ...uncredited, failsOnBase: judgment.failsOnBase });
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
deps.stderrWrite(`build-pr-section: baseline ${test.baselineRunId} for "${test.name}" not credited: ${judgment.refusalReason}
|
|
455
|
+
`);
|
|
456
|
+
tests.push(uncredited);
|
|
457
|
+
}
|
|
458
|
+
return { ...report, tests };
|
|
459
|
+
}
|
|
372
460
|
|
|
373
461
|
// src/e2e-result/types.ts
|
|
374
462
|
var UnpassedCaseStatus = /* @__PURE__ */ ((UnpassedCaseStatus2) => {
|
|
@@ -400,8 +488,12 @@ function describeUnpassedCase(unpassed) {
|
|
|
400
488
|
const outcome = unpassed.status === "failed" /* Failed */ && unpassed.failedStep !== void 0 ? `failed at step ${unpassed.failedStep}` : unpassed.status;
|
|
401
489
|
return `${outcome}: ${escapeHtml(unpassed.detail)}`;
|
|
402
490
|
}
|
|
491
|
+
function describeFailsOnBase(failsOnBase) {
|
|
492
|
+
const baselineLink = failsOnBase.viewUrl ? ` ([baseline run](${failsOnBase.viewUrl}))` : "";
|
|
493
|
+
return ` \xB7 also fails on base \`${failsOnBase.baselineSha.slice(0, SHORT_SHA_LENGTH)}\`${baselineLink}`;
|
|
494
|
+
}
|
|
403
495
|
function renderUnpassedCaseBullet(unpassed, linkText, linkTarget) {
|
|
404
|
-
return `- ${UNPASSED_STATUS_EMOJI[unpassed.status]} **${escapeHtml(withResolutionLabel(unpassed.name, unpassed.resolutionLabel))}** \u2014 ${describeUnpassedCase(unpassed)} \xB7 [${linkText}](${linkTarget})
|
|
496
|
+
return `- ${UNPASSED_STATUS_EMOJI[unpassed.status]} **${escapeHtml(withResolutionLabel(unpassed.name, unpassed.resolutionLabel))}** \u2014 ${describeUnpassedCase(unpassed)} \xB7 [${linkText}](${linkTarget})` + (unpassed.failsOnBase ? describeFailsOnBase(unpassed.failsOnBase) : "");
|
|
405
497
|
}
|
|
406
498
|
var UnpassedCaseSchema = z.object({
|
|
407
499
|
name: z.string(),
|
|
@@ -411,7 +503,8 @@ var UnpassedCaseSchema = z.object({
|
|
|
411
503
|
status: z.enum(UnpassedCaseStatus),
|
|
412
504
|
failedStep: z.number().int().positive().optional(),
|
|
413
505
|
detail: z.string(),
|
|
414
|
-
viewUrl: z.string()
|
|
506
|
+
viewUrl: z.string(),
|
|
507
|
+
failsOnBase: z.object({ baselineRunId: z.string(), baselineSha: z.string(), viewUrl: z.string().optional() }).optional()
|
|
415
508
|
});
|
|
416
509
|
var E2eResultRecordSchema = z.object({
|
|
417
510
|
total: z.number().int().nonnegative(),
|
|
@@ -467,7 +560,8 @@ function toUnpassedCase(test) {
|
|
|
467
560
|
status: "failed" /* Failed */,
|
|
468
561
|
failedStep: failureOrdinal(test, test.failureStepIndex),
|
|
469
562
|
detail: oneLineDetail(test.error),
|
|
470
|
-
viewUrl: test.viewUrl
|
|
563
|
+
viewUrl: test.viewUrl,
|
|
564
|
+
...test.failsOnBase && { failsOnBase: test.failsOnBase }
|
|
471
565
|
};
|
|
472
566
|
}
|
|
473
567
|
if (test.status === "inconclusive") {
|
|
@@ -532,6 +626,9 @@ var FailedTestSchema = z.object({
|
|
|
532
626
|
steps: z.array(StepSchema),
|
|
533
627
|
failureStepIndex: z.number().int().nonnegative(),
|
|
534
628
|
error: z.string().min(1),
|
|
629
|
+
// A run of the same test case on the PR's base the author says failed too; build-pr-section verifies it before
|
|
630
|
+
// the walkthrough credits it.
|
|
631
|
+
baselineRunId: z.string().min(1).optional(),
|
|
535
632
|
artifactsDir: z.string().min(1).optional(),
|
|
536
633
|
useCaseName: z.string().min(1).optional(),
|
|
537
634
|
description: z.string().min(1).optional(),
|
|
@@ -1054,7 +1151,7 @@ async function resolveGsScreenshotUrls(report, opts) {
|
|
|
1054
1151
|
if (gsUrls.length === 0) {
|
|
1055
1152
|
return report;
|
|
1056
1153
|
}
|
|
1057
|
-
const mcps = await import('./src-
|
|
1154
|
+
const mcps = await import('./src-MCJFH7AB.js');
|
|
1058
1155
|
const credentials = await mcps.getCallerCredentialsAsync();
|
|
1059
1156
|
if (!credentials.bearerToken && !credentials.apiKey) {
|
|
1060
1157
|
stderrWrite(
|
|
@@ -1148,7 +1245,15 @@ ${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
|
|
|
1148
1245
|
}
|
|
1149
1246
|
return 1;
|
|
1150
1247
|
}
|
|
1151
|
-
const
|
|
1248
|
+
const verifyBaselines = opts.verifyBaselines ?? ((parsedReport) => verifyBaselineFailures(parsedReport, {
|
|
1249
|
+
cwd: process.cwd(),
|
|
1250
|
+
baseBranch: opts.baseBranch,
|
|
1251
|
+
runGit: runGitCommand,
|
|
1252
|
+
readRunResult: (runId) => local_exports.getRunResultStorageService().getRunResult(runId),
|
|
1253
|
+
stderrWrite: opts.stderrWrite
|
|
1254
|
+
}));
|
|
1255
|
+
const verifiedReport = await verifyBaselines(report);
|
|
1256
|
+
const resolvedReport = await resolveGsScreenshotUrls(verifiedReport, { stderrWrite: opts.stderrWrite });
|
|
1152
1257
|
const sentinelCost = Buffer.byteLength(`${REPORT_SECTION_SENTINEL}
|
|
1153
1258
|
`, "utf-8");
|
|
1154
1259
|
const renderedSection = buildPrSection(resolvedReport, {
|
|
@@ -1174,7 +1279,8 @@ async function buildPrSectionCommand(options) {
|
|
|
1174
1279
|
stdin: process.stdin,
|
|
1175
1280
|
stdoutWrite: (s) => process.stdout.write(s),
|
|
1176
1281
|
stderrWrite: (s) => process.stderr.write(s),
|
|
1177
|
-
maxBodyBytes
|
|
1282
|
+
maxBodyBytes,
|
|
1283
|
+
baseBranch: options.base
|
|
1178
1284
|
});
|
|
1179
1285
|
if (code !== 0) {
|
|
1180
1286
|
process.exitCode = code;
|
|
@@ -2278,7 +2384,6 @@ var WATCH_STATUS_FILE = "status.json";
|
|
|
2278
2384
|
var WATCH_STATE_FILE = "state.json";
|
|
2279
2385
|
var WATCH_EVENTS_FILE = "events.jsonl";
|
|
2280
2386
|
var WATCH_EVENTS_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
2281
|
-
var MONITOR_STDOUT_CAP_MS = 30 * 6e4;
|
|
2282
2387
|
var WATCHER_REARM_GRACE_MS = 5 * 6e4;
|
|
2283
2388
|
var CODEX_ACTIVE_WINDOW_MS = 10 * 6e4;
|
|
2284
2389
|
var CODEX_ROLLOUT_DAY_DIRS_SCANNED = 7;
|
|
@@ -3192,16 +3297,6 @@ var NON_APP_PATH_PATTERNS = [
|
|
|
3192
3297
|
/^\.gitlab-ci\.yml$/
|
|
3193
3298
|
];
|
|
3194
3299
|
|
|
3195
|
-
// src/pr-walkthrough/constants.ts
|
|
3196
|
-
var REPORT_SENTINEL = "muggle-pr-section";
|
|
3197
|
-
var WALKTHROUGH_SLOT_MARKER = "<!-- muggle-pr-walkthrough:v1 -->";
|
|
3198
|
-
var WALKTHROUGH_SKIPPED_MARKER = "<!-- muggle-pr-walkthrough-status:skipped -->";
|
|
3199
|
-
var WALKTHROUGH_COMMENT_HEADING = "### Muggle AI \u2014 PR visual walkthrough";
|
|
3200
|
-
var RERUN_NOTICE_OPENING = "<!-- muggle-pr-walkthrough-rerun -->";
|
|
3201
|
-
var SHORT_SHA_LENGTH = 7;
|
|
3202
|
-
var GH_COMMENT_TIMEOUT_MS = 1e4;
|
|
3203
|
-
var DEPENDENCY_BOT_LOGINS = ["dependabot[bot]", "renovate[bot]"];
|
|
3204
|
-
|
|
3205
3300
|
// src/pr-walkthrough/comment.ts
|
|
3206
3301
|
function renderReservedComment() {
|
|
3207
3302
|
return `${WALKTHROUGH_SLOT_MARKER}
|
|
@@ -3411,6 +3506,7 @@ var CHECK_SUMMARY_MAX_CASES = 50;
|
|
|
3411
3506
|
var OUTCOME_CONCLUSIONS = {
|
|
3412
3507
|
["settled" /* Settled */]: "success" /* Success */,
|
|
3413
3508
|
["e2e-not-passing" /* E2eNotPassing */]: "failure" /* Failure */,
|
|
3509
|
+
["e2e-fails-on-base" /* E2eFailsOnBase */]: "neutral" /* Neutral */,
|
|
3414
3510
|
["rerun-in-progress" /* RerunInProgress */]: null,
|
|
3415
3511
|
["unsettled" /* Unsettled */]: "failure" /* Failure */,
|
|
3416
3512
|
["unreadable" /* Unreadable */]: "neutral" /* Neutral */
|
|
@@ -3427,15 +3523,32 @@ function countLabel(count, status) {
|
|
|
3427
3523
|
return count > 0 ? `${count} ${status}` : null;
|
|
3428
3524
|
}
|
|
3429
3525
|
function renderNotPassingSummary(record) {
|
|
3526
|
+
return renderCaseSummary(
|
|
3527
|
+
record,
|
|
3528
|
+
`E2E acceptance did not pass \u2014 ${unpassedTally(record)} of ${testCaseCount(record)}. Fix them, re-run the whole suite, and update the walkthrough comment; editing it re-runs this check.`
|
|
3529
|
+
);
|
|
3530
|
+
}
|
|
3531
|
+
function renderFailsOnBaseSummary(record) {
|
|
3532
|
+
return renderCaseSummary(
|
|
3533
|
+
record,
|
|
3534
|
+
`E2E acceptance: ${unpassedTally(record)} of ${testCaseCount(record)}, each verified to fail on the base too. This PR did not cause them; fix them on the base branch.`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
function testCaseCount(record) {
|
|
3538
|
+
return `${record.total} test ${record.total === 1 ? "case" : "cases"}`;
|
|
3539
|
+
}
|
|
3540
|
+
function unpassedTally(record) {
|
|
3430
3541
|
const failed2 = record.unpassed.filter((c) => c.status === "failed" /* Failed */).length;
|
|
3431
|
-
|
|
3542
|
+
return [
|
|
3432
3543
|
countLabel(failed2, "failed" /* Failed */),
|
|
3433
3544
|
countLabel(record.unpassed.length - failed2, "inconclusive" /* Inconclusive */)
|
|
3434
3545
|
].filter((part) => part !== null).join(", ");
|
|
3546
|
+
}
|
|
3547
|
+
function renderCaseSummary(record, headline) {
|
|
3435
3548
|
const listed = record.unpassed.slice(0, CHECK_SUMMARY_MAX_CASES).map((unpassed) => renderUnpassedCaseBullet(unpassed, "open run", unpassed.viewUrl));
|
|
3436
3549
|
const hidden = record.unpassed.length - listed.length;
|
|
3437
3550
|
return [
|
|
3438
|
-
|
|
3551
|
+
headline,
|
|
3439
3552
|
"",
|
|
3440
3553
|
...listed,
|
|
3441
3554
|
...hidden > 0 ? [`- \u2026and ${hidden} more \u2014 see the walkthrough comment.`] : []
|
|
@@ -3469,7 +3582,10 @@ function judgeComments(pr, comments, run) {
|
|
|
3469
3582
|
}
|
|
3470
3583
|
const record = latestE2eResult(bodies);
|
|
3471
3584
|
if (record && record.unpassed.length > 0) {
|
|
3472
|
-
|
|
3585
|
+
const isEveryUnpassedOnBase = record.unpassed.every(
|
|
3586
|
+
(unpassed) => unpassed.status === "failed" /* Failed */ && unpassed.failsOnBase !== void 0
|
|
3587
|
+
);
|
|
3588
|
+
return isEveryUnpassedOnBase ? { outcome: "e2e-fails-on-base" /* E2eFailsOnBase */, summary: renderFailsOnBaseSummary(record) } : { outcome: "e2e-not-passing" /* E2eNotPassing */, summary: renderNotPassingSummary(record) };
|
|
3473
3589
|
}
|
|
3474
3590
|
const outcome = verdictNow === "satisfied" /* Satisfied */ ? "settled" /* Settled */ : "unsettled" /* Unsettled */;
|
|
3475
3591
|
return { outcome, summary: FIXED_SUMMARIES[outcome] };
|
|
@@ -4873,8 +4989,7 @@ function resolveWatcherLiveness(loop, heartbeatAgeMs, isOwnerSessionActive, isPr
|
|
|
4873
4989
|
return isHeartbeatWithin(WATCH_HEARTBEAT_STALE_MS) ? "polling" /* Polling */ : "dead" /* Dead */;
|
|
4874
4990
|
}
|
|
4875
4991
|
if (loop) {
|
|
4876
|
-
|
|
4877
|
-
return hasOutlivedMonitor || isOwnerSessionActive === false ? "deaf" /* Deaf */ : "polling" /* Polling */;
|
|
4992
|
+
return isOwnerSessionActive === false ? "deaf" /* Deaf */ : "polling" /* Polling */;
|
|
4878
4993
|
}
|
|
4879
4994
|
return isHeartbeatWithin(WATCHER_REARM_GRACE_MS) ? "rearming" /* Rearming */ : "dead" /* Dead */;
|
|
4880
4995
|
}
|
|
@@ -4941,12 +5056,11 @@ function watcherFinding(assessment, subject) {
|
|
|
4941
5056
|
);
|
|
4942
5057
|
}
|
|
4943
5058
|
if (assessment.watcher === "deaf" /* Deaf */) {
|
|
4944
|
-
const reason = assessment.isOwnerSessionActive === false ? `its owner session ${shortSessionId(assessment.ownerSessionId ?? "")} has ended` : `it has run ${formatDuration(assessment.loopAgeMs ?? 0)}, past the 30-minute Monitor cap`;
|
|
4945
5059
|
return finding(
|
|
4946
5060
|
"WATCHER_DEAF" /* WatcherDeaf */,
|
|
4947
5061
|
[assessment.pr.url, assessment.loopPid],
|
|
4948
5062
|
subject,
|
|
4949
|
-
`watch loop pid ${assessment.loopPid} reports to no session: ${
|
|
5063
|
+
`watch loop pid ${assessment.loopPid} reports to no session: its owner session ${shortSessionId(assessment.ownerSessionId ?? "")} has ended`
|
|
4950
5064
|
);
|
|
4951
5065
|
}
|
|
4952
5066
|
return void 0;
|
|
@@ -6058,8 +6172,8 @@ function renderReloadReport(report, isDryRun) {
|
|
|
6058
6172
|
const lines = report.decisions.map((decision) => {
|
|
6059
6173
|
const label = decision.name ?? decision.shortId ?? decision.sessionId;
|
|
6060
6174
|
const failed2 = report.failedSessionIds.includes(decision.sessionId) ? " (restart failed)" : "";
|
|
6061
|
-
const
|
|
6062
|
-
return ` ${decision.action.padEnd(16)} ${label} ${decision.reason}${failed2}${
|
|
6175
|
+
const refused2 = report.untrustedSessionIds.includes(decision.sessionId) ? ` (left running: ${decision.cwd ?? "its workspace"} refuses a background restart \u2014 run /reload-plugins in it)` : "";
|
|
6176
|
+
return ` ${decision.action.padEnd(16)} ${label} ${decision.reason}${failed2}${refused2}`;
|
|
6063
6177
|
});
|
|
6064
6178
|
const isRestart = (action) => action === "respawn" /* Respawn */ || action === "resume-reconcile" /* ResumeReconcile */;
|
|
6065
6179
|
const respawnVerb = isDryRun ? "would restart" : "restarted";
|
|
@@ -6137,7 +6251,7 @@ function createProgram() {
|
|
|
6137
6251
|
program.command("login").description("Authenticate with Muggle AI (uses device code flow)").option("--key-name <name>", "Name for the API key").option("--key-expiry <expiry>", "API key expiry: 30d, 90d, 1y, never", "90d").action(loginCommand);
|
|
6138
6252
|
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
6139
6253
|
program.command("status").description("Show authentication status").action(statusCommand);
|
|
6140
|
-
program.command("build-pr-section").description("Render a muggle-do PR body evidence block from an e2e report on stdin").option("--max-body-bytes <n>", "Max UTF-8 byte budget for the PR body (default 60000)").action(buildPrSectionCommand);
|
|
6254
|
+
program.command("build-pr-section").description("Render a muggle-do PR body evidence block from an e2e report on stdin").option("--max-body-bytes <n>", "Max UTF-8 byte budget for the PR body (default 60000)").option("--base <branch>", "Branch the PR merges into, for verifying cited baseline runs (default: the remote's default branch)").action(buildPrSectionCommand);
|
|
6141
6255
|
program.command("run-view").argument("[runId]", "Run id, or a unique prefix of one (default: the newest run)").description("Show a local run's steps beside the frames they produced").option("--html", "Render a self-contained HTML page instead of terminal text").option("--open", "Open the rendered page in the default browser (implies --html)").option("--out-dir <dir>", "Directory to write the page into (default: current directory)").action(runViewCommand);
|
|
6142
6256
|
program.command("pr-walkthrough-check").description("Check that a PR's Muggle AI walkthrough comment carries a result or a stated skip").option("--repo <owner/repo>", "Repository (default: $GITHUB_REPOSITORY)").option("--pr <number>", "Pull request number (default: the Actions event payload)").option("--head-sha <sha>", "Commit the check run attaches to (default: the PR head)").option("--check-run", "Publish the verdict as a check run instead of failing the process").action(prWalkthroughCheckCommand);
|
|
6143
6257
|
program.command("checkout-freshness").description("Fetch the base branch and check that a checkout is built on its latest commit").option("--cwd <dir>", "Directory inside the checkout (default: the current directory)").option("--base <branch>", "Branch the change merges into (default: origin's default branch)").option("--no-fetch", "Check against the last fetched base instead of fetching").option("--json", "Print the full report as JSON").action(checkoutFreshnessCommand);
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-
|
|
2
|
-
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-
|
|
1
|
+
export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-B5BWBZZ5.js';
|
|
2
|
+
export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-6BAB7IVG.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
4
|
-
"version": "5.27.0-staging.
|
|
4
|
+
"version": "5.27.0-staging.247",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.27.0-staging.
|
|
3
|
+
"version": "5.27.0-staging.247",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
|
-
"version": "5.27.0-staging.
|
|
5
|
+
"version": "5.27.0-staging.247",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/dist/plugin/.mcp.json
CHANGED
|
@@ -77,9 +77,9 @@ Codex runs a plugin's hooks only once the user trusts them: it asks at startup (
|
|
|
77
77
|
|
|
78
78
|
`SessionStart` (`scripts/reconcile-stale-watchers.sh`) — a standalone advisory, not part of the `guardrails.mjs` decision tree above.
|
|
79
79
|
|
|
80
|
-
`muggle-pr-followup` watchers are session-only (a
|
|
80
|
+
`muggle-pr-followup` watchers are session-only (a background watch loop or `/loop` cron); they die with their session, leaving open PRs with no live poller. This is by design — a review is addressed only inside a session that carries the context to address it. The skill's [`reconcile`](../skills/muggle-pr-followup/reconcile.md) procedure recovers them at the next session start — finalizes slots whose PR went terminal, sweeps orphan crons, re-arms silently-stopped open watchers — but re-arming needs Claude tools a shell hook can't call. So this hook nudges rather than acts.
|
|
81
81
|
|
|
82
|
-
The nudge counts only slots **this session owns
|
|
82
|
+
The nudge counts only slots **this session owns** — ownership and the recovery rules it gates are defined in [`reconcile`](../skills/muggle-pr-followup/reconcile.md). A session that owns none hears nothing: the hook speaks into every session start, and slots owned elsewhere are no work of this one's — `muggle watch status` reports them. A pure directory scan (no `gh`, no writes), so it's cheap enough for every session start.
|
|
83
83
|
|
|
84
84
|
## Session-start upkeep
|
|
85
85
|
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
# and never reaching its terminal check. Every health signal said fine. Doing
|
|
9
9
|
# the three steps here, in one command, is what makes that unskippable.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
# afterwards: anything that lands between the two would otherwise be marked
|
|
13
|
-
# without ever being reported.
|
|
11
|
+
# A first arm takes its floors from the drain's own read rather than from a fresh
|
|
12
|
+
# fetch afterwards: anything that lands between the two would otherwise be marked
|
|
13
|
+
# seen without ever being reported. A re-arm keeps the review, comment and thread
|
|
14
|
+
# floors the session already holds, for the same reason.
|
|
14
15
|
#
|
|
15
16
|
# A stale branch or a red head is floored only when the slot proves a cycle ran
|
|
16
17
|
# for it. Flooring whatever was outstanding at arm time let a session that never
|
|
@@ -38,7 +39,7 @@ if [ -z "$slot" ] || [ -z "$repo" ] || [ -z "$pr_number" ] || [ -z "$base_branch
|
|
|
38
39
|
exit 2
|
|
39
40
|
fi
|
|
40
41
|
|
|
41
|
-
for lib in pr-watch-guards.sh pr-watch-events.sh pr-watch-fetch.sh pr-watch-due.sh; do
|
|
42
|
+
for lib in pr-watch-guards.sh pr-watch-events.sh pr-watch-fetch.sh pr-watch-due.sh pr-watch-floors.sh; do
|
|
42
43
|
if [ ! -f "${script_dir}/${lib}" ]; then
|
|
43
44
|
echo "pr-watch-arm: missing ${lib} beside this script" >&2
|
|
44
45
|
exit 2
|
|
@@ -135,9 +136,10 @@ recorded_rebase_due_key="$rebase_due_key"
|
|
|
135
136
|
thread_count=0
|
|
136
137
|
[ -n "$unresolved_threads" ] && thread_count=$(printf '%s' "$unresolved_threads" | tr ';' '\n' | grep -c .)
|
|
137
138
|
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
# loop's first
|
|
139
|
+
# A record of the PR as armed, read from the watch's output when its loop ends.
|
|
140
|
+
# Printing wakes nobody: the session hears the watch only when the loop exits.
|
|
141
|
+
# Unattempted work is left unfloored, so the loop's first poll reports it and
|
|
142
|
+
# exits — that report is the wake.
|
|
141
143
|
echo "DRAIN pr=$pr_number state=$pr_state head=${head_sha:0:8}"
|
|
142
144
|
echo "DRAIN unresolved-threads=$thread_count latest-review=${latest_review:-0} latest-comment=${latest_comment:-0}"
|
|
143
145
|
echo "DRAIN checks pending=${pending_checks:-0} failed=${failed_checks:-0} behind-base=${behind_count:-unknown} mergeable=${mergeable:-UNKNOWN}"
|
|
@@ -158,10 +160,25 @@ if [ -n "$rebase_due_key" ]; then
|
|
|
158
160
|
fi
|
|
159
161
|
fi
|
|
160
162
|
|
|
163
|
+
# A re-arm carries the review, comment and thread floors forward. The session
|
|
164
|
+
# advanced them past what it handled, so anything newer — a review landing
|
|
165
|
+
# between its last tick's read and this one — is above them, and the loop's
|
|
166
|
+
# first poll reports it. Seeding them from this read would floor that review
|
|
167
|
+
# unseen. Only a first arm, with nothing handled yet, takes them from its read.
|
|
168
|
+
review_floor="${latest_review:-0}"
|
|
169
|
+
comment_floor="${latest_comment:-0}"
|
|
170
|
+
threads_floor="$unresolved_threads"
|
|
171
|
+
if [ -f "${slot}/watch-watermark.env" ]; then
|
|
172
|
+
review_floor="$(watch_read_slot_value "$slot" watch-watermark.env REV)"
|
|
173
|
+
comment_floor="$(watch_read_slot_value "$slot" watch-watermark.env COM)"
|
|
174
|
+
threads_floor="$(watch_threads_still_unresolved \
|
|
175
|
+
"$(watch_read_slot_value "$slot" watch-watermark.env THREADS)" "$unresolved_threads")"
|
|
176
|
+
fi
|
|
177
|
+
|
|
161
178
|
{
|
|
162
|
-
printf 'REV=%s\n' "${
|
|
163
|
-
printf 'COM=%s\n' "${
|
|
164
|
-
printf 'THREADS="%s"\n' "$
|
|
179
|
+
printf 'REV=%s\n' "${review_floor:-0}"
|
|
180
|
+
printf 'COM=%s\n' "${comment_floor:-0}"
|
|
181
|
+
printf 'THREADS="%s"\n' "$threads_floor"
|
|
165
182
|
printf 'CIRED="%s"\n' "$ci_red_floor"
|
|
166
183
|
printf 'REBASED="%s"\n' "$rebase_floor"
|
|
167
184
|
printf 'BLOCKED_CIDIGEST="%s"\n' "$blocked_digest_floor"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# The floors a watch compares each poll against, as they persist between loops.
|
|
4
|
+
# Sourced by pr-watch-arm.sh and pr-watch-loop.sh.
|
|
5
|
+
#
|
|
6
|
+
# A loop exits on every report, because the session that owns it hears it only
|
|
7
|
+
# when it exits. So no floor can live in a loop's memory: whatever stops one
|
|
8
|
+
# occurrence firing twice has to be on disk for the next loop to read. Two files
|
|
9
|
+
# hold them — watch-watermark.env, which the session advances past what it
|
|
10
|
+
# handled, and watch-reported.env, which the loop advances past what it told
|
|
11
|
+
# the session — and a poll fires only on what is above both.
|
|
12
|
+
|
|
13
|
+
MUGGLE_PR_WATCH_REPORTED_FILE="watch-reported.env"
|
|
14
|
+
|
|
15
|
+
# Prints the value of `key` in a slot file of `KEY="value"` lines, quotes
|
|
16
|
+
# stripped, the last match winning; nothing when the file or key is absent.
|
|
17
|
+
# Read with builtins, never sourced: THREADS holds bare semicolons, which a
|
|
18
|
+
# sourced unquoted line would split, and a CI digest carries check names an
|
|
19
|
+
# author controls.
|
|
20
|
+
watch_read_slot_value() {
|
|
21
|
+
local slot="$1" file_name="$2" key="$3" line value=""
|
|
22
|
+
[ -f "${slot}/${file_name}" ] || return 0
|
|
23
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
24
|
+
line="${line%$'\r'}"
|
|
25
|
+
case "$line" in
|
|
26
|
+
"${key}="*)
|
|
27
|
+
value="${line#"${key}"=}"
|
|
28
|
+
value="${value%\"}"
|
|
29
|
+
value="${value#\"}"
|
|
30
|
+
;;
|
|
31
|
+
esac
|
|
32
|
+
done < "${slot}/${file_name}"
|
|
33
|
+
printf '%s' "$value"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Writes <slot>/watch-reported.env: the newest review and comment ids and the
|
|
37
|
+
# unresolved threads a loop's last poll saw — every one of them either reported
|
|
38
|
+
# or already floored — and the red head, rebase key and blocked digest it
|
|
39
|
+
# reported, with the block (`blocked_for`, that block's watermark digest) the
|
|
40
|
+
# digest was reported under.
|
|
41
|
+
watch_record_reported() {
|
|
42
|
+
local slot="$1" review="$2" comment="$3" threads="$4" ci_red="$5" rebase="$6" blocked_digest="$7" blocked_for="$8"
|
|
43
|
+
printf 'REV=%s\nCOM=%s\nTHREADS="%s"\nCIRED="%s"\nREBASED="%s"\nBLOCKED_CIDIGEST="%s"\nBLOCKED_FOR="%s"\n' \
|
|
44
|
+
"$review" "$comment" "$threads" "$ci_red" "$rebase" "$blocked_digest" "$blocked_for" \
|
|
45
|
+
> "${slot}/${MUGGLE_PR_WATCH_REPORTED_FILE}"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Prints the threads of `known` (semicolon-joined) that are still in
|
|
49
|
+
# `unresolved`, semicolon-joined. A thread resolved since it was floored drops
|
|
50
|
+
# out, so its unresolving again is news.
|
|
51
|
+
watch_threads_still_unresolved() {
|
|
52
|
+
local known="$1" unresolved="$2" thread_id kept=""
|
|
53
|
+
local -a thread_ids
|
|
54
|
+
IFS=';' read -ra thread_ids <<< "$known"
|
|
55
|
+
for thread_id in "${thread_ids[@]}"; do
|
|
56
|
+
[ -n "$thread_id" ] || continue
|
|
57
|
+
case ";${unresolved};" in
|
|
58
|
+
*";${thread_id};"*) kept="${kept:+${kept};}${thread_id}" ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
printf '%s' "$kept"
|
|
62
|
+
}
|