Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2796,10 +2796,18 @@ async function runAgentMaintenancePlanAndExecute(
// Each read is independent and fail-open (isHoldOnly / isCloseHoldOnly read false until a breaker actually
// engages), so the common path is byte-identical (both downgrades return the plan unchanged). The chaining is
// extracted into the pure applyPrecisionBreakers below so it is unit-tested directly.
const breakerMinerAuthored = pr.authorLogin
? (
await getCachedOfficialMinerDetection(env, pr.authorLogin, {
targetKey: `${repoFullName}#${pr.number}`,
deliveryId,
})
).status === "confirmed"
: false;
const breakerOnPlan = applyPrecisionBreakers(
planned,
await isHoldOnly(env, repoFullName),
await isCloseHoldOnly(env, repoFullName),
await isHoldOnly(env, repoFullName, breakerMinerAuthored),
await isCloseHoldOnly(env, repoFullName, breakerMinerAuthored),
{
manualReviewLabel: settings.manualReviewLabel,
readyToMergeLabel: settings.readyToMergeLabel,
Expand Down Expand Up @@ -2846,6 +2854,10 @@ async function runAgentMaintenancePlanAndExecute(
conclusion: gate.conclusion,
action: disposition.actionClass,
reasonCode: disposition.blockerClass === "none" ? gate.conclusion : disposition.blockerClass,
// #2352: this row is the ACTUAL autonomous disposition that the precision breaker evaluates, so preserve
// the same miner-authored scope as the gate-check audit row below. Omitting it defaults to non-miner and can
// erase a prior miner-authored prediction for the same head.
minerAuthored: breakerMinerAuthored,
});
// #2349 (PR 1): additive per-contributor calibration data, gated identically to recordNativeGateDecision
// above -- see src/review/contributor-calibration.ts's doc comment. Currently write-only; nothing reads
Expand Down
19 changes: 16 additions & 3 deletions src/review/outcomes-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,22 @@ function flagTruthy(v: string | null | undefined): boolean {

/** Is auto-merge disabled (would-merge → hold) for this project (or globally)? Fail-OPEN (false) on a DB error.
* This is the read the merge path consults to downgrade a would-MERGE into a HOLD. */
export async function isHoldOnly(env: Env, project: string): Promise<boolean> {
export async function isHoldOnly(
env: Env,
project: string,
minerAuthored = false,
): Promise<boolean> {
try {
const res = await env.DB.prepare(
"SELECT key, value FROM system_flags",
).all<{ key: string; value: string }>();
const set = new Set<string>();
for (const r of res.results ?? []) if (flagTruthy(r.value)) set.add(r.key);
return set.has("holdonly:global") || set.has(`holdonly:${project}`);
return (
set.has("holdonly:global") ||
set.has(`holdonly:${project}`) ||
(minerAuthored && set.has(`holdonly:${minerBreakerScope(project)}`))
);
} catch (error) {
console.warn(
JSON.stringify({
Expand All @@ -86,14 +94,19 @@ export async function isHoldOnly(env: Env, project: string): Promise<boolean> {
export async function isCloseHoldOnly(
env: Env,
project: string,
minerAuthored = false,
): Promise<boolean> {
try {
const res = await env.DB.prepare(
"SELECT key, value FROM system_flags",
).all<{ key: string; value: string }>();
const set = new Set<string>();
for (const r of res.results ?? []) if (flagTruthy(r.value)) set.add(r.key);
return set.has("closehold:global") || set.has(`closehold:${project}`);
return (
set.has("closehold:global") ||
set.has(`closehold:${project}`) ||
(minerAuthored && set.has(`closehold:${minerBreakerScope(project)}`))
);
} catch (error) {
console.warn(
JSON.stringify({
Expand Down
22 changes: 22 additions & 0 deletions test/unit/outcomes-wire.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,17 @@ describe("isHoldOnly + createFlagStore (system_flags, migration 0054)", () => {
expect(await isHoldOnly(env, "any/repo")).toBe(true);
});

it("enforces a miner-scoped holdonly flag only for confirmed miner-authored PRs", async () => {
const env = createTestEnv();
const flags = createFlagStore(env);
await flags.setFlag("holdonly:owner/repo:miner", true);

expect(await isHoldOnly(env, "owner/repo")).toBe(false);
expect(await isHoldOnly(env, "owner/repo", false)).toBe(false);
expect(await isHoldOnly(env, "owner/repo", true)).toBe(true);
expect(await isHoldOnly(env, "owner/other", true)).toBe(false);
});

it("flagSetAt round-trips the updated_at and is null when unset", async () => {
const env = createTestEnv();
const flags = createFlagStore(env);
Expand All @@ -552,6 +563,17 @@ describe("isCloseHoldOnly + createFlagStore.isCloseHoldOnly (closehold:<scope>,
expect(await isCloseHoldOnly(env, "any/repo")).toBe(true);
});

it("enforces a miner-scoped closehold flag only for confirmed miner-authored PRs", async () => {
const env = createTestEnv();
const flags = createFlagStore(env);
await flags.setFlag("closehold:owner/repo:miner", true);

expect(await isCloseHoldOnly(env, "owner/repo")).toBe(false);
expect(await isCloseHoldOnly(env, "owner/repo", false)).toBe(false);
expect(await isCloseHoldOnly(env, "owner/repo", true)).toBe(true);
expect(await isCloseHoldOnly(env, "owner/other", true)).toBe(false);
});

it("createFlagStore.isCloseHoldOnly reads the per-project closehold key (not the global one)", async () => {
const env = createTestEnv();
const flags = createFlagStore(env);
Expand Down