mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-07 16:41:06 +08:00
feat: Add templates for epics, product brief, and requirements documentation
- Introduced a comprehensive template for generating epics and stories in Phase 5, including an index and individual epic files. - Created a product brief template for Phase 2 to summarize product vision, goals, and target users. - Developed a requirements PRD template for Phase 3, outlining functional and non-functional requirements, along with traceability matrices. feat: Implement tech debt roles for assessment, execution, planning, scanning, validation, and analysis - Added roles for tech debt assessment, executor, planner, scanner, validator, and analyst, each with defined phases and processes for managing technical debt. - Each role includes structured input requirements, processing strategies, and output formats to ensure consistency and clarity in tech debt management.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
// Default database path (same as Python DeepWikiStore)
|
||||
@@ -251,6 +251,122 @@ export class DeepWikiService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all symbols for multiple source file paths (batch query)
|
||||
* @param paths - Array of source file paths
|
||||
* @returns Map of normalized path to array of symbol info
|
||||
*/
|
||||
public getSymbolsForPaths(paths: string[]): Map<string, Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
doc_file: string;
|
||||
anchor: string;
|
||||
start_line: number;
|
||||
end_line: number;
|
||||
staleness_score: number;
|
||||
}>> {
|
||||
const db = this.getConnection();
|
||||
if (!db) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
try {
|
||||
const result = new Map<string, Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
doc_file: string;
|
||||
anchor: string;
|
||||
start_line: number;
|
||||
end_line: number;
|
||||
staleness_score: number;
|
||||
}>>();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT name, type, doc_file, anchor, start_line, end_line, staleness_score
|
||||
FROM deepwiki_symbols
|
||||
WHERE source_file = ?
|
||||
ORDER BY start_line
|
||||
`);
|
||||
|
||||
for (const filePath of paths) {
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const rows = stmt.all(normalizedPath) as Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
doc_file: string;
|
||||
anchor: string;
|
||||
start_line: number;
|
||||
end_line: number;
|
||||
staleness_score: number;
|
||||
}>;
|
||||
|
||||
if (rows.length > 0) {
|
||||
result.set(normalizedPath, rows);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[DeepWiki] Error getting symbols for paths:', error);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check which files have stale documentation by comparing content hashes
|
||||
* @param files - Array of {path, hash} to check
|
||||
* @returns Array of file paths where stored hash differs from provided hash
|
||||
*/
|
||||
public getStaleFiles(files: Array<{ path: string; hash: string }>): Array<{
|
||||
path: string;
|
||||
stored_hash: string | null;
|
||||
current_hash: string;
|
||||
}> {
|
||||
const db = this.getConnection();
|
||||
if (!db) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const stmt = db.prepare('SELECT content_hash FROM deepwiki_files WHERE path = ?');
|
||||
const stale: Array<{ path: string; stored_hash: string | null; current_hash: string }> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const normalizedPath = file.path.replace(/\\/g, '/');
|
||||
const row = stmt.get(normalizedPath) as { content_hash: string } | undefined;
|
||||
|
||||
if (row && row.content_hash !== file.hash) {
|
||||
stale.push({ path: file.path, stored_hash: row.content_hash, current_hash: file.hash });
|
||||
} else if (!row) {
|
||||
stale.push({ path: file.path, stored_hash: null, current_hash: file.hash });
|
||||
}
|
||||
}
|
||||
|
||||
return stale;
|
||||
} catch (error) {
|
||||
console.error('[DeepWiki] Error checking stale files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read symbol documentation file content
|
||||
* @param docFile - Path to the documentation file (relative to .deepwiki/)
|
||||
* @returns Documentation content string or null
|
||||
*/
|
||||
public getSymbolDocContent(docFile: string): string | null {
|
||||
try {
|
||||
const fullPath = join(homedir(), '.codexlens', 'deepwiki', docFile);
|
||||
if (!existsSync(fullPath)) {
|
||||
return null;
|
||||
}
|
||||
return readFileSync(fullPath, 'utf-8');
|
||||
} catch (error) {
|
||||
console.error('[DeepWiki] Error reading doc content:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage statistics
|
||||
* @returns Statistics object with counts
|
||||
@@ -293,3 +409,58 @@ export function getDeepWikiService(): DeepWikiService {
|
||||
}
|
||||
return deepWikiService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle DeepWiki API routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
*/
|
||||
export async function handleDeepWikiRoutes(ctx: {
|
||||
pathname: string;
|
||||
req: any;
|
||||
res: any;
|
||||
}): Promise<boolean> {
|
||||
const { pathname, req, res } = ctx;
|
||||
const service = getDeepWikiService();
|
||||
|
||||
if (pathname === '/api/deepwiki/symbols-for-paths' && req.method === 'POST') {
|
||||
const body = await readJsonBody(req);
|
||||
const paths: string[] = body?.paths || [];
|
||||
const result = service.getSymbolsForPaths(paths);
|
||||
// Convert Map to plain object for JSON serialization
|
||||
const obj: Record<string, any> = {};
|
||||
for (const [key, value] of result) {
|
||||
obj[key] = value;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(obj));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathname === '/api/deepwiki/stale-files' && req.method === 'POST') {
|
||||
const body = await readJsonBody(req);
|
||||
const files: Array<{ path: string; hash: string }> = body?.files || [];
|
||||
const result = service.getStaleFiles(files);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON body from request
|
||||
*/
|
||||
function readJsonBody(req: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk: string) => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
} catch {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,50 +35,46 @@ import {
|
||||
saveConversation
|
||||
} from './cli-executor-state.js';
|
||||
|
||||
// Track current running child process for cleanup on interruption
|
||||
let currentChildProcess: ChildProcess | null = null;
|
||||
let killTimeout: NodeJS.Timeout | null = null;
|
||||
let killTimeoutProcess: ChildProcess | null = null;
|
||||
// Track all running child processes for cleanup on interruption (multi-process support)
|
||||
const runningChildProcesses = new Set<ChildProcess>();
|
||||
|
||||
/**
|
||||
* Kill the current running CLI child process
|
||||
* Kill all running CLI child processes
|
||||
* Called when parent process receives SIGINT/SIGTERM
|
||||
*/
|
||||
export function killCurrentCliProcess(): boolean {
|
||||
const child = currentChildProcess;
|
||||
if (!child || child.killed) return false;
|
||||
export function killAllCliProcesses(): boolean {
|
||||
if (runningChildProcesses.size === 0) return false;
|
||||
|
||||
debugLog('KILL', 'Killing current child process', { pid: child.pid });
|
||||
const processesToKill = Array.from(runningChildProcesses);
|
||||
debugLog('KILL', `Killing ${processesToKill.length} child process(es)`, { pids: processesToKill.map(p => p.pid) });
|
||||
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
} catch {
|
||||
// Ignore kill errors (process may already be gone)
|
||||
// 1. SIGTERM for graceful shutdown
|
||||
for (const child of processesToKill) {
|
||||
if (!child.killed) {
|
||||
try { child.kill('SIGTERM'); } catch { /* Ignore kill errors */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (killTimeout) {
|
||||
clearTimeout(killTimeout);
|
||||
killTimeout = null;
|
||||
killTimeoutProcess = null;
|
||||
}
|
||||
|
||||
// Force kill after 2 seconds if still running.
|
||||
killTimeoutProcess = child;
|
||||
killTimeout = setTimeout(() => {
|
||||
const target = killTimeoutProcess;
|
||||
if (!target || target !== currentChildProcess) return;
|
||||
if (target.killed) return;
|
||||
|
||||
try {
|
||||
target.kill('SIGKILL');
|
||||
} catch {
|
||||
// Ignore kill errors (process may already be gone)
|
||||
// 2. SIGKILL after 2s timeout
|
||||
const killTimeout = setTimeout(() => {
|
||||
for (const child of processesToKill) {
|
||||
if (!child.killed) {
|
||||
try { child.kill('SIGKILL'); } catch { /* Ignore kill errors */ }
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
killTimeout.unref();
|
||||
|
||||
runningChildProcesses.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward compatibility alias
|
||||
* @deprecated Use killAllCliProcesses() instead
|
||||
*/
|
||||
export const killCurrentCliProcess = killAllCliProcesses;
|
||||
|
||||
// LiteLLM integration
|
||||
import { executeLiteLLMEndpoint } from './litellm-executor.js';
|
||||
import { findEndpointById } from '../config/litellm-api-config-manager.js';
|
||||
@@ -242,8 +238,8 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Track current child process for cleanup
|
||||
currentChildProcess = child;
|
||||
// Track child process for cleanup (multi-process support)
|
||||
runningChildProcesses.add(child);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
@@ -282,7 +278,7 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
currentChildProcess = null;
|
||||
runningChildProcesses.delete(child);
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
@@ -338,7 +334,7 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
currentChildProcess = null;
|
||||
runningChildProcesses.delete(child);
|
||||
reject(new Error(`Failed to spawn claude: ${error.message}`));
|
||||
});
|
||||
});
|
||||
@@ -999,8 +995,8 @@ async function executeCliTool(
|
||||
env: spawnEnv
|
||||
});
|
||||
|
||||
// Track current child process for cleanup on interruption
|
||||
currentChildProcess = child;
|
||||
// Track child process for cleanup on interruption (multi-process support)
|
||||
runningChildProcesses.add(child);
|
||||
|
||||
debugLog('SPAWN', `Process spawned`, { pid: child.pid });
|
||||
|
||||
@@ -1050,14 +1046,8 @@ async function executeCliTool(
|
||||
|
||||
// Handle completion
|
||||
child.on('close', async (code) => {
|
||||
if (killTimeout && killTimeoutProcess === child) {
|
||||
clearTimeout(killTimeout);
|
||||
killTimeout = null;
|
||||
killTimeoutProcess = null;
|
||||
}
|
||||
|
||||
// Clear current child process reference
|
||||
currentChildProcess = null;
|
||||
// Remove from running processes
|
||||
runningChildProcesses.delete(child);
|
||||
|
||||
// Flush remaining buffer from parser
|
||||
const remainingUnits = parser.flush();
|
||||
@@ -1319,6 +1309,9 @@ async function executeCliTool(
|
||||
|
||||
// Handle errors
|
||||
child.on('error', (error) => {
|
||||
// Remove from running processes
|
||||
runningChildProcesses.delete(child);
|
||||
|
||||
errorLog('SPAWN', `Failed to spawn process`, error, {
|
||||
tool,
|
||||
command,
|
||||
|
||||
Reference in New Issue
Block a user