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.
This commit is contained in:
parent
fbcc1ff30c
commit
bbcaf1eba5
@ -1,6 +1,5 @@
|
|||||||
import { END } from "@uncaged/nerve-core";
|
import { END } from "@uncaged/nerve-core";
|
||||||
import type { Moderator } 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 { IntakeMeta } from "./roles/intake/index.js";
|
||||||
import type { IssueReaderMeta } from "./roles/issue-reader/index.js";
|
import type { IssueReaderMeta } from "./roles/issue-reader/index.js";
|
||||||
import type { PlannerMeta } from "./roles/planner/index.js";
|
import type { PlannerMeta } from "./roles/planner/index.js";
|
||||||
@ -19,12 +18,25 @@ export type WorkflowMeta = {
|
|||||||
"pr-publisher": PrPublisherMeta;
|
"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<string, boolean>)[metaKey]).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countImplementerSelfIterations(steps: { role: string; meta: unknown }[]): number {
|
||||||
|
return steps.filter((s) => s.role === "implementer" && !(s.meta as Record<string, boolean>).implementationOk).length;
|
||||||
|
}
|
||||||
|
|
||||||
export const moderator: Moderator<WorkflowMeta> = (context) => {
|
export const moderator: Moderator<WorkflowMeta> = (context) => {
|
||||||
if (context.steps.length === 0) {
|
if (context.steps.length === 0) {
|
||||||
return "intake";
|
return "intake";
|
||||||
}
|
}
|
||||||
|
|
||||||
const last = context.steps[context.steps.length - 1];
|
const last = context.steps[context.steps.length - 1];
|
||||||
|
|
||||||
if (last.role === "intake") {
|
if (last.role === "intake") {
|
||||||
const meta = last.meta as WorkflowMeta["intake"];
|
const meta = last.meta as WorkflowMeta["intake"];
|
||||||
return meta.valid ? "issue-reader" : END;
|
return meta.valid ? "issue-reader" : END;
|
||||||
@ -32,10 +44,7 @@ export const moderator: Moderator<WorkflowMeta> = (context) => {
|
|||||||
|
|
||||||
if (last.role === "issue-reader") {
|
if (last.role === "issue-reader") {
|
||||||
const meta = last.meta as WorkflowMeta["issue-reader"];
|
const meta = last.meta as WorkflowMeta["issue-reader"];
|
||||||
if (meta.fetchOk) {
|
return meta.fetchOk ? "planner" : END;
|
||||||
return "planner";
|
|
||||||
}
|
|
||||||
return END;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last.role === "planner") {
|
if (last.role === "planner") {
|
||||||
@ -44,21 +53,20 @@ export const moderator: Moderator<WorkflowMeta> = (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (last.role === "implementer") {
|
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") {
|
if (last.role === "reviewer") {
|
||||||
const meta = last.meta as WorkflowMeta["reviewer"];
|
if (last.meta.approved) return "tester";
|
||||||
if (meta.approved) return "tester";
|
return countRejections(context.steps, "reviewer", "approved") < MAX_REVIEWER_REJECTIONS ? "implementer" : END;
|
||||||
return "implementer";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last.role === "tester") {
|
if (last.role === "tester") {
|
||||||
const meta = last.meta as WorkflowMeta["tester"];
|
const meta = last.meta as WorkflowMeta["tester"];
|
||||||
if (meta.passed) {
|
if (meta.passed) return "pr-publisher";
|
||||||
return "pr-publisher";
|
return countRejections(context.steps, "tester", "passed") < MAX_TESTER_REJECTIONS ? "implementer" : END;
|
||||||
}
|
|
||||||
return meta.attempt < TESTER_MAX_ATTEMPTS ? "implementer" : END;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last.role === "pr-publisher") {
|
if (last.role === "pr-publisher") {
|
||||||
|
|||||||
@ -14,32 +14,45 @@ export type SenseMeta = {
|
|||||||
committer: CommitterMeta;
|
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 {
|
function countRejections(steps: { role: string; meta: unknown }[], rejector: string, metaKey: string): number {
|
||||||
return steps.filter((s) => s.role === name).length;
|
return steps.filter((s) => s.role === rejector && !(s.meta as Record<string, boolean>)[metaKey]).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countCoderSelfIterations(steps: { role: string; meta: unknown }[]): number {
|
||||||
|
return steps.filter((s) => s.role === "coder" && !(s.meta as Record<string, boolean>).done).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const moderator: Moderator<SenseMeta> = (context) => {
|
export const moderator: Moderator<SenseMeta> = (context) => {
|
||||||
if (context.steps.length === 0) return "planner";
|
if (context.steps.length === 0) return "planner";
|
||||||
|
|
||||||
const last = context.steps[context.steps.length - 1];
|
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 === "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.role === "reviewer") {
|
||||||
if (last.meta.approved) return "tester";
|
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.role === "tester") {
|
||||||
if (last.meta.passed) return "committer";
|
if (last.meta.passed) return "committer";
|
||||||
const testerCount = countRole(context.steps, "tester");
|
return countRejections(context.steps, "tester", "passed") < MAX_TESTER_REJECTIONS ? "coder" : END;
|
||||||
if (testerCount < 3 && coderCount < MAX_CODER_ITERATIONS) return "coder";
|
|
||||||
return END;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (last.role === "committer") {
|
if (last.role === "committer") {
|
||||||
if (last.meta.success) return END;
|
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;
|
return END;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,13 +14,24 @@ export type WorkflowMeta = {
|
|||||||
committer: CommitterMeta;
|
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<string, boolean>)[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<string, boolean>).done).length;
|
||||||
|
}
|
||||||
|
|
||||||
export const moderator: Moderator<WorkflowMeta> = (context) => {
|
export const moderator: Moderator<WorkflowMeta> = (context) => {
|
||||||
if (context.steps.length === 0) return "planner";
|
if (context.steps.length === 0) return "planner";
|
||||||
|
|
||||||
const last = context.steps[context.steps.length - 1];
|
const last = context.steps[context.steps.length - 1];
|
||||||
const coderCount = context.steps.filter((s) => s.role === "coder").length;
|
|
||||||
|
|
||||||
if (last.role === "planner") {
|
if (last.role === "planner") {
|
||||||
return last.meta.ready ? "coder" : END;
|
return last.meta.ready ? "coder" : END;
|
||||||
@ -28,22 +39,23 @@ export const moderator: Moderator<WorkflowMeta> = (context) => {
|
|||||||
|
|
||||||
if (last.role === "coder") {
|
if (last.role === "coder") {
|
||||||
if (last.meta.done) return "reviewer";
|
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.role === "reviewer") {
|
||||||
if (last.meta.approved) return "tester";
|
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.role === "tester") {
|
||||||
if (last.meta.passed) return "committer";
|
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.role === "committer") {
|
||||||
if (last.meta.success) return END;
|
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;
|
return END;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user