mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-15 02:42:45 +08:00
Add orchestrator types and error handling configurations
- Introduced new TypeScript types for orchestrator functionality, including `SessionStrategy`, `ErrorHandlingStrategy`, and `OrchestrationStep`. - Defined interfaces for `OrchestrationPlan` and `ManualOrchestrationParams` to facilitate orchestration management. - Added a new PNG image file for visual representation. - Created a placeholder file named 'nul' for future use.
This commit is contained in:
@@ -20,6 +20,7 @@ interface TaskFlowControl {
|
||||
step: string;
|
||||
action: string;
|
||||
}>;
|
||||
target_files?: Array<{ path: string }>;
|
||||
}
|
||||
|
||||
interface NormalizedTask {
|
||||
@@ -777,18 +778,28 @@ function normalizeTask(task: unknown): NormalizedTask | null {
|
||||
acceptance: (context.acceptance as string[]) || [],
|
||||
depends_on: (context.depends_on as string[]) || []
|
||||
} : {
|
||||
requirements: (taskObj.requirements as string[]) || (taskObj.description ? [taskObj.description as string] : []),
|
||||
focus_paths: (taskObj.focus_paths as string[]) || modificationPoints?.map(m => m.file).filter((f): f is string => !!f) || [],
|
||||
requirements: (taskObj.requirements as string[])
|
||||
|| (taskObj.details as string[])
|
||||
|| (taskObj.description ? [taskObj.description as string] : taskObj.scope ? [taskObj.scope as string] : []),
|
||||
focus_paths: (taskObj.focus_paths as string[])
|
||||
|| (Array.isArray(taskObj.files) && taskObj.files.length > 0 && typeof taskObj.files[0] === 'string'
|
||||
? taskObj.files as string[] : undefined)
|
||||
|| modificationPoints?.map(m => m.file).filter((f): f is string => !!f)
|
||||
|| [],
|
||||
acceptance: (taskObj.acceptance as string[]) || [],
|
||||
depends_on: (taskObj.depends_on as string[]) || []
|
||||
},
|
||||
flow_control: flowControl ? {
|
||||
implementation_approach: (flowControl.implementation_approach as Array<{ step: string; action: string }>) || []
|
||||
implementation_approach: (flowControl.implementation_approach as Array<{ step: string; action: string }>) || [],
|
||||
target_files: (flowControl.target_files as Array<{ path: string }>) || undefined
|
||||
} : {
|
||||
implementation_approach: implementation?.map((step, i) => ({
|
||||
step: `Step ${i + 1}`,
|
||||
action: step as string
|
||||
})) || []
|
||||
})) || [],
|
||||
target_files: Array.isArray(taskObj.files) && taskObj.files.length > 0 && typeof taskObj.files[0] === 'string'
|
||||
? (taskObj.files as string[]).map(f => ({ path: f }))
|
||||
: undefined
|
||||
},
|
||||
// Keep all original fields for raw JSON view
|
||||
_raw: task
|
||||
|
||||
@@ -1,61 +1,149 @@
|
||||
/**
|
||||
* Team Routes - REST API for team message visualization
|
||||
* Team Routes - REST API for team message visualization & management
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/teams - List all teams
|
||||
* - GET /api/teams/:name/messages - Get messages (with filters)
|
||||
* - GET /api/teams/:name/status - Get member status summary
|
||||
* - GET /api/teams - List all teams (with ?location filter)
|
||||
* - GET /api/teams/:name/messages - Get messages (with filters)
|
||||
* - GET /api/teams/:name/status - Get member status summary
|
||||
* - POST /api/teams/:name/archive - Archive a team
|
||||
* - POST /api/teams/:name/unarchive - Unarchive a team
|
||||
* - DELETE /api/teams/:name - Delete a team
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { existsSync, readdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import type { RouteContext } from './types.js';
|
||||
import { readAllMessages, getLogDir } from '../../tools/team-msg.js';
|
||||
import { readAllMessages, getLogDir, getEffectiveTeamMeta, readTeamMeta, writeTeamMeta } from '../../tools/team-msg.js';
|
||||
import type { TeamMeta } from '../../tools/team-msg.js';
|
||||
import { getProjectRoot } from '../../utils/path-validator.js';
|
||||
|
||||
function jsonResponse(res: import('http').ServerResponse, status: number, data: unknown): void {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
export async function handleTeamRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, req, res, url } = ctx;
|
||||
const { pathname, req, res, url, handlePostRequest } = ctx;
|
||||
|
||||
if (!pathname.startsWith('/api/teams')) return false;
|
||||
if (req.method !== 'GET') return false;
|
||||
|
||||
// GET /api/teams - List all teams
|
||||
if (pathname === '/api/teams') {
|
||||
// ====== GET /api/teams - List all teams ======
|
||||
if (pathname === '/api/teams' && req.method === 'GET') {
|
||||
try {
|
||||
const root = getProjectRoot();
|
||||
const teamMsgDir = join(root, '.workflow', '.team-msg');
|
||||
|
||||
if (!existsSync(teamMsgDir)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ teams: [] }));
|
||||
jsonResponse(res, 200, { teams: [] });
|
||||
return true;
|
||||
}
|
||||
|
||||
const locationFilter = url.searchParams.get('location') || 'active';
|
||||
const entries = readdirSync(teamMsgDir, { withFileTypes: true });
|
||||
|
||||
const teams = entries
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => {
|
||||
const messages = readAllMessages(e.name);
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
const meta = getEffectiveTeamMeta(e.name);
|
||||
|
||||
// Count unique members from messages
|
||||
const memberSet = new Set<string>();
|
||||
for (const msg of messages) {
|
||||
memberSet.add(msg.from);
|
||||
memberSet.add(msg.to);
|
||||
}
|
||||
|
||||
return {
|
||||
name: e.name,
|
||||
messageCount: messages.length,
|
||||
lastActivity: lastMsg?.ts || '',
|
||||
status: meta.status,
|
||||
created_at: meta.created_at,
|
||||
updated_at: meta.updated_at,
|
||||
archived_at: meta.archived_at,
|
||||
pipeline_mode: meta.pipeline_mode,
|
||||
memberCount: memberSet.size,
|
||||
members: Array.from(memberSet),
|
||||
};
|
||||
})
|
||||
.filter(t => {
|
||||
if (locationFilter === 'all') return true;
|
||||
if (locationFilter === 'archived') return t.status === 'archived';
|
||||
// 'active' = everything that's not archived
|
||||
return t.status !== 'archived';
|
||||
})
|
||||
.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ teams }));
|
||||
jsonResponse(res, 200, { teams });
|
||||
return true;
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
jsonResponse(res, 500, { error: (error as Error).message });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Match /api/teams/:name/messages or /api/teams/:name/status
|
||||
// ====== POST /api/teams/:name/archive ======
|
||||
const archiveMatch = pathname.match(/^\/api\/teams\/([^/]+)\/archive$/);
|
||||
if (archiveMatch && req.method === 'POST') {
|
||||
const teamName = decodeURIComponent(archiveMatch[1]);
|
||||
handlePostRequest(req, res, async () => {
|
||||
const dir = getLogDir(teamName);
|
||||
if (!existsSync(dir)) {
|
||||
throw new Error(`Team "${teamName}" not found`);
|
||||
}
|
||||
const meta = getEffectiveTeamMeta(teamName);
|
||||
meta.status = 'archived';
|
||||
meta.archived_at = new Date().toISOString();
|
||||
meta.updated_at = new Date().toISOString();
|
||||
writeTeamMeta(teamName, meta);
|
||||
return { success: true, team: teamName, status: 'archived' };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ====== POST /api/teams/:name/unarchive ======
|
||||
const unarchiveMatch = pathname.match(/^\/api\/teams\/([^/]+)\/unarchive$/);
|
||||
if (unarchiveMatch && req.method === 'POST') {
|
||||
const teamName = decodeURIComponent(unarchiveMatch[1]);
|
||||
handlePostRequest(req, res, async () => {
|
||||
const dir = getLogDir(teamName);
|
||||
if (!existsSync(dir)) {
|
||||
throw new Error(`Team "${teamName}" not found`);
|
||||
}
|
||||
const meta = getEffectiveTeamMeta(teamName);
|
||||
meta.status = 'active';
|
||||
delete meta.archived_at;
|
||||
meta.updated_at = new Date().toISOString();
|
||||
writeTeamMeta(teamName, meta);
|
||||
return { success: true, team: teamName, status: 'active' };
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ====== DELETE /api/teams/:name ======
|
||||
const deleteMatch = pathname.match(/^\/api\/teams\/([^/]+)$/);
|
||||
if (deleteMatch && req.method === 'DELETE') {
|
||||
const teamName = decodeURIComponent(deleteMatch[1]);
|
||||
try {
|
||||
const dir = getLogDir(teamName);
|
||||
if (!existsSync(dir)) {
|
||||
jsonResponse(res, 404, { error: `Team "${teamName}" not found` });
|
||||
return true;
|
||||
}
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
jsonResponse(res, 200, { success: true, team: teamName, deleted: true });
|
||||
return true;
|
||||
} catch (error) {
|
||||
jsonResponse(res, 500, { error: (error as Error).message });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ====== GET /api/teams/:name/messages or /api/teams/:name/status ======
|
||||
if (req.method !== 'GET') return false;
|
||||
|
||||
const match = pathname.match(/^\/api\/teams\/([^/]+)\/(messages|status)$/);
|
||||
if (!match) return false;
|
||||
|
||||
@@ -81,12 +169,10 @@ export async function handleTeamRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const total = messages.length;
|
||||
const sliced = messages.slice(Math.max(0, total - last - offset), total - offset);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ total, showing: sliced.length, messages: sliced }));
|
||||
jsonResponse(res, 200, { total, showing: sliced.length, messages: sliced });
|
||||
return true;
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
jsonResponse(res, 500, { error: (error as Error).message });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -112,12 +198,10 @@ export async function handleTeamRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
|
||||
const members = Array.from(memberMap.values()).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ members, total_messages: messages.length }));
|
||||
jsonResponse(res, 200, { members, total_messages: messages.length });
|
||||
return true;
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
||||
jsonResponse(res, 500, { error: (error as Error).message });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,76 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, rmSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync, rmSync, statSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { getProjectRoot } from '../utils/path-validator.js';
|
||||
|
||||
// --- Team Metadata ---
|
||||
|
||||
export interface TeamMeta {
|
||||
status: 'active' | 'completed' | 'archived';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
archived_at?: string;
|
||||
pipeline_mode?: string;
|
||||
}
|
||||
|
||||
export function getMetaPath(team: string): string {
|
||||
return join(getLogDir(team), 'meta.json');
|
||||
}
|
||||
|
||||
export function readTeamMeta(team: string): TeamMeta | null {
|
||||
const metaPath = getMetaPath(team);
|
||||
if (!existsSync(metaPath)) return null;
|
||||
try {
|
||||
return JSON.parse(readFileSync(metaPath, 'utf-8')) as TeamMeta;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeTeamMeta(team: string, meta: TeamMeta): void {
|
||||
const dir = getLogDir(team);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(getMetaPath(team), JSON.stringify(meta, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer team status when no meta.json exists.
|
||||
* If last message is 'shutdown' → 'completed', otherwise 'active'.
|
||||
*/
|
||||
export function inferTeamStatus(team: string): TeamMeta['status'] {
|
||||
const messages = readAllMessages(team);
|
||||
if (messages.length === 0) return 'active';
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
return lastMsg.type === 'shutdown' ? 'completed' : 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective team meta: reads meta.json or infers from messages.
|
||||
*/
|
||||
export function getEffectiveTeamMeta(team: string): TeamMeta {
|
||||
const meta = readTeamMeta(team);
|
||||
if (meta) return meta;
|
||||
|
||||
// Infer from messages and directory stat
|
||||
const status = inferTeamStatus(team);
|
||||
const dir = getLogDir(team);
|
||||
let created_at = new Date().toISOString();
|
||||
try {
|
||||
const stat = statSync(dir);
|
||||
created_at = stat.birthtime.toISOString();
|
||||
} catch { /* use now as fallback */ }
|
||||
|
||||
const messages = readAllMessages(team);
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
const updated_at = lastMsg?.ts || created_at;
|
||||
|
||||
return { status, created_at, updated_at };
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
|
||||
export interface TeamMessage {
|
||||
|
||||
Reference in New Issue
Block a user