mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat(cli): 添加 --rule 选项支持模板自动发现
重构 ccw cli 模板系统: - 新增 template-discovery.ts 模块,支持扁平化模板自动发现 - 添加 --rule <template> 选项,自动加载 protocol 和 template - 模板目录从嵌套结构 (prompts/category/file.txt) 迁移到扁平结构 (prompts/category-function.txt) - 更新所有 agent/command 文件,使用 $PROTO $TMPL 环境变量替代 $(cat ...) 模式 - 支持模糊匹配:--rule 02-review-architecture 可匹配 analysis-review-architecture.txt 其他更新: - Dashboard: 添加 Claude Manager 和 Issue Manager 页面 - Codex-lens: 增强 chain_search 和 clustering 模块 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -811,6 +811,56 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Batch delete files
|
||||
if (pathname === '/api/memory/claude/batch-delete' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { paths, confirm } = body;
|
||||
|
||||
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
||||
return { error: 'paths array is required', status: 400 };
|
||||
}
|
||||
|
||||
if (confirm !== true) {
|
||||
return { error: 'Confirmation required', status: 400 };
|
||||
}
|
||||
|
||||
const results = {
|
||||
success: true,
|
||||
total: paths.length,
|
||||
deleted: 0,
|
||||
errors: [] as Array<{ path: string; error: string }>
|
||||
};
|
||||
|
||||
// Delete each file
|
||||
for (const filePath of paths) {
|
||||
const result = deleteClaudeFile(filePath);
|
||||
if (result.success) {
|
||||
results.deleted++;
|
||||
// Broadcast individual file deletion
|
||||
broadcastToClients({
|
||||
type: 'CLAUDE_FILE_DELETED',
|
||||
data: { path: filePath }
|
||||
});
|
||||
} else {
|
||||
results.errors.push({ path: filePath, error: result.error || 'Unknown error' });
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast batch deletion completion
|
||||
broadcastToClients({
|
||||
type: 'CLAUDE_BATCH_DELETED',
|
||||
data: {
|
||||
total: results.total,
|
||||
deleted: results.deleted,
|
||||
failed: results.errors.length
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Create file
|
||||
if (pathname === '/api/memory/claude/create' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
|
||||
@@ -79,6 +79,12 @@ function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[
|
||||
writeFileSync(join(solutionsDir, `${issueId}.jsonl`), solutions.map(s => JSON.stringify(s)).join('\n'));
|
||||
}
|
||||
|
||||
function generateQueueFileId(): string {
|
||||
const now = new Date();
|
||||
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
||||
return `QUE-${ts}`;
|
||||
}
|
||||
|
||||
function readQueue(issuesDir: string) {
|
||||
// Try new multi-queue structure first
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
@@ -718,6 +724,183 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/queue/split - Split items from source queue into a new queue
|
||||
if (pathname === '/api/queue/split' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body: any) => {
|
||||
const { sourceQueueId, itemIds } = body;
|
||||
if (!sourceQueueId || !itemIds || !Array.isArray(itemIds) || itemIds.length === 0) {
|
||||
return { error: 'sourceQueueId and itemIds (non-empty array) required' };
|
||||
}
|
||||
|
||||
const queuesDir = join(issuesDir, 'queues');
|
||||
const sourcePath = join(queuesDir, `${sourceQueueId}.json`);
|
||||
|
||||
if (!existsSync(sourcePath)) {
|
||||
return { error: `Source queue ${sourceQueueId} not found` };
|
||||
}
|
||||
|
||||
try {
|
||||
const sourceQueue = JSON.parse(readFileSync(sourcePath, 'utf8'));
|
||||
const sourceItems = sourceQueue.solutions || sourceQueue.tasks || [];
|
||||
const isSolutionBased = !!sourceQueue.solutions;
|
||||
|
||||
// Find items to split
|
||||
const itemsToSplit = sourceItems.filter((item: any) =>
|
||||
itemIds.includes(item.item_id) ||
|
||||
itemIds.includes(item.solution_id) ||
|
||||
itemIds.includes(item.task_id)
|
||||
);
|
||||
|
||||
if (itemsToSplit.length === 0) {
|
||||
return { error: 'No matching items found to split' };
|
||||
}
|
||||
|
||||
if (itemsToSplit.length === sourceItems.length) {
|
||||
return { error: 'Cannot split all items - at least one item must remain in source queue' };
|
||||
}
|
||||
|
||||
// Find remaining items
|
||||
const remainingItems = sourceItems.filter((item: any) =>
|
||||
!itemIds.includes(item.item_id) &&
|
||||
!itemIds.includes(item.solution_id) &&
|
||||
!itemIds.includes(item.task_id)
|
||||
);
|
||||
|
||||
// Create new queue with split items
|
||||
const newQueueId = generateQueueFileId();
|
||||
const newQueuePath = join(queuesDir, `${newQueueId}.json`);
|
||||
|
||||
// Re-index split items
|
||||
const reindexedSplitItems = itemsToSplit.map((item: any, idx: number) => ({
|
||||
...item,
|
||||
execution_order: idx + 1
|
||||
}));
|
||||
|
||||
// Extract issue IDs from split items
|
||||
const splitIssueIds = [...new Set(itemsToSplit.map((item: any) => item.issue_id).filter(Boolean))];
|
||||
|
||||
// Remaining issue IDs
|
||||
const remainingIssueIds = [...new Set(remainingItems.map((item: any) => item.issue_id).filter(Boolean))];
|
||||
|
||||
// Create new queue
|
||||
const newQueue: any = {
|
||||
id: newQueueId,
|
||||
status: 'active',
|
||||
issue_ids: splitIssueIds,
|
||||
conflicts: [],
|
||||
_metadata: {
|
||||
version: '2.1',
|
||||
updated_at: new Date().toISOString(),
|
||||
split_from: sourceQueueId,
|
||||
split_at: new Date().toISOString(),
|
||||
...(isSolutionBased
|
||||
? {
|
||||
total_solutions: reindexedSplitItems.length,
|
||||
completed_solutions: reindexedSplitItems.filter((i: any) => i.status === 'completed').length
|
||||
}
|
||||
: {
|
||||
total_tasks: reindexedSplitItems.length,
|
||||
completed_tasks: reindexedSplitItems.filter((i: any) => i.status === 'completed').length
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if (isSolutionBased) {
|
||||
newQueue.solutions = reindexedSplitItems;
|
||||
} else {
|
||||
newQueue.tasks = reindexedSplitItems;
|
||||
}
|
||||
|
||||
// Update source queue with remaining items
|
||||
const reindexedRemainingItems = remainingItems.map((item: any, idx: number) => ({
|
||||
...item,
|
||||
execution_order: idx + 1
|
||||
}));
|
||||
|
||||
if (isSolutionBased) {
|
||||
sourceQueue.solutions = reindexedRemainingItems;
|
||||
} else {
|
||||
sourceQueue.tasks = reindexedRemainingItems;
|
||||
}
|
||||
|
||||
sourceQueue.issue_ids = remainingIssueIds;
|
||||
sourceQueue._metadata = {
|
||||
...sourceQueue._metadata,
|
||||
updated_at: new Date().toISOString(),
|
||||
...(isSolutionBased
|
||||
? {
|
||||
total_solutions: reindexedRemainingItems.length,
|
||||
completed_solutions: reindexedRemainingItems.filter((i: any) => i.status === 'completed').length
|
||||
}
|
||||
: {
|
||||
total_tasks: reindexedRemainingItems.length,
|
||||
completed_tasks: reindexedRemainingItems.filter((i: any) => i.status === 'completed').length
|
||||
})
|
||||
};
|
||||
|
||||
// Write both queues
|
||||
writeFileSync(newQueuePath, JSON.stringify(newQueue, null, 2));
|
||||
writeFileSync(sourcePath, JSON.stringify(sourceQueue, null, 2));
|
||||
|
||||
// Update index
|
||||
const indexPath = join(queuesDir, 'index.json');
|
||||
if (existsSync(indexPath)) {
|
||||
try {
|
||||
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
||||
|
||||
// Add new queue to index
|
||||
const newQueueEntry: any = {
|
||||
id: newQueueId,
|
||||
status: 'active',
|
||||
issue_ids: splitIssueIds,
|
||||
created_at: new Date().toISOString(),
|
||||
...(isSolutionBased
|
||||
? {
|
||||
total_solutions: reindexedSplitItems.length,
|
||||
completed_solutions: reindexedSplitItems.filter((i: any) => i.status === 'completed').length
|
||||
}
|
||||
: {
|
||||
total_tasks: reindexedSplitItems.length,
|
||||
completed_tasks: reindexedSplitItems.filter((i: any) => i.status === 'completed').length
|
||||
})
|
||||
};
|
||||
|
||||
index.queues = index.queues || [];
|
||||
index.queues.push(newQueueEntry);
|
||||
|
||||
// Update source queue in index
|
||||
const sourceEntry = index.queues.find((q: any) => q.id === sourceQueueId);
|
||||
if (sourceEntry) {
|
||||
sourceEntry.issue_ids = remainingIssueIds;
|
||||
if (isSolutionBased) {
|
||||
sourceEntry.total_solutions = reindexedRemainingItems.length;
|
||||
sourceEntry.completed_solutions = reindexedRemainingItems.filter((i: any) => i.status === 'completed').length;
|
||||
} else {
|
||||
sourceEntry.total_tasks = reindexedRemainingItems.length;
|
||||
sourceEntry.completed_tasks = reindexedRemainingItems.filter((i: any) => i.status === 'completed').length;
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
||||
} catch {
|
||||
// Ignore index update errors
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sourceQueueId,
|
||||
newQueueId,
|
||||
splitItemCount: itemsToSplit.length,
|
||||
remainingItemCount: remainingItems.length
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: 'Failed to split queue' };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy: GET /api/issues/queue (backward compat)
|
||||
if (pathname === '/api/issues/queue' && req.method === 'GET') {
|
||||
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|
||||
|
||||
@@ -75,10 +75,13 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
|
||||
}
|
||||
}
|
||||
|
||||
// Load summaries from .summaries/
|
||||
// Load summaries from .summaries/ and fallback to plan.json
|
||||
if (dataType === 'summary' || dataType === 'all') {
|
||||
const summariesDir = join(normalizedPath, '.summaries');
|
||||
result.summaries = [];
|
||||
result.summary = null; // Single summary text from plan.json
|
||||
|
||||
// 1. Try to load from .summaries/ directory
|
||||
if (await fileExists(summariesDir)) {
|
||||
const files = (await readdir(summariesDir)).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
@@ -90,6 +93,26 @@ async function getSessionDetailData(sessionPath: string, dataType: string): Prom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: Try to get summary from plan.json (for lite-fix-plan sessions)
|
||||
if (result.summaries.length === 0) {
|
||||
const planFile = join(normalizedPath, 'plan.json');
|
||||
if (await fileExists(planFile)) {
|
||||
try {
|
||||
const planData = JSON.parse(await readFile(planFile, 'utf8'));
|
||||
// Check plan.summary
|
||||
if (planData.summary) {
|
||||
result.summary = planData.summary;
|
||||
}
|
||||
// Check synthesis.convergence.summary
|
||||
if (!result.summary && planData.synthesis?.convergence?.summary) {
|
||||
result.summary = planData.synthesis.convergence.summary;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse plan file for summary:', planFile, (e as Error).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load plan.json (for lite tasks)
|
||||
|
||||
Reference in New Issue
Block a user