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

@@ -9,16 +9,13 @@
*
* All workflow behavior is derived from this config — no hardcoded state names.
*/
import fs from "node:fs/promises";
import path from "node:path";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type StateType = "queue" | "active" | "hold" | "terminal";
/** @deprecated Use WorkerRole from lib/roles/ */
export type Role = "dev" | "qa" | "architect";
/** Role identifier. Built-in: "developer", "tester", "architect". Extensible via config. */
export type Role = string;
export type TransitionAction = "gitPull" | "detectPr" | "closeIssue" | "reopenIssue";
export type TransitionTarget = string | {
@@ -64,7 +61,7 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
},
todo: {
type: "queue",
role: "dev",
role: "developer",
label: "To Do",
color: "#428bca",
priority: 1,
@@ -72,7 +69,7 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
},
doing: {
type: "active",
role: "dev",
role: "developer",
label: "Doing",
color: "#f0ad4e",
on: {
@@ -82,7 +79,7 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
},
toTest: {
type: "queue",
role: "qa",
role: "tester",
label: "To Test",
color: "#5bc0de",
priority: 2,
@@ -90,7 +87,7 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
},
testing: {
type: "active",
role: "qa",
role: "tester",
label: "Testing",
color: "#9b59b6",
on: {
@@ -102,7 +99,7 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
},
toImprove: {
type: "queue",
role: "dev",
role: "developer",
label: "To Improve",
color: "#d9534f",
priority: 3,
@@ -146,38 +143,15 @@ export const DEFAULT_WORKFLOW: WorkflowConfig = {
/**
* Load workflow config for a project.
* Priority: project-specific → workspace default → built-in default
* Delegates to loadConfig() which handles the three-layer merge.
*/
export async function loadWorkflow(
workspaceDir: string,
_groupId?: string,
projectName?: string,
): Promise<WorkflowConfig> {
// TODO: Support per-project overrides from projects.json when needed
// For now, try workspace-level config, fall back to default
const workflowPath = path.join(workspaceDir, "projects", "workflow.json");
try {
const content = await fs.readFile(workflowPath, "utf-8");
const parsed = JSON.parse(content) as { workflow?: WorkflowConfig };
if (parsed.workflow) {
return mergeWorkflow(DEFAULT_WORKFLOW, parsed.workflow);
}
} catch {
// No custom workflow, use default
}
return DEFAULT_WORKFLOW;
}
/**
* Merge custom workflow config over defaults.
* Custom states are merged, not replaced entirely.
*/
function mergeWorkflow(base: WorkflowConfig, custom: Partial<WorkflowConfig>): WorkflowConfig {
return {
initial: custom.initial ?? base.initial,
states: { ...base.states, ...custom.states },
};
const { loadConfig } = await import("./config/loader.js");
const config = await loadConfig(workspaceDir, projectName);
return config.workflow;
}
// ---------------------------------------------------------------------------
@@ -305,31 +279,30 @@ export function findStateKeyByLabel(workflow: WorkflowConfig, label: string): st
// ---------------------------------------------------------------------------
/**
* Map role:result to completion event name.
* Map completion result to workflow transition event name.
* Convention: "done" → COMPLETE, others → uppercase.
*/
const RESULT_TO_EVENT: Record<string, string> = {
"dev:done": "COMPLETE",
"dev:blocked": "BLOCKED",
"qa:pass": "PASS",
"qa:fail": "FAIL",
"qa:refine": "REFINE",
"qa:blocked": "BLOCKED",
"architect:done": "COMPLETE",
"architect:blocked": "BLOCKED",
};
function resultToEvent(result: string): string {
if (result === "done") return "COMPLETE";
return result.toUpperCase();
}
/**
* Get completion rule for a role:result pair.
* Derives entirely from workflow transitions — no hardcoded role:result mapping.
*/
export function getCompletionRule(
workflow: WorkflowConfig,
role: Role,
result: string,
): CompletionRule | null {
const event = RESULT_TO_EVENT[`${role}:${result}`];
if (!event) return null;
const event = resultToEvent(result);
let activeLabel: string;
try {
activeLabel = getActiveLabel(workflow, role);
} catch { return null; }
const activeLabel = getActiveLabel(workflow, role);
const activeKey = findStateKeyByLabel(workflow, activeLabel);
if (!activeKey) return null;
@@ -356,6 +329,7 @@ export function getCompletionRule(
/**
* Get human-readable next state description.
* Derives from target state type — no hardcoded role names.
*/
export function getNextStateDescription(
workflow: WorkflowConfig,
@@ -365,15 +339,13 @@ export function getNextStateDescription(
const rule = getCompletionRule(workflow, role, result);
if (!rule) return "";
// Find the target state to determine the description
const targetState = findStateByLabel(workflow, rule.to);
if (!targetState) return "";
if (targetState.type === "terminal") return "Done!";
if (targetState.type === "hold") return "awaiting human decision";
if (targetState.type === "queue") {
if (targetState.role === "qa") return "QA queue";
if (targetState.role === "dev") return "back to DEV";
if (targetState.type === "queue" && targetState.role) {
return `${targetState.role.toUpperCase()} queue`;
}
return rule.to;
@@ -381,19 +353,18 @@ export function getNextStateDescription(
/**
* Get emoji for a completion result.
* Keyed by result name — role-independent.
*/
export function getCompletionEmoji(role: Role, result: string): string {
const map: Record<string, string> = {
"dev:done": "",
"qa:pass": "🎉",
"qa:fail": "",
"qa:refine": "🤔",
"dev:blocked": "🚫",
"qa:blocked": "🚫",
"architect:done": "🏗️",
"architect:blocked": "🚫",
};
return map[`${role}:${result}`] ?? "📋";
const RESULT_EMOJI: Record<string, string> = {
done: "✅",
pass: "🎉",
fail: "",
refine: "🤔",
blocked: "🚫",
};
export function getCompletionEmoji(_role: Role, result: string): string {
return RESULT_EMOJI[result] ?? "📋";
}
// ---------------------------------------------------------------------------