From bbcaf1eba51473c21a88afac98bc18e61b027aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Tue, 28 Apr 2026 14:22:04 +0000 Subject: [PATCH] refactor: moderator uses per-rejector limits instead of shared coderCount - coder self-iterations (done=false): max 5 - reviewer rejections: max 3 - tester rejections: max 3 - committer rejections: max 2 Each budget is independent, no longer starved by coder's own passes. --- workflows/gitea-issue-solver/moderator.ts | 34 ++++++++++++++--------- workflows/sense-generator/moderator.ts | 33 +++++++++++++++------- workflows/workflow-generator/moderator.ts | 24 ++++++++++++---- 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/workflows/gitea-issue-solver/moderator.ts b/workflows/gitea-issue-solver/moderator.ts index 10b9afd..cdfd0e6 100644 --- a/workflows/gitea-issue-solver/moderator.ts +++ b/workflows/gitea-issue-solver/moderator.ts @@ -1,6 +1,5 @@ import { END } from "@uncaged/nerve-core"; import type { Moderator } from "@uncaged/nerve-core"; -import { TESTER_MAX_ATTEMPTS } from "./lib/constants.js"; import type { IntakeMeta } from "./roles/intake/index.js"; import type { IssueReaderMeta } from "./roles/issue-reader/index.js"; import type { PlannerMeta } from "./roles/planner/index.js"; @@ -19,12 +18,25 @@ export type WorkflowMeta = { "pr-publisher": PrPublisherMeta; }; +const MAX_IMPLEMENTER_SELF_ITERATIONS = 5; +const MAX_REVIEWER_REJECTIONS = 3; +const MAX_TESTER_REJECTIONS = 3; + +function countRejections(steps: { role: string; meta: unknown }[], rejector: string, metaKey: string): number { + return steps.filter((s) => s.role === rejector && !(s.meta as Record)[metaKey]).length; +} + +function countImplementerSelfIterations(steps: { role: string; meta: unknown }[]): number { + return steps.filter((s) => s.role === "implementer" && !(s.meta as Record).implementationOk).length; +} + export const moderator: Moderator = (context) => { if (context.steps.length === 0) { return "intake"; } const last = context.steps[context.steps.length - 1]; + if (last.role === "intake") { const meta = last.meta as WorkflowMeta["intake"]; return meta.valid ? "issue-reader" : END; @@ -32,10 +44,7 @@ export const moderator: Moderator = (context) => { if (last.role === "issue-reader") { const meta = last.meta as WorkflowMeta["issue-reader"]; - if (meta.fetchOk) { - return "planner"; - } - return END; + return meta.fetchOk ? "planner" : END; } if (last.role === "planner") { @@ -44,21 +53,20 @@ export const moderator: Moderator = (context) => { } if (last.role === "implementer") { - return "reviewer"; + const meta = last.meta as WorkflowMeta["implementer"]; + if (meta.implementationOk) return "reviewer"; + return countImplementerSelfIterations(context.steps) < MAX_IMPLEMENTER_SELF_ITERATIONS ? "implementer" : END; } if (last.role === "reviewer") { - const meta = last.meta as WorkflowMeta["reviewer"]; - if (meta.approved) return "tester"; - return "implementer"; + if (last.meta.approved) return "tester"; + return countRejections(context.steps, "reviewer", "approved") < MAX_REVIEWER_REJECTIONS ? "implementer" : END; } if (last.role === "tester") { const meta = last.meta as WorkflowMeta["tester"]; - if (meta.passed) { - return "pr-publisher"; - } - return meta.attempt < TESTER_MAX_ATTEMPTS ? "implementer" : END; + if (meta.passed) return "pr-publisher"; + return countRejections(context.steps, "tester", "passed") < MAX_TESTER_REJECTIONS ? "implementer" : END; } if (last.role === "pr-publisher") { diff --git a/workflows/sense-generator/moderator.ts b/workflows/sense-generator/moderator.ts index 3dd09de..ac941ef 100644 --- a/workflows/sense-generator/moderator.ts +++ b/workflows/sense-generator/moderator.ts @@ -14,32 +14,45 @@ export type SenseMeta = { committer: CommitterMeta; }; -const MAX_CODER_ITERATIONS = 5; +const MAX_CODER_SELF_ITERATIONS = 5; +const MAX_REVIEWER_REJECTIONS = 3; +const MAX_TESTER_REJECTIONS = 3; +const MAX_COMMITTER_REJECTIONS = 2; -function countRole(steps: { role: string }[], name: string): number { - return steps.filter((s) => s.role === name).length; +function countRejections(steps: { role: string; meta: unknown }[], rejector: string, metaKey: string): number { + return steps.filter((s) => s.role === rejector && !(s.meta as Record)[metaKey]).length; +} + +function countCoderSelfIterations(steps: { role: string; meta: unknown }[]): number { + return steps.filter((s) => s.role === "coder" && !(s.meta as Record).done).length; } export const moderator: Moderator = (context) => { if (context.steps.length === 0) return "planner"; + const last = context.steps[context.steps.length - 1]; - const coderCount = context.steps.filter((s) => s.role === "coder").length; if (last.role === "planner") return "coder"; - if (last.role === "coder") return "reviewer"; + + if (last.role === "coder") { + if (last.meta.done) return "reviewer"; + return countCoderSelfIterations(context.steps) < MAX_CODER_SELF_ITERATIONS ? "coder" : END; + } + if (last.role === "reviewer") { if (last.meta.approved) return "tester"; - return coderCount < MAX_CODER_ITERATIONS ? "coder" : END; + return countRejections(context.steps, "reviewer", "approved") < MAX_REVIEWER_REJECTIONS ? "coder" : END; } + if (last.role === "tester") { if (last.meta.passed) return "committer"; - const testerCount = countRole(context.steps, "tester"); - if (testerCount < 3 && coderCount < MAX_CODER_ITERATIONS) return "coder"; - return END; + return countRejections(context.steps, "tester", "passed") < MAX_TESTER_REJECTIONS ? "coder" : END; } + if (last.role === "committer") { if (last.meta.success) return END; - return coderCount < MAX_CODER_ITERATIONS ? "coder" : END; + return countRejections(context.steps, "committer", "success") < MAX_COMMITTER_REJECTIONS ? "coder" : END; } + return END; }; diff --git a/workflows/workflow-generator/moderator.ts b/workflows/workflow-generator/moderator.ts index ee30801..dfec6b3 100644 --- a/workflows/workflow-generator/moderator.ts +++ b/workflows/workflow-generator/moderator.ts @@ -14,13 +14,24 @@ export type WorkflowMeta = { committer: CommitterMeta; }; -const MAX_CODER_ITERATIONS = 5; +const MAX_CODER_SELF_ITERATIONS = 5; +const MAX_REVIEWER_REJECTIONS = 3; +const MAX_TESTER_REJECTIONS = 3; +const MAX_COMMITTER_REJECTIONS = 2; + +function countRejections(steps: { role: string; meta: unknown }[], rejector: string, metaKey: string): number { + return steps.filter((s) => s.role === rejector && !(s.meta as Record)[metaKey]).length; +} + +function countCoderSelfIterations(steps: { role: string; meta: unknown }[]): number { + // coder rounds where done=false (self-iteration, not a rejection retry) + return steps.filter((s) => s.role === "coder" && !(s.meta as Record).done).length; +} export const moderator: Moderator = (context) => { if (context.steps.length === 0) return "planner"; const last = context.steps[context.steps.length - 1]; - const coderCount = context.steps.filter((s) => s.role === "coder").length; if (last.role === "planner") { return last.meta.ready ? "coder" : END; @@ -28,22 +39,23 @@ export const moderator: Moderator = (context) => { if (last.role === "coder") { if (last.meta.done) return "reviewer"; - return coderCount < MAX_CODER_ITERATIONS ? "coder" : END; + // coder says not done yet — allow self-iteration + return countCoderSelfIterations(context.steps) < MAX_CODER_SELF_ITERATIONS ? "coder" : END; } if (last.role === "reviewer") { if (last.meta.approved) return "tester"; - return coderCount < MAX_CODER_ITERATIONS ? "coder" : END; + return countRejections(context.steps, "reviewer", "approved") < MAX_REVIEWER_REJECTIONS ? "coder" : END; } if (last.role === "tester") { if (last.meta.passed) return "committer"; - return coderCount < MAX_CODER_ITERATIONS ? "coder" : END; + return countRejections(context.steps, "tester", "passed") < MAX_TESTER_REJECTIONS ? "coder" : END; } if (last.role === "committer") { if (last.meta.success) return END; - return coderCount < MAX_CODER_ITERATIONS ? "coder" : END; + return countRejections(context.steps, "committer", "success") < MAX_COMMITTER_REJECTIONS ? "coder" : END; } return END;