@galda/cli 0.10.43 → 0.10.44
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/app/index.html +6 -6
- package/engine/lib.mjs +43 -0
- package/engine/server.mjs +31 -11
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -4748,7 +4748,7 @@ function renderTasks(){
|
|
|
4748
4748
|
}
|
|
4749
4749
|
for (const inp of $('tasklist').querySelectorAll('[data-qreplyinput]')) {
|
|
4750
4750
|
inp.onclick = (e) => e.stopPropagation();
|
|
4751
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); sendQReply(inp.dataset.qreplyinput, inp); } };
|
|
4751
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); sendQReply(inp.dataset.qreplyinput, inp); } };
|
|
4752
4752
|
}
|
|
4753
4753
|
// Pending rows open the same detail dialog as Review (tap anywhere but the
|
|
4754
4754
|
// Start/Delete buttons, which stop propagation in their own handlers).
|
|
@@ -4872,7 +4872,7 @@ function startGoalheadEdit(id, btn){
|
|
|
4872
4872
|
const save = async () => { const t = inp.value.trim(); if (!t) { wrap.remove(); return; }
|
|
4873
4873
|
const r = await fetch(withKey(`/api/goals/${id}`), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
|
|
4874
4874
|
if (!r.ok) showErr('Cannot edit a running/finished goal — reply to append instead'); await refresh(); };
|
|
4875
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4875
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); save(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4876
4876
|
}
|
|
4877
4877
|
// SL1 inline Reply: append an instruction to a started goal → POST /api/goals/:id/reply.
|
|
4878
4878
|
function openGoalheadReply(id, btn){
|
|
@@ -4885,7 +4885,7 @@ function openGoalheadReply(id, btn){
|
|
|
4885
4885
|
const send = async () => { const t = inp.value.trim(); if (!t) return;
|
|
4886
4886
|
const r = await fetch(withKey(`/api/goals/${id}/reply`), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) });
|
|
4887
4887
|
if (!r.ok) showErr('未開始のゴールは編集を使ってください'); await refresh(); };
|
|
4888
|
-
inp.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4888
|
+
inp.onkeydown = (e) => { if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); send(); } if (e.key === 'Escape') wrap.remove(); };
|
|
4889
4889
|
snd.onclick = send;
|
|
4890
4890
|
}
|
|
4891
4891
|
|
|
@@ -5778,7 +5778,7 @@ async function sendReviewReject(){
|
|
|
5778
5778
|
$('reviewDismiss').onclick = () => sendReviewReject();
|
|
5779
5779
|
$('reviewFixSend').onclick = () => { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); };
|
|
5780
5780
|
$('reviewFixInput').addEventListener('keydown', (e) => {
|
|
5781
|
-
if (e.key === 'Enter') { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
5781
|
+
if (e.key === 'Enter' && !e.isComposing) { const t = $('reviewFixInput').value.trim(); if (t || reviewAttach.length) sendReviewDismiss(t); }
|
|
5782
5782
|
});
|
|
5783
5783
|
$('reviewFixImg').onclick = () => $('reviewFixFile').click();
|
|
5784
5784
|
$('reviewFixFile').addEventListener('change', async () => {
|
|
@@ -6934,7 +6934,7 @@ function inlineFsRename(span, onCommit){
|
|
|
6934
6934
|
if (changed) onCommit?.(v);
|
|
6935
6935
|
span.removeEventListener('keydown', onKey); span.removeEventListener('keyup', onKeyUp); span.removeEventListener('blur', onBlur); };
|
|
6936
6936
|
const onKey = (ev) => { ev.stopPropagation();
|
|
6937
|
-
if (ev.key === 'Enter') { ev.preventDefault(); finish(true); span.blur(); }
|
|
6937
|
+
if (ev.key === 'Enter' && !ev.isComposing) { ev.preventDefault(); finish(true); span.blur(); }
|
|
6938
6938
|
else if (ev.key === 'Escape') { ev.preventDefault(); finish(false); span.blur(); } };
|
|
6939
6939
|
const onKeyUp = (ev) => { ev.stopPropagation(); };
|
|
6940
6940
|
const onBlur = () => finish(true);
|
|
@@ -9586,7 +9586,7 @@ $('ctxdirmenu').addEventListener('click', (e) => {
|
|
|
9586
9586
|
});
|
|
9587
9587
|
$('ctxdirmenu').addEventListener('keydown', (e) => {
|
|
9588
9588
|
e.stopPropagation();
|
|
9589
|
-
if (e.key === 'Enter' && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
|
|
9589
|
+
if (e.key === 'Enter' && !e.isComposing && e.target.id === 'ctxdirin') { e.preventDefault(); setProjectFolder(e.target.value); }
|
|
9590
9590
|
});
|
|
9591
9591
|
$('appbtn').onclick = (e) => { e.stopPropagation(); $('appmenu').classList.toggle('show'); };
|
|
9592
9592
|
$('appmenu').addEventListener('click', (e) => {
|
package/engine/lib.mjs
CHANGED
|
@@ -1158,6 +1158,49 @@ export function isInconclusiveTestRun({ ran = false, timedOut = false, passed =
|
|
|
1158
1158
|
return Boolean(ran) && Boolean(timedOut) && !realAssertion && passed === 0;
|
|
1159
1159
|
}
|
|
1160
1160
|
|
|
1161
|
+
// Infra failures the review gate must NOT block real work on. A test that stands
|
|
1162
|
+
// up a real server/headless Chrome fails these ways under WHOLE-MACHINE
|
|
1163
|
+
// saturation (prod :4400 + several agent sessions + the baseline run all live at
|
|
1164
|
+
// once — which --test-concurrency=1 inside one suite cannot prevent, the load is
|
|
1165
|
+
// from OTHER processes): the server dies during boot ("server exited (code N)
|
|
1166
|
+
// before it was ready"), misses its window ("did not report ready in time"), or a
|
|
1167
|
+
// puppeteer wait/navigation times out. None is the goal's code regressing, and
|
|
1168
|
+
// they SHUFFLE to a different set of tests each run, so classifyTestGate's
|
|
1169
|
+
// name-diff can never subtract them. Count them so a run can discount them.
|
|
1170
|
+
export function countInfraFlakes(text = '') {
|
|
1171
|
+
const pats = [
|
|
1172
|
+
/server exited \(code \d+\) before it was ready/g, // test helper: server crashed on boot under load
|
|
1173
|
+
/did not report ready in time/g, // test helper: server missed its startup window
|
|
1174
|
+
/Waiting failed: \d+\s*ms exceeded/g, // puppeteer waitForFunction/Selector
|
|
1175
|
+
/Navigation timeout of \d+\s*ms exceeded/g, // puppeteer page.goto
|
|
1176
|
+
/test timed out after \d+\s*ms/g, // node --test per-test timeout
|
|
1177
|
+
/\bTarget closed\b/g, // puppeteer: browser died mid-test
|
|
1178
|
+
];
|
|
1179
|
+
return pats.reduce((n, re) => n + (String(text ?? '').match(re) || []).length, 0);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Decide how many of a test run's failures are REAL (should block Review) vs
|
|
1183
|
+
// infra flakes (countInfraFlakes above). The old runTestsOnce let a SINGLE
|
|
1184
|
+
// "expected … to" match anywhere in megabytes of output (realAssertion=true)
|
|
1185
|
+
// disable the infra discount for ALL failures — which is exactly why 93 boot
|
|
1186
|
+
// failures ("server exited (code 1) before it was ready", 650 passed) blocked
|
|
1187
|
+
// goals #616/#620 (2026-07-24): every failure was infra, yet realAssertion
|
|
1188
|
+
// tripped on stray text and none was discounted.
|
|
1189
|
+
// - infraOnly: every counted failure is an infra flake (infraFlakes covers the
|
|
1190
|
+
// whole count) → block on none, regardless of a stray assertion string. If a
|
|
1191
|
+
// REAL assertion failure existed it would be an ADDITIONAL failure not covered
|
|
1192
|
+
// by the infra count, so infraFlakes < failedTotal and we fall through.
|
|
1193
|
+
// - a real assertion sharing a mixed run → fail CLOSED on the full count.
|
|
1194
|
+
// - otherwise → discount the infra flakes from the total.
|
|
1195
|
+
export function classifyRunFailures({ failedTotal = 0, infraFlakes = 0, realAssertion = false } = {}) {
|
|
1196
|
+
const ft = Math.max(0, Number(failedTotal) || 0);
|
|
1197
|
+
const inf = Math.max(0, Number(infraFlakes) || 0);
|
|
1198
|
+
if (ft <= 0) return { effectiveFailed: 0, infraOnly: false };
|
|
1199
|
+
if (inf >= ft) return { effectiveFailed: 0, infraOnly: true };
|
|
1200
|
+
if (realAssertion) return { effectiveFailed: ft, infraOnly: false };
|
|
1201
|
+
return { effectiveFailed: Math.max(0, ft - inf), infraOnly: false };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1161
1204
|
// The verification-gate decision (Masa 2026-07-19: "検証/proof は Failed を作らない").
|
|
1162
1205
|
// PROOF IS ADVISORY: a UI goal whose screenshot could not be captured
|
|
1163
1206
|
// (proofMissing) or whose pixel heuristic could not auto-confirm the target
|
package/engine/server.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { homedir } from 'node:os';
|
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { needsProjectFolder, folderLabel, chooseFolderScript, parseChosenFolder, buildFolderQuestion, resolveFolderAnswer, classifyFolderTarget, buildFolderInitConfirm, isFolderInitConfirmed, needsReviewPreference, buildReviewPreferenceQuestion, resolveReviewPreferenceAnswer, routeQuestionAnswer, notUnderstoodNote, parseRelocalizedQuestion, classifyAuthProbe, authBannerText, makeSigninChallenge, parseClaimResponse } from './lib.mjs';
|
|
24
|
-
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
24
|
+
import { parseStreamEvents, parseCodexEvents, buildCodexArgs, parsePlan, refinePlanTasks, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, taskStatusAfterVerify, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildGoalPrBody, buildReviewSummaryPrompt, parseReviewSummary, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, rejectGoal, reopenGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, workerPrompt, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, pickFoldTarget, hasTestRelevantChanges, parseFailingTestNames, classifyTestGate, isInconclusiveTestRun, countInfraFlakes, classifyRunFailures, classifyVerifyGate, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, QUEUED_GOAL_STATUSES, verifyLicenseToken, shouldEmitSetupCompleted, pickAnalyticsUid, shouldEmitFreeExhausted, detectRequestLanguage, isNothingVerifiable, classifyComposerIntentHeuristic, detectPauseIntent, buildIntentPrompt, parseIntentResponse, buildBoardSnapshot, validateBoardSnapshot, boardIsEmpty, decideBoardPull, resolveConnectedAgents, pickAvailableAgent, pickUtilityAgent, utilityModel, liveTakeoverDecision, shouldOpenBrowserOnBoot, goalTaskTitle, buildGoalTask, RUN_DECL_FILE, parseRunDeclaration, SHOT_DIR, collectShotNames, parseGoalAddress, REPLY_OUTCOMES, REPLY_INTENTS, buildReplyIntentPrompt, parseReplyIntent, buildGoalMessage, serializeGoalMessage, parseGoalMessages } from './lib.mjs';
|
|
25
25
|
import { createSerialQueue } from './lib.mjs';
|
|
26
26
|
import { collectProjectRules } from './lib.mjs';
|
|
27
27
|
import { openPR } from './pr.mjs';
|
|
@@ -2475,15 +2475,21 @@ function runTestsOnce(dir) {
|
|
|
2475
2475
|
const n = (re) => { const m = text.match(re); return m ? Number(m[1]) : null; };
|
|
2476
2476
|
const passed = n(/ℹ pass (\d+)/) ?? n(/# pass (\d+)/) ?? n(/(\d+) passing/) ?? 0;
|
|
2477
2477
|
const failedTotal = n(/ℹ fail (\d+)/) ?? n(/# fail (\d+)/) ?? n(/(\d+) failing/) ?? (e ? 1 : 0);
|
|
2478
|
-
// Don't block on INFRA flakes: the integration suite spawns real servers
|
|
2479
|
-
// and under
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
//
|
|
2483
|
-
//
|
|
2484
|
-
|
|
2478
|
+
// Don't block on INFRA flakes: the integration suite spawns real servers +
|
|
2479
|
+
// headless Chrome, and under WHOLE-MACHINE saturation (prod :4400 + several
|
|
2480
|
+
// agent sessions + the baseline run) a server dies on boot ("server exited
|
|
2481
|
+
// (code N) before it was ready"), misses its window ("did not report ready
|
|
2482
|
+
// in time"), or a puppeteer wait times out — none a code regression, and
|
|
2483
|
+
// they shuffle to different tests each run. countInfraFlakes tallies them;
|
|
2484
|
+
// classifyRunFailures decides how many failures are REAL. Fail CLOSED: a
|
|
2485
|
+
// real AssertionError sharing a MIXED run still blocks (infraFlakes <
|
|
2486
|
+
// failedTotal). Only when EVERY failure is infra (infraOnly) do we discount
|
|
2487
|
+
// despite a stray "expected … to" match — the coarse global realAssertion
|
|
2488
|
+
// flag is exactly what stranded #616/#620 (93 boot failures, all discountable,
|
|
2489
|
+
// yet realAssertion tripped on unrelated text and blocked them).
|
|
2490
|
+
const infraFlakes = countInfraFlakes(text);
|
|
2485
2491
|
const realAssertion = /AssertionError|Expected values to be|expected .+ (?:to |but )/i.test(text);
|
|
2486
|
-
const
|
|
2492
|
+
const { effectiveFailed: failed, infraOnly } = classifyRunFailures({ failedTotal, infraFlakes, realAssertion });
|
|
2487
2493
|
// Was the run KILLED at the execFile timeout (machine too saturated to
|
|
2488
2494
|
// finish), rather than exiting on its own? execFile sets killed=true and
|
|
2489
2495
|
// signal='SIGTERM' on timeout. That is the inconclusive signal — a normal
|
|
@@ -2492,7 +2498,7 @@ function runTestsOnce(dir) {
|
|
|
2492
2498
|
const timedOut = Boolean(e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT'));
|
|
2493
2499
|
// Capture the failing test NAMES too (not just the count) so the gate can
|
|
2494
2500
|
// diff them against the base's failures and block only on NEW ones.
|
|
2495
|
-
res({ passed, failed, timedOut, realAssertion, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2501
|
+
res({ passed, failed, timedOut, realAssertion, infraOnly, infraFlakes, detail: testOutputExcerpt(text), failingNames: parseFailingTestNames(text) });
|
|
2496
2502
|
});
|
|
2497
2503
|
}));
|
|
2498
2504
|
}
|
|
@@ -2516,11 +2522,16 @@ async function runProjectTests(dir) {
|
|
|
2516
2522
|
// a real red in either run still fails closed.
|
|
2517
2523
|
timedOut: Boolean(r.timedOut && r2.timedOut),
|
|
2518
2524
|
realAssertion: r.realAssertion || r2.realAssertion,
|
|
2525
|
+
// infra state from the run we keep (the one with fewer real failures): if
|
|
2526
|
+
// its failures were all infra flakes (infraOnly → failed 0), that is what
|
|
2527
|
+
// the gate should see, so no good goal is blocked on a saturated boot.
|
|
2528
|
+
infraOnly: !!kept.infraOnly,
|
|
2529
|
+
infraFlakes: kept.infraFlakes ?? 0,
|
|
2519
2530
|
detail: kept.detail || r.detail || r2.detail || '',
|
|
2520
2531
|
failingNames: kept.failingNames ?? r.failingNames ?? [],
|
|
2521
2532
|
};
|
|
2522
2533
|
}
|
|
2523
|
-
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
2534
|
+
return { ran: true, passed: r.passed, failed: r.failed, ok: r.failed === 0, timedOut: !!r.timedOut, realAssertion: !!r.realAssertion, infraOnly: !!r.infraOnly, infraFlakes: r.infraFlakes ?? 0, detail: r.detail ?? '', failingNames: r.failingNames ?? [] };
|
|
2524
2535
|
}
|
|
2525
2536
|
|
|
2526
2537
|
// Failing tests on the BASE (pre-change) checkout — the set the review gate
|
|
@@ -2982,6 +2993,15 @@ async function verifyGate(goal, prProject, siblings, baseProject, activityTask =
|
|
|
2982
2993
|
goal.testResult.inconclusive = true;
|
|
2983
2994
|
goalAct('Project tests could not complete — the suite was killed before finishing (0 passed, no real assertion), which means the machine was too saturated to run them, not a code failure. Treating as inconclusive, not blocking. Re-run the tests when the machine is idle to confirm.');
|
|
2984
2995
|
}
|
|
2996
|
+
// Infra-only run (root fix for #616/#620, 2026-07-24): the suite finished but
|
|
2997
|
+
// EVERY failure was an infra flake (a real server could not boot — "server
|
|
2998
|
+
// exited (code N) before it was ready" — or a wait timed out under whole-machine
|
|
2999
|
+
// saturation), so runProjectTests already discounted them to 0. testsFailing is
|
|
3000
|
+
// therefore already false; surface it as an advisory so the human knows the
|
|
3001
|
+
// suite couldn't be trusted here (not that it ran green), and can re-run idle.
|
|
3002
|
+
if (goal.testResult.ran && goal.testResult.infraOnly && !goal.testResult.inconclusive) {
|
|
3003
|
+
goalAct(`Project tests: all ${goal.testResult.infraFlakes || goal.testResult.failed || ''} failure(s) were infra flakes (a real server could not boot / a wait timed out under machine load), not code regressions — not blocking. Re-run when the machine is idle to confirm.`.replace(/\s{2,}/g, ' '));
|
|
3004
|
+
}
|
|
2985
3005
|
// Baseline diff (Masa 2026-07-19): block ONLY on failures THIS change
|
|
2986
3006
|
// introduced. A test already red on the base — e.g. persist-failure on clean
|
|
2987
3007
|
// origin/main — is pre-existing noise and must not knock a proof-verified
|
package/package.json
CHANGED