mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-18 18:48:48 +08:00
Refactor agent spawning and delegation check mechanisms
- Updated agent spawning from `Task()` to `Agent()` across various files to align with new standards. - Enhanced the `code-developer` agent description to clarify its invocation context and responsibilities. - Introduced a new `delegation-check` skill to validate command delegation prompts against agent role definitions, ensuring content separation and conflict detection. - Established comprehensive separation rules for command delegation prompts and agent definitions, detailing ownership and conflict patterns. - Improved documentation for command and agent design specifications to reflect the updated spawning patterns and validation processes.
This commit is contained in:
@@ -8,9 +8,11 @@ import {
|
||||
checkVenvStatus,
|
||||
executeCodexLens,
|
||||
getVenvPythonPath,
|
||||
useCodexLensV2,
|
||||
} from '../../../tools/codex-lens.js';
|
||||
import type { RouteContext } from '../types.js';
|
||||
import { extractJSON, stripAnsiCodes } from './utils.js';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
// File watcher state (persisted across requests)
|
||||
let watcherProcess: any = null;
|
||||
@@ -43,6 +45,29 @@ export async function stopWatcherForUninstall(): Promise<void> {
|
||||
watcherProcess = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn v2 bridge watcher subprocess.
|
||||
* Runs 'codexlens-search watch --root X --debounce-ms Y' and reads JSONL stdout.
|
||||
* @param root - Root directory to watch
|
||||
* @param debounceMs - Debounce interval in milliseconds
|
||||
* @returns Spawned child process
|
||||
*/
|
||||
function spawnV2Watcher(root: string, debounceMs: number): ChildProcess {
|
||||
const { spawn } = require('child_process') as typeof import('child_process');
|
||||
return spawn('codexlens-search', [
|
||||
'watch',
|
||||
'--root', root,
|
||||
'--debounce-ms', String(debounceMs),
|
||||
'--db-path', require('path').join(root, '.codexlens'),
|
||||
], {
|
||||
cwd: root,
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CodexLens watcher routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
@@ -91,46 +116,52 @@ export async function handleCodexLensWatcherRoutes(ctx: RouteContext): Promise<b
|
||||
return { success: false, error: `Path is not a directory: ${targetPath}`, status: 400 };
|
||||
}
|
||||
|
||||
// Get the codexlens CLI path
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
return { success: false, error: 'CodexLens not installed', status: 400 };
|
||||
}
|
||||
|
||||
// Verify directory is indexed before starting watcher
|
||||
try {
|
||||
const statusResult = await executeCodexLens(['projects', 'list', '--json']);
|
||||
if (statusResult.success && statusResult.output) {
|
||||
const parsed = extractJSON(statusResult.output);
|
||||
const projects = parsed.result || parsed || [];
|
||||
const normalizedTarget = targetPath.toLowerCase().replace(/\\/g, '/');
|
||||
const isIndexed = Array.isArray(projects) && projects.some((p: { source_root?: string }) =>
|
||||
p.source_root && p.source_root.toLowerCase().replace(/\\/g, '/') === normalizedTarget
|
||||
);
|
||||
if (!isIndexed) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Directory is not indexed: ${targetPath}. Run 'codexlens init' first.`,
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
// Route to v2 or v1 watcher based on feature flag
|
||||
if (useCodexLensV2()) {
|
||||
// v2 bridge watcher: codexlens-search watch
|
||||
console.log('[CodexLens] Using v2 bridge watcher');
|
||||
watcherProcess = spawnV2Watcher(targetPath, debounceMs);
|
||||
} else {
|
||||
// v1 watcher: python -m codexlens watch
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
return { success: false, error: 'CodexLens not installed', status: 400 };
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CodexLens] Could not verify index status:', err);
|
||||
// Continue anyway - watcher will fail with proper error if not indexed
|
||||
}
|
||||
|
||||
// Spawn watch process using Python (no shell: true for security)
|
||||
// CodexLens is a Python package, must run via python -m codexlens
|
||||
const pythonPath = getVenvPythonPath();
|
||||
const args = ['-m', 'codexlens', 'watch', targetPath, '--debounce', String(debounceMs)];
|
||||
watcherProcess = spawn(pythonPath, args, {
|
||||
cwd: targetPath,
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' }
|
||||
});
|
||||
// Verify directory is indexed before starting watcher
|
||||
try {
|
||||
const statusResult = await executeCodexLens(['projects', 'list', '--json']);
|
||||
if (statusResult.success && statusResult.output) {
|
||||
const parsed = extractJSON(statusResult.output);
|
||||
const projects = parsed.result || parsed || [];
|
||||
const normalizedTarget = targetPath.toLowerCase().replace(/\\/g, '/');
|
||||
const isIndexed = Array.isArray(projects) && projects.some((p: { source_root?: string }) =>
|
||||
p.source_root && p.source_root.toLowerCase().replace(/\\/g, '/') === normalizedTarget
|
||||
);
|
||||
if (!isIndexed) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Directory is not indexed: ${targetPath}. Run 'codexlens init' first.`,
|
||||
status: 400
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CodexLens] Could not verify index status:', err);
|
||||
// Continue anyway - watcher will fail with proper error if not indexed
|
||||
}
|
||||
|
||||
// Spawn watch process using Python (no shell: true for security)
|
||||
const pythonPath = getVenvPythonPath();
|
||||
const args = ['-m', 'codexlens', 'watch', targetPath, '--debounce', String(debounceMs)];
|
||||
watcherProcess = spawn(pythonPath, args, {
|
||||
cwd: targetPath,
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' }
|
||||
});
|
||||
}
|
||||
|
||||
watcherStats = {
|
||||
running: true,
|
||||
@@ -153,13 +184,37 @@ export async function handleCodexLensWatcherRoutes(ctx: RouteContext): Promise<b
|
||||
}
|
||||
|
||||
// Handle process output for event counting
|
||||
const isV2Watcher = useCodexLensV2();
|
||||
let stdoutLineBuffer = '';
|
||||
if (watcherProcess.stdout) {
|
||||
watcherProcess.stdout.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
// Count processed events from output
|
||||
const matches = output.match(/Processed \d+ events?/g);
|
||||
if (matches) {
|
||||
watcherStats.events_processed += matches.length;
|
||||
|
||||
if (isV2Watcher) {
|
||||
// v2 bridge outputs JSONL - parse line by line
|
||||
stdoutLineBuffer += output;
|
||||
const lines = stdoutLineBuffer.split('\n');
|
||||
// Keep incomplete last line in buffer
|
||||
stdoutLineBuffer = lines.pop() || '';
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const event = JSON.parse(trimmed);
|
||||
// Count file change events (created, modified, deleted, moved)
|
||||
if (event.event && event.event !== 'watching') {
|
||||
watcherStats.events_processed += 1;
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, skip
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// v1 watcher: count text-based event messages
|
||||
const matches = output.match(/Processed \d+ events?/g);
|
||||
if (matches) {
|
||||
watcherStats.events_processed += matches.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1067,6 +1067,103 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if codexlens-search (v2) bridge CLI is installed and functional.
|
||||
* Runs 'codexlens-search status' and checks exit code.
|
||||
* @returns true if the v2 bridge CLI is available
|
||||
*/
|
||||
function isCodexLensV2Installed(): boolean {
|
||||
try {
|
||||
const result = spawnSync('codexlens-search', ['status', '--db-path', '.codexlens'], {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUTS.PYTHON_VERSION,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
});
|
||||
// Exit code 0 or valid JSON output means it's installed
|
||||
return result.status === 0 || (result.stdout != null && result.stdout.includes('"status"'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap codexlens-search (v2) package using UV.
|
||||
* Installs 'codexlens-search[semantic]' into the shared CodexLens venv.
|
||||
* @returns Bootstrap result
|
||||
*/
|
||||
async function bootstrapV2WithUv(): Promise<BootstrapResult> {
|
||||
console.log('[CodexLens] Bootstrapping codexlens-search (v2) with UV...');
|
||||
|
||||
const preFlightError = preFlightCheck();
|
||||
if (preFlightError) {
|
||||
return { success: false, error: `Pre-flight failed: ${preFlightError}` };
|
||||
}
|
||||
|
||||
repairVenvIfCorrupted();
|
||||
|
||||
const uvInstalled = await ensureUvInstalled();
|
||||
if (!uvInstalled) {
|
||||
return { success: false, error: 'Failed to install UV package manager' };
|
||||
}
|
||||
|
||||
const uv = createCodexLensUvManager();
|
||||
|
||||
if (!uv.isVenvValid()) {
|
||||
console.log('[CodexLens] Creating virtual environment with UV for v2...');
|
||||
const createResult = await uv.createVenv();
|
||||
if (!createResult.success) {
|
||||
return { success: false, error: `Failed to create venv: ${createResult.error}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Find local codexlens-search package using unified discovery
|
||||
const { findCodexLensSearchPath } = await import('../utils/package-discovery.js');
|
||||
const discovery = findCodexLensSearchPath();
|
||||
|
||||
const extras = ['semantic'];
|
||||
const editable = isDevEnvironment() && !discovery.insideNodeModules;
|
||||
|
||||
if (!discovery.path) {
|
||||
// Fallback: try installing from PyPI
|
||||
console.log('[CodexLens] Local codexlens-search not found, trying PyPI install...');
|
||||
const pipResult = await uv.install(['codexlens-search[semantic]']);
|
||||
if (!pipResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to install codexlens-search from PyPI: ${pipResult.error}`,
|
||||
diagnostics: { venvPath: getCodexLensVenvDir(), installer: 'uv' },
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log(`[CodexLens] Installing codexlens-search from local path with UV: ${discovery.path} (editable: ${editable})`);
|
||||
const installResult = await uv.installFromProject(discovery.path, extras, editable);
|
||||
if (!installResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to install codexlens-search: ${installResult.error}`,
|
||||
diagnostics: { packagePath: discovery.path, venvPath: getCodexLensVenvDir(), installer: 'uv', editable },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
clearVenvStatusCache();
|
||||
console.log('[CodexLens] codexlens-search (v2) bootstrap complete');
|
||||
return {
|
||||
success: true,
|
||||
message: 'Installed codexlens-search (v2) with UV',
|
||||
diagnostics: { packagePath: discovery.path ?? undefined, venvPath: getCodexLensVenvDir(), installer: 'uv', editable },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if v2 bridge should be used based on CCW_USE_CODEXLENS_V2 env var.
|
||||
*/
|
||||
function useCodexLensV2(): boolean {
|
||||
const flag = process.env.CCW_USE_CODEXLENS_V2;
|
||||
return flag === '1' || flag === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap CodexLens venv with required packages
|
||||
* @returns Bootstrap result
|
||||
@@ -1074,6 +1171,23 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
|
||||
async function bootstrapVenv(): Promise<BootstrapResult> {
|
||||
const warnings: string[] = [];
|
||||
|
||||
// If v2 flag is set, also bootstrap codexlens-search alongside v1
|
||||
if (useCodexLensV2() && await isUvAvailable()) {
|
||||
try {
|
||||
const v2Result = await bootstrapV2WithUv();
|
||||
if (v2Result.success) {
|
||||
console.log('[CodexLens] codexlens-search (v2) installed successfully');
|
||||
} else {
|
||||
console.warn(`[CodexLens] codexlens-search (v2) bootstrap failed: ${v2Result.error}`);
|
||||
warnings.push(`v2 bootstrap failed: ${v2Result.error || 'Unknown error'}`);
|
||||
}
|
||||
} catch (v2Err) {
|
||||
const msg = v2Err instanceof Error ? v2Err.message : String(v2Err);
|
||||
console.warn(`[CodexLens] codexlens-search (v2) bootstrap error: ${msg}`);
|
||||
warnings.push(`v2 bootstrap error: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer UV if available (faster package resolution and installation)
|
||||
if (await isUvAvailable()) {
|
||||
console.log('[CodexLens] Using UV for bootstrap...');
|
||||
@@ -2502,6 +2616,10 @@ export {
|
||||
// UV-based installation functions
|
||||
bootstrapWithUv,
|
||||
installSemanticWithUv,
|
||||
// v2 bridge support
|
||||
useCodexLensV2,
|
||||
isCodexLensV2Installed,
|
||||
bootstrapV2WithUv,
|
||||
};
|
||||
|
||||
// Export Python path for direct spawn usage (e.g., watcher)
|
||||
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
ensureLiteLLMEmbedderReady,
|
||||
executeCodexLens,
|
||||
getVenvPythonPath,
|
||||
useCodexLensV2,
|
||||
} from './codex-lens.js';
|
||||
import { execFile } from 'child_process';
|
||||
import type { ProgressInfo } from './codex-lens.js';
|
||||
import { getProjectRoot } from '../utils/path-validator.js';
|
||||
import { getCodexLensDataDir } from '../utils/codexlens-path.js';
|
||||
@@ -2774,6 +2776,90 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// codexlens-search v2 bridge integration
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Execute search via codexlens-search (v2) bridge CLI.
|
||||
* Spawns 'codexlens-search search --query X --top-k Y --db-path Z' and parses JSON output.
|
||||
*
|
||||
* @param query - Search query string
|
||||
* @param topK - Number of results to return
|
||||
* @param dbPath - Path to the v2 index database directory
|
||||
* @returns Parsed search results as SemanticMatch array
|
||||
*/
|
||||
async function executeCodexLensV2Bridge(
|
||||
query: string,
|
||||
topK: number,
|
||||
dbPath: string,
|
||||
): Promise<SearchResult> {
|
||||
return new Promise((resolve) => {
|
||||
const args = [
|
||||
'search',
|
||||
'--query', query,
|
||||
'--top-k', String(topK),
|
||||
'--db-path', dbPath,
|
||||
];
|
||||
|
||||
execFile('codexlens-search', args, {
|
||||
encoding: 'utf-8',
|
||||
timeout: EXEC_TIMEOUTS.PROCESS_SPAWN,
|
||||
windowsHide: true,
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||
}, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.warn(`[CodexLens-v2] Bridge search failed: ${error.message}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: `codexlens-search v2 bridge failed: ${error.message}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
|
||||
// Bridge outputs {"error": string} on failure
|
||||
if (parsed && typeof parsed === 'object' && 'error' in parsed) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `codexlens-search v2: ${parsed.error}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Bridge outputs array of {path, score, snippet}
|
||||
const results: SemanticMatch[] = (Array.isArray(parsed) ? parsed : []).map((r: { path?: string; score?: number; snippet?: string }) => ({
|
||||
file: r.path || '',
|
||||
score: r.score || 0,
|
||||
content: r.snippet || '',
|
||||
symbol: null,
|
||||
}));
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
results,
|
||||
metadata: {
|
||||
mode: 'semantic' as any,
|
||||
backend: 'codexlens-v2',
|
||||
count: results.length,
|
||||
query,
|
||||
note: 'Using codexlens-search v2 bridge (2-stage vector + reranking)',
|
||||
},
|
||||
});
|
||||
} catch (parseErr) {
|
||||
console.warn(`[CodexLens-v2] Failed to parse bridge output: ${(parseErr as Error).message}`);
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to parse codexlens-search v2 output: ${(parseErr as Error).message}`,
|
||||
output: stdout,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode: exact - CodexLens exact/FTS search
|
||||
* Requires index
|
||||
@@ -4276,7 +4362,21 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
||||
|
||||
case 'search':
|
||||
default:
|
||||
// Handle search modes: fuzzy | semantic
|
||||
// v2 bridge: if CCW_USE_CODEXLENS_V2 is set, try v2 bridge first for semantic mode
|
||||
if (useCodexLensV2() && (mode === 'semantic' || mode === 'fuzzy')) {
|
||||
const scope = resolveSearchScope(parsed.data.path ?? '.');
|
||||
const dbPath = join(scope.workingDirectory, '.codexlens');
|
||||
const topK = (parsed.data.maxResults || 5) + (parsed.data.extraFilesCount || 10);
|
||||
const v2Result = await executeCodexLensV2Bridge(parsed.data.query || '', topK, dbPath);
|
||||
if (v2Result.success) {
|
||||
result = v2Result;
|
||||
break;
|
||||
}
|
||||
// v2 failed, fall through to v1
|
||||
console.warn(`[CodexLens-v2] Falling back to v1: ${v2Result.error}`);
|
||||
}
|
||||
|
||||
// Handle search modes: fuzzy | semantic (v1 path)
|
||||
switch (mode) {
|
||||
case 'fuzzy':
|
||||
result = await executeFuzzyMode(parsed.data);
|
||||
|
||||
@@ -55,18 +55,20 @@ export interface PackageDiscoveryResult {
|
||||
}
|
||||
|
||||
/** Known local package names */
|
||||
export type LocalPackageName = 'codex-lens' | 'ccw-litellm';
|
||||
export type LocalPackageName = 'codex-lens' | 'ccw-litellm' | 'codexlens-search';
|
||||
|
||||
/** Environment variable mapping for each package */
|
||||
const PACKAGE_ENV_VARS: Record<LocalPackageName, string> = {
|
||||
'codex-lens': 'CODEXLENS_PACKAGE_PATH',
|
||||
'ccw-litellm': 'CCW_LITELLM_PATH',
|
||||
'codexlens-search': 'CODEXLENS_SEARCH_PATH',
|
||||
};
|
||||
|
||||
/** Config key mapping for each package */
|
||||
const PACKAGE_CONFIG_KEYS: Record<LocalPackageName, string> = {
|
||||
'codex-lens': 'codexLensPath',
|
||||
'ccw-litellm': 'ccwLitellmPath',
|
||||
'codexlens-search': 'codexlensSearchPath',
|
||||
};
|
||||
|
||||
// ========================================
|
||||
@@ -296,6 +298,13 @@ export function findCcwLitellmPath(): PackageDiscoveryResult {
|
||||
return findPackagePath('ccw-litellm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find codexlens-search (v2) package path (convenience wrapper)
|
||||
*/
|
||||
export function findCodexLensSearchPath(): PackageDiscoveryResult {
|
||||
return findPackagePath('codexlens-search');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format search results for error messages
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user