refactor: remove closer role from coding and coding-tdd workflows
CI / test (pull_request) Has been cancelled

What: Remove the closer role from coding and coding-tdd workflows.
Why: The closer role only produced a summary after reviewer approved,
adding no value — reviewer approval is sufficient to end the workflow.
Changes:
- Delete CloserMeta/TddCloserMeta types and defaultCloser implementations
- Moderator now routes directly to END after reviewer approves
- Update all tests, .js and .d.ts artifacts accordingly
- Remove closer from index exports

团子 🐰
This commit is contained in:
2026-04-18 12:16:02 +00:00
parent 6c3888af48
commit f3857888da
11 changed files with 55 additions and 125 deletions
+2 -2
View File
@@ -3,8 +3,8 @@
*
* 小橘 🍊 (NEKO Team)
*/
export { type ArchitectMeta, type CloserMeta, type CoderMeta, type CodingRoles, createCodingWorkflow, type ReviewerMeta, } from './workflows/coding.js';
export { type AutoTesterMeta, type CreateTddCodingWorkflowOpts, createTddCodingWorkflow, type ManualTesterMeta, type TddCloserMeta, type TddCoderMeta, type TddCodingRoles, type TddReviewerMeta, type TestCoderMeta, type TestPlannerMeta, type TestReviewerMeta, } from './workflows/coding-tdd.js';
export { type ArchitectMeta, type CoderMeta, type CodingRoles, createCodingWorkflow, type ReviewerMeta, } from './workflows/coding.js';
export { type AutoTesterMeta, type CreateTddCodingWorkflowOpts, createTddCodingWorkflow, type ManualTesterMeta, type TddCoderMeta, type TddCodingRoles, type TddReviewerMeta, type TestCoderMeta, type TestPlannerMeta, type TestReviewerMeta, } from './workflows/coding-tdd.js';
export { type AnalystMeta, createReportWorkflow, type RendererMeta, type ReportRoles, } from './workflows/report.js';
export { checkCursorHealth, type CursorHealthOptions, type CursorHealthResult, } from './workflows/cursor-health.js';
export { createWerewolfWorkflow, type CreateWerewolfWorkflowOpts, type DaySpeechMeta, filterChainForPlayer, type GameEndMeta, type GameState, type HunterShotMeta, type Identity, parseGameState, type Player, type SeerCheckMeta, type VoteMeta, type WerewolfRoles, type WitchActionMeta, type WolfNightMeta, createPlayers, } from './workflows/werewolf.js';
+2 -2
View File
@@ -8,7 +8,7 @@
export {
type ArchitectMeta,
type CloserMeta,
type CoderMeta,
type CodingRoles,
createCodingWorkflow,
@@ -20,7 +20,7 @@ export {
type CreateTddCodingWorkflowOpts,
createTddCodingWorkflow,
type ManualTesterMeta,
type TddCloserMeta,
type TddCoderMeta,
type TddCodingRoles,
type TddReviewerMeta,
+1 -4
View File
@@ -1,5 +1,5 @@
/**
* TDD-driven coding workflow — test-planner → … → closer.
* TDD-driven coding workflow — test-planner → … → reviewer → END.
*
* Pure roles + START/END automaton. Trigger: coding-tdd.__start__
*
@@ -40,7 +40,6 @@ export type TddReviewerMeta = {
codeQuality: string;
testQuality: string;
};
export type TddCloserMeta = Record<string, never>;
export type TddCodingRoles = {
'test-planner': Role<TestPlannerMeta>;
'test-reviewer': Role<TestReviewerMeta>;
@@ -49,7 +48,6 @@ export type TddCodingRoles = {
'auto-tester': Role<AutoTesterMeta>;
'manual-tester': Role<ManualTesterMeta>;
reviewer: Role<TddReviewerMeta>;
closer: Role<TddCloserMeta>;
};
export type CreateTddCodingWorkflowOpts = {
testPlannerFn?: Role<TestPlannerMeta>;
@@ -59,6 +57,5 @@ export type CreateTddCodingWorkflowOpts = {
autoTesterFn?: Role<AutoTesterMeta>;
manualTesterFn?: Role<ManualTesterMeta>;
reviewerFn?: Role<TddReviewerMeta>;
closerFn?: Role<TddCloserMeta>;
};
export declare function createTddCodingWorkflow(opts?: CreateTddCodingWorkflowOpts): WorkflowType<TddCodingRoles>;
@@ -1,5 +1,5 @@
/**
* TDD-driven coding workflow — test-planner → … → closer.
* TDD-driven coding workflow — test-planner → … → reviewer → END.
*
* Pure roles + START/END automaton. Trigger: coding-tdd.__start__
*
@@ -67,14 +67,7 @@ const defaultTddReviewer = async () => ({
testQuality: 'good',
},
});
const defaultCloser = async (chain) => {
const startMsg = chain.find((m) => m.role === '__start__');
const title = startMsg?.meta?.title ?? 'task';
return {
content: `[mock] TDD workflow report: ${title}`,
meta: {},
};
};
function tddCodingModerator(output, _topicId, remainingRounds) {
const emergency = remainingRounds !== undefined && remainingRounds <= 1;
if (output.role === START)
@@ -88,7 +81,7 @@ function tddCodingModerator(output, _topicId, remainingRounds) {
return 'test-coder';
if (verdict === 'rejected') {
if (emergency)
return 'closer';
return END;
return 'test-planner';
}
return 'test-planner';
@@ -101,21 +94,21 @@ function tddCodingModerator(output, _topicId, remainingRounds) {
if (output.meta?.pass)
return 'manual-tester';
if (emergency)
return 'closer';
return END;
return 'coder';
}
case 'manual-tester': {
if (output.meta?.pass)
return 'reviewer';
if (emergency)
return 'closer';
return END;
return 'coder';
}
case 'reviewer': {
if (emergency)
return 'closer';
return END;
if (output.meta?.verdict === 'approved')
return 'closer';
return END;
return 'coder';
}
case 'closer':
@@ -136,8 +129,7 @@ export function createTddCodingWorkflow(opts) {
'auto-tester': opts?.autoTesterFn ?? defaultAutoTester,
'manual-tester': opts?.manualTesterFn ?? defaultManualTester,
reviewer: opts?.reviewerFn ?? defaultTddReviewer,
closer: opts?.closerFn ?? defaultCloser,
},
},
moderator: tddCodingModerator,
limits: { maxRounds: 25 },
};
@@ -80,7 +80,6 @@ describe('coding-tdd WorkflowType', () => {
'auto-tester',
'manual-tester',
'reviewer',
'closer',
]);
const r = await rule.tick();
@@ -135,7 +134,7 @@ describe('coding-tdd WorkflowType', () => {
).toBe('test-planner');
});
it('moderator: test-reviewer rejected + emergency → closer', () => {
it('moderator: test-reviewer rejected + emergency → END', () => {
const wf = createTddCodingWorkflow();
expect(
wf.moderator(
@@ -146,7 +145,7 @@ describe('coding-tdd WorkflowType', () => {
'x',
1,
),
).toBe('closer');
).toBe(END);
});
it('moderator: test-coder → coder', () => {
@@ -205,7 +204,7 @@ describe('coding-tdd WorkflowType', () => {
).toBe('coder');
});
it('moderator: auto-tester fail + emergency → closer', () => {
it('moderator: auto-tester fail + emergency → END', () => {
const wf = createTddCodingWorkflow();
expect(
wf.moderator(
@@ -216,7 +215,7 @@ describe('coding-tdd WorkflowType', () => {
'x',
1,
),
).toBe('closer');
).toBe(END);
});
it('moderator: manual-tester pass → reviewer', () => {
@@ -243,7 +242,7 @@ describe('coding-tdd WorkflowType', () => {
).toBe('coder');
});
it('moderator: manual-tester fail + emergency → closer', () => {
it('moderator: manual-tester fail + emergency → END', () => {
const wf = createTddCodingWorkflow();
expect(
wf.moderator(
@@ -254,10 +253,10 @@ describe('coding-tdd WorkflowType', () => {
'x',
1,
),
).toBe('closer');
).toBe(END);
});
it('moderator: reviewer approved → closer', () => {
it('moderator: reviewer approved → END', () => {
const wf = createTddCodingWorkflow();
expect(
wf.moderator(
@@ -272,10 +271,10 @@ describe('coding-tdd WorkflowType', () => {
},
'x',
),
).toBe('closer');
).toBe(END);
});
it('moderator: reviewer approved + emergency → closer', () => {
it('moderator: reviewer approved + emergency → END', () => {
const wf = createTddCodingWorkflow();
expect(
wf.moderator(
@@ -291,7 +290,7 @@ describe('coding-tdd WorkflowType', () => {
'x',
1,
),
).toBe('closer');
).toBe(END);
});
it('moderator: reviewer rejected → coder', () => {
@@ -313,7 +312,7 @@ describe('coding-tdd WorkflowType', () => {
).toBe('coder');
});
it('moderator: reviewer rejected + emergency → closer', () => {
it('moderator: reviewer rejected + emergency → END', () => {
const wf = createTddCodingWorkflow();
expect(
wf.moderator(
@@ -329,12 +328,7 @@ describe('coding-tdd WorkflowType', () => {
'x',
1,
),
).toBe('closer');
});
it('moderator: closer → END', () => {
const wf = createTddCodingWorkflow();
expect(wf.moderator({ role: 'closer', meta: {} }, 'x')).toBe(END);
).toBe(END);
});
it('loop: test-reviewer rejects once then approves', async () => {
@@ -370,7 +364,6 @@ describe('coding-tdd WorkflowType', () => {
'test-reviewer',
]);
expect(roles).toContain('test-coder');
expect(roles).toContain('closer');
} finally {
await cleanup();
}
@@ -415,7 +408,6 @@ describe('coding-tdd WorkflowType', () => {
const secondCoder = roles.indexOf('coder', firstCoder + 1);
expect(firstAuto).toBeGreaterThan(firstCoder);
expect(secondCoder).toBeGreaterThan(firstAuto);
expect(roles).toContain('closer');
} finally {
await cleanup();
}
@@ -452,7 +444,6 @@ describe('coding-tdd WorkflowType', () => {
expect(roles.lastIndexOf('coder')).toBeGreaterThan(
roles.indexOf('manual-tester'),
);
expect(roles).toContain('closer');
} finally {
await cleanup();
}
@@ -495,7 +486,6 @@ describe('coding-tdd WorkflowType', () => {
}
expect(roles.filter((r) => r === 'reviewer').length).toBe(2);
expect(roles).toContain('closer');
} finally {
await cleanup();
}
@@ -1,5 +1,5 @@
/**
* TDD-driven coding workflow — test-planner → … → closer.
* TDD-driven coding workflow — test-planner → … → reviewer → END.
*
* Pure roles + START/END automaton. Trigger: coding-tdd.__start__
*
@@ -57,8 +57,6 @@ export type TddReviewerMeta = {
testQuality: string;
};
export type TddCloserMeta = Record<string, never>;
// ── Roles record ───────────────────────────────────────────────
export type TddCodingRoles = {
@@ -69,7 +67,7 @@ export type TddCodingRoles = {
'auto-tester': Role<AutoTesterMeta>;
'manual-tester': Role<ManualTesterMeta>;
reviewer: Role<TddReviewerMeta>;
closer: Role<TddCloserMeta>;
};
// ── Default mock implementations ────────────────────────────────
@@ -138,15 +136,6 @@ const defaultTddReviewer: Role<TddReviewerMeta> = async () => ({
},
});
const defaultCloser: Role<TddCloserMeta> = async (chain) => {
const startMsg = chain.find((m) => m.role === '__start__');
const title = (startMsg?.meta as { title?: string } | null)?.title ?? 'task';
return {
content: `[mock] TDD workflow report: ${title}`,
meta: {},
};
};
// ── Moderator ──────────────────────────────────────────────────
type TddCodingInput = ModeratorInput<TddCodingRoles>;
@@ -167,7 +156,7 @@ function tddCodingModerator(
const verdict = output.meta?.verdict;
if (verdict === 'approved') return 'test-coder';
if (verdict === 'rejected') {
if (emergency) return 'closer';
if (emergency) return END;
return 'test-planner';
}
return 'test-planner';
@@ -178,21 +167,19 @@ function tddCodingModerator(
return 'auto-tester';
case 'auto-tester': {
if (output.meta?.pass) return 'manual-tester';
if (emergency) return 'closer';
if (emergency) return END;
return 'coder';
}
case 'manual-tester': {
if (output.meta?.pass) return 'reviewer';
if (emergency) return 'closer';
if (emergency) return END;
return 'coder';
}
case 'reviewer': {
if (emergency) return 'closer';
if (output.meta?.verdict === 'approved') return 'closer';
if (output.meta?.verdict === 'approved') return END;
if (emergency) return END;
return 'coder';
}
case 'closer':
return END;
default:
return END;
}
@@ -206,7 +193,7 @@ export type CreateTddCodingWorkflowOpts = {
autoTesterFn?: Role<AutoTesterMeta>;
manualTesterFn?: Role<ManualTesterMeta>;
reviewerFn?: Role<TddReviewerMeta>;
closerFn?: Role<TddCloserMeta>;
};
// ── Factory ────────────────────────────────────────────────────
@@ -224,7 +211,6 @@ export function createTddCodingWorkflow(
'auto-tester': opts?.autoTesterFn ?? defaultAutoTester,
'manual-tester': opts?.manualTesterFn ?? defaultManualTester,
reviewer: opts?.reviewerFn ?? defaultTddReviewer,
closer: opts?.closerFn ?? defaultCloser,
},
moderator: tddCodingModerator,
limits: { maxRounds: 25 },
+1 -3
View File
@@ -1,7 +1,7 @@
/**
* CodingTask WorkflowType — pure roles + START/END automaton.
*
* Roles: architect → coder → reviewer → closer
* Roles: architect → coder → reviewer
* Trigger: coding.__start__ (external)
* Each role returns { content, meta } — adapter writes events.
*
@@ -22,12 +22,10 @@ export type ReviewerMeta = {
rejectionReason: string[];
retryCount: number;
};
export type CloserMeta = null;
export type CodingRoles = {
architect: Role<ArchitectMeta>;
coder: Role<CoderMeta>;
reviewer: Role<ReviewerMeta>;
closer: Role<CloserMeta>;
};
export declare function createCodingWorkflow(opts?: {
architectFn?: Role<ArchitectMeta>;
@@ -1,7 +1,7 @@
/**
* CodingTask WorkflowType — pure roles + START/END automaton.
*
* Roles: architect → coder → reviewer → closer
* Roles: architect → coder → reviewer
* Trigger: coding.__start__ (external)
* Each role returns { content, meta } — adapter writes events.
*
@@ -43,14 +43,7 @@ const defaultReviewer = async (chain) => {
meta: { verdict: 'approved', rejectionReason: [], retryCount },
};
};
const defaultCloser = async (chain) => {
const startMsg = chain.find((m) => m.role === '__start__');
const title = startMsg?.meta?.title ?? 'unknown';
return {
content: `Completed: ${title}`,
meta: null,
};
};
function codingModerator(output, _topicId, remainingRounds) {
if (output.role === START)
return 'architect';
@@ -61,13 +54,12 @@ function codingModerator(output, _topicId, remainingRounds) {
return 'reviewer';
case 'reviewer': {
if (remainingRounds !== undefined && remainingRounds <= 1)
return 'closer';
return END;
const rejected = output.meta?.verdict === 'rejected';
const retryCount = output.meta?.retryCount ?? 0;
return rejected && retryCount < 3 ? 'coder' : 'closer';
return rejected && retryCount < 3 ? 'coder' : END;
}
case 'closer':
return END;
}
return END;
}
@@ -79,8 +71,7 @@ export function createCodingWorkflow(opts) {
architect: opts?.architectFn ?? defaultArchitect,
coder: opts?.coderFn ?? defaultCoder,
reviewer: opts?.reviewerFn ?? defaultReviewer,
closer: defaultCloser,
},
},
moderator: codingModerator,
limits: { maxRounds: 15 },
};
@@ -10,7 +10,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createStore, type PulseStore } from '@uncaged/pulse';
import { createCodingWorkflow } from './coding.js';
import { createWorkflowRule } from '@uncaged/pulse';
import { createWorkflowRule, END } from '@uncaged/pulse';
describe('CodingTask WorkflowType', () => {
let store: PulseStore;
@@ -47,7 +47,7 @@ describe('CodingTask WorkflowType', () => {
});
}
it('full lifecycle: START → architect → coder → reviewer → closer → END', async () => {
it('full lifecycle: START → architect → coder → reviewer → END', async () => {
setup();
const codingTask = createCodingWorkflow();
const rule = createWorkflowRule(codingTask, store, undefined, {
@@ -78,10 +78,7 @@ describe('CodingTask WorkflowType', () => {
]);
const r4 = await rule.tick();
expect(r4.executed).toMatchObject([{ topicId: 'task-1', role: 'closer' }]);
const r5 = await rule.tick();
expect(r5.executed).toEqual([]);
expect(r4.executed).toEqual([]);
});
it('rejection triggers re-coding with rejectionReason', async () => {
@@ -129,13 +126,10 @@ describe('CodingTask WorkflowType', () => {
await rule.tick(); // reviewer (approves, retryCount=1)
const r6 = await rule.tick();
expect(r6.executed).toMatchObject([{ topicId: 'task-2', role: 'closer' }]);
const r7 = await rule.tick();
expect(r7.executed).toEqual([]);
expect(r6.executed).toEqual([]);
});
it('retryCount >= 3 forces transition to closer even if rejected', async () => {
it('retryCount >= 3 forces END even if rejected', async () => {
setup();
let reviewRound = 0;
@@ -175,15 +169,10 @@ describe('CodingTask WorkflowType', () => {
await rule.tick(); // reviewer rejects, retryCount=2
await rule.tick(); // coder (round 4 — last retry)
await rule.tick(); // reviewer rejects, retryCount=3 → forces closer
await rule.tick(); // reviewer rejects, retryCount=3 → forces END
const r = await rule.tick();
expect(r.executed).toMatchObject([
{ topicId: 'task-retry', role: 'closer' },
]);
const rEnd = await rule.tick();
expect(rEnd.executed).toEqual([]);
expect(r.executed).toEqual([]);
});
it('adapter writes events, not roles (CAS content is string)', async () => {
@@ -250,7 +239,7 @@ describe('CodingTask WorkflowType', () => {
expect(wf.limits?.maxRounds).toBe(15);
});
it('moderator: remainingRounds <= 1 forces closer even if reviewer rejected', () => {
it('moderator: remainingRounds <= 1 forces END even if reviewer rejected', () => {
const wf = createCodingWorkflow();
const result = wf.moderator(
{
@@ -260,7 +249,7 @@ describe('CodingTask WorkflowType', () => {
'topic-1',
1, // remainingRounds = 1, 紧急收敛
);
expect(result).toBe('closer');
expect(result).toBe(END);
});
it('moderator: remainingRounds > 1 keeps normal retry logic', () => {
@@ -1,7 +1,7 @@
/**
* CodingTask WorkflowType — pure roles + START/END automaton.
*
* Roles: architect → coder → reviewer → closer
* Roles: architect → coder → reviewer
* Trigger: coding.__start__ (external)
* Each role returns { content, meta } — adapter writes events.
*
@@ -29,15 +29,13 @@ export type ReviewerMeta = {
rejectionReason: string[];
retryCount: number;
};
export type CloserMeta = null;
// ── Roles record ───────────────────────────────────────────────
export type CodingRoles = {
architect: Role<ArchitectMeta>;
coder: Role<CoderMeta>;
reviewer: Role<ReviewerMeta>;
closer: Role<CloserMeta>;
};
// ── Default mock implementations ───────────────────────────────
@@ -79,15 +77,6 @@ const defaultReviewer: Role<ReviewerMeta> = async (chain) => {
};
};
const defaultCloser: Role<CloserMeta> = async (chain) => {
const startMsg = chain.find((m) => m.role === '__start__');
const title = startMsg?.meta?.title ?? 'unknown';
return {
content: `Completed: ${title}`,
meta: null,
};
};
// ── Moderator (type-safe automaton) ────────────────────────────
type CodingInput = ModeratorInput<CodingRoles>;
@@ -105,13 +94,12 @@ function codingModerator(
return 'reviewer';
case 'reviewer': {
if (remainingRounds !== undefined && remainingRounds <= 1)
return 'closer';
return END;
const rejected = output.meta?.verdict === 'rejected';
const retryCount = output.meta?.retryCount ?? 0;
return rejected && retryCount < 3 ? 'coder' : 'closer';
return rejected && retryCount < 3 ? 'coder' : END;
}
case 'closer':
return END;
}
return END;
}
@@ -129,7 +117,7 @@ export function createCodingWorkflow(opts?: {
architect: opts?.architectFn ?? defaultArchitect,
coder: opts?.coderFn ?? defaultCoder,
reviewer: opts?.reviewerFn ?? defaultReviewer,
closer: defaultCloser,
},
moderator: codingModerator,
limits: { maxRounds: 15 },
@@ -44,7 +44,6 @@ describe('Report Workflow', () => {
durationMs: 4000,
meta: { verdict: 'approved' },
},
{ id: 5, role: 'closer', offsetMs: 10000, durationMs: 1000 },
],
});
const hash = await store.putObject(timeline);