Refactor setup and tool helpers for improved modularity and clarity
- Moved setup logic into dedicated files: agent.ts, config.ts, index.ts, workspace.ts. - Introduced tool-helpers.ts for shared functions across tools, reducing boilerplate. - Updated tools (status, task-comment, task-create, task-update, work-finish, work-start) to utilize new helper functions for workspace resolution and provider creation. - Enhanced error handling and context detection in tools. - Improved project resolution logic to streamline tool execution. - Added new functionality for agent creation and configuration management in setup.
This commit is contained in:
118
lib/projects.ts
118
lib/projects.ts
@@ -11,7 +11,7 @@ export type WorkerState = {
|
||||
active: boolean;
|
||||
issueId: string | null;
|
||||
startTime: string | null;
|
||||
model: string | null;
|
||||
tier: string | null;
|
||||
sessions: Record<string, string | null>;
|
||||
};
|
||||
|
||||
@@ -39,20 +39,22 @@ export type ProjectsData = {
|
||||
/**
|
||||
* Migrate old WorkerState schema to current format.
|
||||
*
|
||||
* Handles two migrations:
|
||||
* Handles three migrations:
|
||||
* 1. Old sessionId field → sessions map (pre-sessions era)
|
||||
* 2. Model-alias session keys → tier-name keys (haiku→junior, sonnet→medior, etc.)
|
||||
* 3. Old "model" field name → "tier" field name
|
||||
*/
|
||||
function migrateWorkerState(worker: Record<string, unknown>): WorkerState {
|
||||
// Read tier from either "tier" (new) or "model" (old) field
|
||||
const rawTier = (worker.tier ?? worker.model) as string | null;
|
||||
|
||||
// Migration 1: old sessionId field → sessions map
|
||||
if (!worker.sessions || typeof worker.sessions !== "object") {
|
||||
const sessionId = worker.sessionId as string | null;
|
||||
const model = worker.model as string | null;
|
||||
const sessions: Record<string, string | null> = {};
|
||||
|
||||
if (sessionId && model) {
|
||||
// Apply tier migration to the model key too
|
||||
const tierKey = TIER_MIGRATION[model] ?? model;
|
||||
if (sessionId && rawTier) {
|
||||
const tierKey = TIER_MIGRATION[rawTier] ?? rawTier;
|
||||
sessions[tierKey] = sessionId;
|
||||
}
|
||||
|
||||
@@ -60,7 +62,7 @@ function migrateWorkerState(worker: Record<string, unknown>): WorkerState {
|
||||
active: worker.active as boolean,
|
||||
issueId: worker.issueId as string | null,
|
||||
startTime: worker.startTime as string | null,
|
||||
model: model ? (TIER_MIGRATION[model] ?? model) : null,
|
||||
tier: rawTier ? (TIER_MIGRATION[rawTier] ?? rawTier) : null,
|
||||
sessions,
|
||||
};
|
||||
}
|
||||
@@ -75,17 +77,23 @@ function migrateWorkerState(worker: Record<string, unknown>): WorkerState {
|
||||
const newKey = TIER_MIGRATION[key] ?? key;
|
||||
newSessions[newKey] = value;
|
||||
}
|
||||
const model = worker.model as string | null;
|
||||
return {
|
||||
active: worker.active as boolean,
|
||||
issueId: worker.issueId as string | null,
|
||||
startTime: worker.startTime as string | null,
|
||||
model: model ? (TIER_MIGRATION[model] ?? model) : null,
|
||||
tier: rawTier ? (TIER_MIGRATION[rawTier] ?? rawTier) : null,
|
||||
sessions: newSessions,
|
||||
};
|
||||
}
|
||||
|
||||
return worker as unknown as WorkerState;
|
||||
// Migration 3: "model" field → "tier" field (already handled by rawTier above)
|
||||
return {
|
||||
active: worker.active as boolean,
|
||||
issueId: worker.issueId as string | null,
|
||||
startTime: worker.startTime as string | null,
|
||||
tier: rawTier ? (TIER_MIGRATION[rawTier] ?? rawTier) : null,
|
||||
sessions: oldSessions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,14 +101,14 @@ function migrateWorkerState(worker: Record<string, unknown>): WorkerState {
|
||||
*/
|
||||
export function emptyWorkerState(tiers: string[]): WorkerState {
|
||||
const sessions: Record<string, string | null> = {};
|
||||
for (const tier of tiers) {
|
||||
sessions[tier] = null;
|
||||
for (const t of tiers) {
|
||||
sessions[t] = null;
|
||||
}
|
||||
return {
|
||||
active: false,
|
||||
issueId: null,
|
||||
startTime: null,
|
||||
model: null,
|
||||
tier: null,
|
||||
sessions,
|
||||
};
|
||||
}
|
||||
@@ -108,7 +116,7 @@ export function emptyWorkerState(tiers: string[]): WorkerState {
|
||||
/**
|
||||
* Get session key for a specific tier from a worker's sessions map.
|
||||
*/
|
||||
export function getSessionForModel(
|
||||
export function getSessionForTier(
|
||||
worker: WorkerState,
|
||||
tier: string,
|
||||
): string | null {
|
||||
@@ -123,7 +131,6 @@ export async function readProjects(workspaceDir: string): Promise<ProjectsData>
|
||||
const raw = await fs.readFile(projectsPath(workspaceDir), "utf-8");
|
||||
const data = JSON.parse(raw) as ProjectsData;
|
||||
|
||||
// Migrate any old-schema or missing fields transparently
|
||||
for (const project of Object.values(data.projects)) {
|
||||
project.dev = project.dev
|
||||
? migrateWorkerState(project.dev as unknown as Record<string, unknown>)
|
||||
@@ -144,7 +151,6 @@ export async function writeProjects(
|
||||
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);
|
||||
@@ -166,12 +172,7 @@ export function getWorker(
|
||||
|
||||
/**
|
||||
* Update worker state for a project. Only provided fields are updated.
|
||||
* This prevents accidentally nulling out fields that should be preserved.
|
||||
*
|
||||
* Session Preservation:
|
||||
* - If updates.sessions is provided, it's merged with existing sessions (new keys added/updated, existing keys preserved)
|
||||
* - If updates.sessions is NOT provided, existing sessions are preserved via spread operator
|
||||
* - Sessions should NEVER be accidentally cleared during state updates
|
||||
* Sessions are merged (not replaced) when both existing and new sessions are present.
|
||||
*/
|
||||
export async function updateWorker(
|
||||
workspaceDir: string,
|
||||
@@ -186,14 +187,11 @@ export async function updateWorker(
|
||||
}
|
||||
|
||||
const worker = project[role];
|
||||
|
||||
// Merge sessions maps if both exist
|
||||
// This ensures we preserve existing sessions while adding/updating new ones
|
||||
|
||||
if (updates.sessions && worker.sessions) {
|
||||
updates.sessions = { ...worker.sessions, ...updates.sessions };
|
||||
}
|
||||
|
||||
// Spread worker first, then updates - this preserves any fields not in updates
|
||||
|
||||
project[role] = { ...worker, ...updates };
|
||||
|
||||
await writeProjects(workspaceDir, data);
|
||||
@@ -202,22 +200,7 @@ export async function updateWorker(
|
||||
|
||||
/**
|
||||
* Mark a worker as active with a new task.
|
||||
* Sets active=true, issueId, model (tier). Stores session key in sessions[tier].
|
||||
*
|
||||
* Session Handling:
|
||||
* - If sessionKey is provided: new session spawned, stored in sessions[model]
|
||||
* - If sessionKey is omitted: existing session reused (sessions map preserved)
|
||||
* - Other tier sessions in the sessions map are ALWAYS preserved
|
||||
*
|
||||
* Example flow:
|
||||
* 1. First senior task: activateWorker({model: "senior", sessionKey: "abc"})
|
||||
* → sessions = {junior: null, medior: null, senior: "abc"}
|
||||
* 2. Task completes: deactivateWorker()
|
||||
* → sessions = {junior: null, medior: null, senior: "abc"} (preserved!)
|
||||
* 3. Next senior task: activateWorker({model: "senior"}) [no sessionKey]
|
||||
* → sessions = {junior: null, medior: null, senior: "abc"} (reused!)
|
||||
* 4. Medior task: activateWorker({model: "medior", sessionKey: "xyz"})
|
||||
* → sessions = {junior: null, medior: "xyz", senior: "abc"} (both preserved!)
|
||||
* Stores session key in sessions[tier] when a new session is spawned.
|
||||
*/
|
||||
export async function activateWorker(
|
||||
workspaceDir: string,
|
||||
@@ -225,7 +208,7 @@ export async function activateWorker(
|
||||
role: "dev" | "qa",
|
||||
params: {
|
||||
issueId: string;
|
||||
model: string;
|
||||
tier: string;
|
||||
sessionKey?: string;
|
||||
startTime?: string;
|
||||
},
|
||||
@@ -233,12 +216,10 @@ export async function activateWorker(
|
||||
const updates: Partial<WorkerState> = {
|
||||
active: true,
|
||||
issueId: params.issueId,
|
||||
model: params.model,
|
||||
tier: params.tier,
|
||||
};
|
||||
// Store session key in the sessions map for this tier (if new spawn)
|
||||
// If sessionKey is omitted, existing sessions are preserved via updateWorker
|
||||
if (params.sessionKey !== undefined) {
|
||||
updates.sessions = { [params.model]: params.sessionKey };
|
||||
updates.sessions = { [params.tier]: params.sessionKey };
|
||||
}
|
||||
if (params.startTime !== undefined) {
|
||||
updates.startTime = params.startTime;
|
||||
@@ -248,58 +229,21 @@ export async function activateWorker(
|
||||
|
||||
/**
|
||||
* Mark a worker as inactive after task completion.
|
||||
* Clears issueId and active, PRESERVES sessions map, model, startTime for reuse.
|
||||
*
|
||||
* IMPORTANT: This function MUST preserve the sessions map to enable session reuse
|
||||
* across multiple tasks of the same tier. Do NOT pass `sessions` in the updates
|
||||
* object, as this would overwrite the existing sessions.
|
||||
* Preserves sessions map and tier for reuse via updateWorker's spread.
|
||||
*/
|
||||
export async function deactivateWorker(
|
||||
workspaceDir: string,
|
||||
groupId: string,
|
||||
role: "dev" | "qa",
|
||||
): Promise<ProjectsData> {
|
||||
// Read current state to verify sessions will be preserved
|
||||
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];
|
||||
const sessionsBefore = worker.sessions;
|
||||
|
||||
// Update worker state (active=false, issueId=null)
|
||||
// Sessions are preserved via spread operator in updateWorker
|
||||
const result = await updateWorker(workspaceDir, groupId, role, {
|
||||
return updateWorker(workspaceDir, groupId, role, {
|
||||
active: false,
|
||||
issueId: null,
|
||||
// Explicitly DO NOT set sessions here to preserve them
|
||||
});
|
||||
|
||||
// Defensive verification: ensure sessions were not accidentally cleared
|
||||
const updatedWorker = result.projects[groupId][role];
|
||||
const sessionsAfter = updatedWorker.sessions;
|
||||
|
||||
// Verify sessions map was preserved
|
||||
if (sessionsBefore && sessionsAfter) {
|
||||
for (const [tier, sessionKey] of Object.entries(sessionsBefore)) {
|
||||
if (sessionKey !== null && sessionsAfter[tier] !== sessionKey) {
|
||||
throw new Error(
|
||||
`BUG: Session for tier "${tier}" was lost during deactivateWorker! ` +
|
||||
`Before: ${sessionKey}, After: ${sessionsAfter[tier]}. ` +
|
||||
`This should never happen - sessions must persist for reuse.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve repo path from projects.json repo field (handles ~/ expansion).
|
||||
* Uses os.homedir() for cross-platform home directory resolution.
|
||||
*/
|
||||
export function resolveRepoPath(repoField: string): string {
|
||||
if (repoField.startsWith("~/")) {
|
||||
|
||||
Reference in New Issue
Block a user