refactor: migrate role handling from tiers to roles module

- Removed the deprecated tiers.ts file and migrated all related functionality to roles/index.js.
- Updated tests and tools to reflect the new role structure, replacing references to "dev", "qa", and "architect" with "developer", "tester", and "architect".
- Adjusted workflow configurations and state management to accommodate the new role naming conventions.
- Enhanced project registration and health check tools to support dynamic role handling.
- Updated task creation, update, and completion processes to align with the new role definitions.
- Improved documentation and comments to clarify role responsibilities and usage.
This commit is contained in:
Lauren ten Hoor
2026-02-15 18:32:10 +08:00
parent 6a99752e5f
commit 0e24a68882
44 changed files with 1162 additions and 762 deletions

View File

@@ -18,7 +18,6 @@ import { log as auditLog } from "../audit.js";
import { checkWorkerHealth, scanOrphanedLabels, fetchGatewaySessions, type SessionLookup } from "./health.js";
import { projectTick } from "./tick.js";
import { createProvider } from "../providers/index.js";
import { getAllRoleIds } from "../roles/index.js";
// ---------------------------------------------------------------------------
// Types
@@ -307,13 +306,13 @@ async function performHealthPass(
const { provider } = await createProvider({ repo: project.repo, provider: project.provider });
let fixedCount = 0;
for (const role of getAllRoleIds()) {
for (const role of Object.keys(project.workers)) {
// Check worker health (session liveness, label consistency, etc)
const healthFixes = await checkWorkerHealth({
workspaceDir,
groupId,
project,
role: role as any,
role,
sessions,
autoFix: true,
provider,
@@ -325,7 +324,7 @@ async function performHealthPass(
workspaceDir,
groupId,
project,
role: role as any,
role,
autoFix: true,
provider,
});
@@ -336,10 +335,10 @@ async function performHealthPass(
}
/**
* Check if a project has active work (dev or qa).
* Check if a project has any active worker.
*/
async function checkProjectActive(workspaceDir: string, groupId: string): Promise<boolean> {
const fresh = (await readProjects(workspaceDir)).projects[groupId];
if (!fresh) return false;
return fresh.dev.active || fresh.qa.active;
return Object.values(fresh.workers).some(w => w.active);
}

View File

@@ -17,40 +17,6 @@ import {
type WorkflowConfig,
} from "../workflow.js";
// ---------------------------------------------------------------------------
// Backward compatibility exports
// ---------------------------------------------------------------------------
/**
* @deprecated Use getCompletionRule() from workflow.ts instead.
* Kept for backward compatibility.
*/
export const COMPLETION_RULES: Record<string, CompletionRule> = {
"dev:done": { from: "Doing", to: "To Test", gitPull: true, detectPr: true },
"qa:pass": { from: "Testing", to: "Done", closeIssue: true },
"qa:fail": { from: "Testing", to: "To Improve", reopenIssue: true },
"qa:refine": { from: "Testing", to: "Refining" },
"dev:blocked": { from: "Doing", to: "Refining" },
"qa:blocked": { from: "Testing", to: "Refining" },
"architect:done": { from: "Designing", to: "Planning" },
"architect:blocked": { from: "Designing", to: "Refining" },
};
/**
* @deprecated Use getNextStateDescription() from workflow.ts instead.
*/
export const NEXT_STATE: Record<string, string> = {
"dev:done": "QA queue",
"dev:blocked": "moved to Refining - needs human input",
"qa:pass": "Done!",
"qa:fail": "back to DEV",
"qa:refine": "awaiting human decision",
"qa:blocked": "moved to Refining - needs human input",
"architect:done": "Planning — ready for review",
"architect:blocked": "moved to Refining - needs clarification",
};
// Re-export CompletionRule type for backward compatibility
export type { CompletionRule };
export type CompletionOutput = {
@@ -72,7 +38,7 @@ export function getRule(
result: string,
workflow: WorkflowConfig = DEFAULT_WORKFLOW,
): CompletionRule | undefined {
return getCompletionRule(workflow, role as "dev" | "qa", result) ?? undefined;
return getCompletionRule(workflow, role, result) ?? undefined;
}
/**
@@ -81,7 +47,7 @@ export function getRule(
export async function executeCompletion(opts: {
workspaceDir: string;
groupId: string;
role: "dev" | "qa" | "architect";
role: string;
result: string;
issueId: number;
summary?: string;

View File

@@ -13,26 +13,6 @@ import {
type Role,
} from "../workflow.js";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/**
* @deprecated Use string labels from workflow config instead.
* Kept for backward compatibility.
*/
export type QueueLabel = "To Improve" | "To Test" | "To Do";
/**
* @deprecated Use getQueuePriority() instead.
* Kept for backward compatibility.
*/
export const QUEUE_PRIORITY: Record<string, number> = {
"To Improve": 3,
"To Test": 2,
"To Do": 1,
};
// ---------------------------------------------------------------------------
// Workflow-driven helpers
// ---------------------------------------------------------------------------

View File

@@ -11,9 +11,9 @@ import { createProvider } from "../providers/index.js";
import { selectLevel } from "../model-selector.js";
import { getWorker, getSessionForLevel, readProjects } from "../projects.js";
import { dispatchTask } from "../dispatch.js";
import { getAllRoleIds, getLevelsForRole, getAllLevels, roleForLevel } from "../roles/index.js";
import { getLevelsForRole, getAllLevels, roleForLevel } from "../roles/index.js";
import { loadConfig } from "../config/index.js";
import {
DEFAULT_WORKFLOW,
getQueueLabels,
getAllQueueLabels,
getActiveLabel,
@@ -22,25 +22,6 @@ import {
type Role,
} from "../workflow.js";
// ---------------------------------------------------------------------------
// Backward compatibility exports (deprecated)
// ---------------------------------------------------------------------------
/**
* @deprecated Use getQueueLabels(workflow, "dev") instead.
*/
export const DEV_LABELS: StateLabel[] = getQueueLabels(DEFAULT_WORKFLOW, "dev");
/**
* @deprecated Use getQueueLabels(workflow, "qa") instead.
*/
export const QA_LABELS: StateLabel[] = getQueueLabels(DEFAULT_WORKFLOW, "qa");
/**
* @deprecated Use getAllQueueLabels(workflow) instead.
*/
export const PRIORITY_ORDER: StateLabel[] = getAllQueueLabels(DEFAULT_WORKFLOW);
// ---------------------------------------------------------------------------
// Shared helpers (used by tick, work-start, auto-pickup)
// ---------------------------------------------------------------------------
@@ -68,7 +49,7 @@ export function detectLevelFromLabels(labels: string[]): string | null {
*/
export function detectRoleFromLabel(
label: StateLabel,
workflow: WorkflowConfig = DEFAULT_WORKFLOW,
workflow: WorkflowConfig,
): Role | null {
return workflowDetectRole(workflow, label);
}
@@ -76,7 +57,7 @@ export function detectRoleFromLabel(
export async function findNextIssueForRole(
provider: Pick<IssueProvider, "listIssuesByLabel">,
role: Role,
workflow: WorkflowConfig = DEFAULT_WORKFLOW,
workflow: WorkflowConfig,
): Promise<{ issue: Issue; label: StateLabel } | null> {
const labels = getQueueLabels(workflow, role);
for (const label of labels) {
@@ -93,8 +74,8 @@ export async function findNextIssueForRole(
*/
export async function findNextIssue(
provider: Pick<IssueProvider, "listIssuesByLabel">,
role?: Role,
workflow: WorkflowConfig = DEFAULT_WORKFLOW,
role: Role | undefined,
workflow: WorkflowConfig,
): Promise<{ issue: Issue; label: StateLabel } | null> {
const labels = role
? getQueueLabels(workflow, role)
@@ -156,15 +137,20 @@ export async function projectTick(opts: {
const {
workspaceDir, groupId, agentId, sessionKey, pluginConfig, dryRun,
maxPickups, targetRole, runtime,
workflow = DEFAULT_WORKFLOW,
} = opts;
const project = (await readProjects(workspaceDir)).projects[groupId];
if (!project) return { pickups: [], skipped: [{ reason: `Project not found: ${groupId}` }] };
const resolvedConfig = await loadConfig(workspaceDir, project.name);
const workflow = opts.workflow ?? resolvedConfig.workflow;
const provider = opts.provider ?? (await createProvider({ repo: project.repo, provider: project.provider })).provider;
const roleExecution = project.roleExecution ?? "parallel";
const roles: Role[] = targetRole ? [targetRole] : getAllRoleIds() as Role[];
const enabledRoles = Object.entries(resolvedConfig.roles)
.filter(([, r]) => r.enabled)
.map(([id]) => id);
const roles: Role[] = targetRole ? [targetRole] : enabledRoles;
const pickups: TickAction[] = [];
const skipped: TickResult["skipped"] = [];
@@ -186,8 +172,8 @@ export async function projectTick(opts: {
continue;
}
// Check sequential role execution: any other role must be inactive
const otherRoles = getAllRoleIds().filter(r => r !== role);
if (roleExecution === "sequential" && otherRoles.some(r => getWorker(fresh, r as any).active)) {
const otherRoles = enabledRoles.filter((r: string) => r !== role);
if (roleExecution === "sequential" && otherRoles.some((r: string) => getWorker(fresh, r).active)) {
skipped.push({ role, reason: "Sequential: other role active" });
continue;
}