Initial commit: DevClaw OpenClaw plugin
Multi-project dev/qa pipeline orchestration with 4 agent tools: - task_pickup: atomic task pickup with model selection and session reuse - task_complete: DEV done, QA pass/fail/refine with label transitions - queue_status: task queue and worker status across projects - session_health: zombie detection and state consistency checks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
131
lib/projects.ts
Normal file
131
lib/projects.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Atomic projects.json read/write operations.
|
||||
* All state mutations go through this module to prevent corruption.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export type WorkerState = {
|
||||
active: boolean;
|
||||
sessionId: string | null;
|
||||
issueId: string | null;
|
||||
startTime: string | null;
|
||||
model: string | null;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
name: string;
|
||||
repo: string;
|
||||
groupName: string;
|
||||
deployUrl: string;
|
||||
baseBranch: string;
|
||||
deployBranch: string;
|
||||
dev: WorkerState;
|
||||
qa: WorkerState;
|
||||
};
|
||||
|
||||
export type ProjectsData = {
|
||||
projects: Record<string, Project>;
|
||||
};
|
||||
|
||||
function projectsPath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, "memory", "projects.json");
|
||||
}
|
||||
|
||||
export async function readProjects(workspaceDir: string): Promise<ProjectsData> {
|
||||
const raw = await fs.readFile(projectsPath(workspaceDir), "utf-8");
|
||||
return JSON.parse(raw) as ProjectsData;
|
||||
}
|
||||
|
||||
export async function writeProjects(
|
||||
workspaceDir: string,
|
||||
data: ProjectsData,
|
||||
): Promise<void> {
|
||||
const filePath = projectsPath(workspaceDir);
|
||||
// Write to temp file first, then rename for atomicity
|
||||
const tmpPath = filePath + ".tmp";
|
||||
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
||||
await fs.rename(tmpPath, filePath);
|
||||
}
|
||||
|
||||
export function getProject(
|
||||
data: ProjectsData,
|
||||
groupId: string,
|
||||
): Project | undefined {
|
||||
return data.projects[groupId];
|
||||
}
|
||||
|
||||
export function getWorker(
|
||||
project: Project,
|
||||
role: "dev" | "qa",
|
||||
): WorkerState {
|
||||
return project[role];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update worker state for a project. Only provided fields are updated.
|
||||
* This prevents accidentally nulling out fields that should be preserved.
|
||||
*/
|
||||
export async function updateWorker(
|
||||
workspaceDir: string,
|
||||
groupId: string,
|
||||
role: "dev" | "qa",
|
||||
updates: Partial<WorkerState>,
|
||||
): Promise<ProjectsData> {
|
||||
const data = await readProjects(workspaceDir);
|
||||
const project = data.projects[groupId];
|
||||
if (!project) {
|
||||
throw new Error(`Project not found for groupId: ${groupId}`);
|
||||
}
|
||||
|
||||
const worker = project[role];
|
||||
project[role] = { ...worker, ...updates };
|
||||
|
||||
await writeProjects(workspaceDir, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a worker as active with a new task.
|
||||
* Sets active=true, issueId, model. Preserves sessionId and startTime if reusing.
|
||||
*/
|
||||
export async function activateWorker(
|
||||
workspaceDir: string,
|
||||
groupId: string,
|
||||
role: "dev" | "qa",
|
||||
params: {
|
||||
issueId: string;
|
||||
model: string;
|
||||
sessionId?: string;
|
||||
startTime?: string;
|
||||
},
|
||||
): Promise<ProjectsData> {
|
||||
const updates: Partial<WorkerState> = {
|
||||
active: true,
|
||||
issueId: params.issueId,
|
||||
model: params.model,
|
||||
};
|
||||
// Only set sessionId and startTime if provided (new spawn)
|
||||
if (params.sessionId !== undefined) {
|
||||
updates.sessionId = params.sessionId;
|
||||
}
|
||||
if (params.startTime !== undefined) {
|
||||
updates.startTime = params.startTime;
|
||||
}
|
||||
return updateWorker(workspaceDir, groupId, role, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a worker as inactive after task completion.
|
||||
* Clears issueId and active, PRESERVES sessionId, model, startTime for reuse.
|
||||
*/
|
||||
export async function deactivateWorker(
|
||||
workspaceDir: string,
|
||||
groupId: string,
|
||||
role: "dev" | "qa",
|
||||
): Promise<ProjectsData> {
|
||||
return updateWorker(workspaceDir, groupId, role, {
|
||||
active: false,
|
||||
issueId: null,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user