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:
catlog22
2026-01-17 19:20:24 +08:00
parent 1fae35c05d
commit f14418603a
137 changed files with 13125 additions and 301 deletions

View File

@@ -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) => {

View File

@@ -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));

View File

@@ -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)