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:
catlog22
2026-03-17 12:55:14 +08:00
parent e6255cf41a
commit bfe5426b7e
31 changed files with 3203 additions and 200 deletions

View File

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

View File

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

View File

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

View File

@@ -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
*/