claude-code-workflow 6.3.4 → 6.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.claude/agents/issue-plan-agent.md +859 -0
  2. package/.claude/agents/issue-queue-agent.md +702 -0
  3. package/.claude/commands/issue/execute.md +453 -0
  4. package/.claude/commands/issue/manage.md +865 -0
  5. package/.claude/commands/issue/new.md +484 -0
  6. package/.claude/commands/issue/plan.md +421 -0
  7. package/.claude/commands/issue/queue.md +354 -0
  8. package/.claude/commands/{clean.md → workflow/clean.md} +5 -5
  9. package/.claude/commands/workflow/docs/analyze.md +1467 -0
  10. package/.claude/commands/workflow/docs/copyright.md +1265 -0
  11. package/.claude/commands/workflow/execute.md +0 -1
  12. package/.claude/commands/workflow/tools/conflict-resolution.md +76 -240
  13. package/.claude/commands/workflow/tools/context-gather.md +0 -2
  14. package/.claude/commands/workflow/tools/task-generate-agent.md +81 -8
  15. package/.claude/commands/workflow/tools/task-generate-tdd.md +0 -9
  16. package/.claude/commands/workflow/tools/test-context-gather.md +2 -3
  17. package/.claude/commands/workflow/tools/test-task-generate.md +0 -2
  18. package/.claude/skills/_shared/mermaid-utils.md +584 -0
  19. package/.claude/skills/command-guide/reference/agents/action-planning-agent.md +0 -2
  20. package/.claude/skills/command-guide/reference/commands/workflow/execute.md +1 -1
  21. package/.claude/skills/command-guide/reference/commands/workflow/tools/context-gather.md +1 -2
  22. package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-tdd.md +1 -8
  23. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-context-gather.md +1 -4
  24. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-task-generate.md +0 -2
  25. package/.claude/skills/copyright-docs/SKILL.md +132 -0
  26. package/.claude/skills/copyright-docs/phases/01-metadata-collection.md +78 -0
  27. package/.claude/skills/copyright-docs/phases/01.5-project-exploration.md +150 -0
  28. package/.claude/skills/copyright-docs/phases/02-deep-analysis.md +664 -0
  29. package/.claude/skills/copyright-docs/phases/02.5-consolidation.md +192 -0
  30. package/.claude/skills/copyright-docs/phases/04-document-assembly.md +261 -0
  31. package/.claude/skills/copyright-docs/phases/05-compliance-refinement.md +192 -0
  32. package/.claude/skills/copyright-docs/specs/cpcc-requirements.md +121 -0
  33. package/.claude/skills/copyright-docs/templates/agent-base.md +200 -0
  34. package/.claude/skills/project-analyze/SKILL.md +162 -0
  35. package/.claude/skills/project-analyze/phases/01-requirements-discovery.md +79 -0
  36. package/.claude/skills/project-analyze/phases/02-project-exploration.md +176 -0
  37. package/.claude/skills/project-analyze/phases/03-deep-analysis.md +854 -0
  38. package/.claude/skills/project-analyze/phases/03.5-consolidation.md +233 -0
  39. package/.claude/skills/project-analyze/phases/04-report-generation.md +217 -0
  40. package/.claude/skills/project-analyze/phases/05-iterative-refinement.md +124 -0
  41. package/.claude/skills/project-analyze/specs/quality-standards.md +115 -0
  42. package/.claude/skills/project-analyze/specs/writing-style.md +152 -0
  43. package/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json +79 -65
  44. package/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json +136 -0
  45. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +74 -0
  46. package/.claude/workflows/cli-templates/schemas/queue-schema.json +136 -0
  47. package/.claude/workflows/cli-templates/schemas/registry-schema.json +94 -0
  48. package/.claude/workflows/cli-templates/schemas/solution-schema.json +120 -0
  49. package/.claude/workflows/cli-templates/schemas/solutions-jsonl-schema.json +125 -0
  50. package/.codex/prompts/issue-execute.md +266 -0
  51. package/README.md +11 -1
  52. package/ccw/dist/cli.d.ts.map +1 -1
  53. package/ccw/dist/cli.js +25 -0
  54. package/ccw/dist/cli.js.map +1 -1
  55. package/ccw/dist/commands/cli.d.ts.map +1 -1
  56. package/ccw/dist/commands/cli.js +46 -8
  57. package/ccw/dist/commands/cli.js.map +1 -1
  58. package/ccw/dist/commands/issue.d.ts +21 -0
  59. package/ccw/dist/commands/issue.d.ts.map +1 -0
  60. package/ccw/dist/commands/issue.js +895 -0
  61. package/ccw/dist/commands/issue.js.map +1 -0
  62. package/ccw/dist/core/dashboard-generator-patch.js +1 -0
  63. package/ccw/dist/core/dashboard-generator-patch.js.map +1 -1
  64. package/ccw/dist/core/routes/cli-routes.js +2 -2
  65. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  66. package/ccw/dist/core/routes/issue-routes.d.ts +34 -0
  67. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -0
  68. package/ccw/dist/core/routes/issue-routes.js +487 -0
  69. package/ccw/dist/core/routes/issue-routes.js.map +1 -0
  70. package/ccw/dist/core/server.d.ts.map +1 -1
  71. package/ccw/dist/core/server.js +17 -2
  72. package/ccw/dist/core/server.js.map +1 -1
  73. package/ccw/dist/tools/claude-cli-tools.d.ts +7 -3
  74. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  75. package/ccw/dist/tools/claude-cli-tools.js +31 -17
  76. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  77. package/ccw/dist/tools/smart-search.d.ts +25 -0
  78. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  79. package/ccw/dist/tools/smart-search.js +121 -17
  80. package/ccw/dist/tools/smart-search.js.map +1 -1
  81. package/ccw/src/cli.ts +26 -0
  82. package/ccw/src/commands/cli.ts +49 -7
  83. package/ccw/src/commands/issue.ts +1184 -0
  84. package/ccw/src/core/dashboard-generator-patch.ts +1 -0
  85. package/ccw/src/core/routes/cli-routes.ts +3 -3
  86. package/ccw/src/core/routes/issue-routes.ts +559 -0
  87. package/ccw/src/core/server.ts +17 -2
  88. package/ccw/src/templates/dashboard-css/32-issue-manager.css +2544 -0
  89. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +467 -0
  90. package/ccw/src/templates/dashboard-js/components/cli-history.js +40 -13
  91. package/ccw/src/templates/dashboard-js/components/cli-status.js +26 -2
  92. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +461 -0
  93. package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
  94. package/ccw/src/templates/dashboard-js/components/notifications.js +16 -0
  95. package/ccw/src/templates/dashboard-js/i18n.js +290 -2
  96. package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -0
  97. package/ccw/src/templates/dashboard-js/views/history.js +19 -4
  98. package/ccw/src/templates/dashboard-js/views/hook-manager.js +11 -5
  99. package/ccw/src/templates/dashboard-js/views/issue-manager.js +1546 -0
  100. package/ccw/src/templates/dashboard.html +55 -0
  101. package/ccw/src/tools/claude-cli-tools.ts +37 -20
  102. package/ccw/src/tools/smart-search.ts +157 -16
  103. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  104. package/codex-lens/src/codexlens/config.py +5 -0
  105. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  106. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  107. package/codex-lens/src/codexlens/search/hybrid_search.py +144 -11
  108. package/codex-lens/src/codexlens/search/ranking.py +267 -1
  109. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  110. package/codex-lens/src/codexlens/semantic/chunker.py +55 -10
  111. package/package.json +2 -2
@@ -275,6 +275,18 @@
275
275
  </div>
276
276
  </div>
277
277
  </div>
278
+ <!-- CLI Stream Viewer Button -->
279
+ <button class="cli-stream-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded relative"
280
+ id="cliStreamBtn"
281
+ onclick="toggleCliStreamViewer()"
282
+ data-i18n-title="header.cliStream"
283
+ title="CLI Stream Viewer">
284
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
285
+ <polyline points="4 17 10 11 4 5"/>
286
+ <line x1="12" y1="19" x2="20" y2="19"/>
287
+ </svg>
288
+ <span class="cli-stream-badge" id="cliStreamBadge"></span>
289
+ </button>
278
290
  <!-- Refresh Button -->
279
291
  <button class="refresh-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="refreshWorkspace" data-i18n-title="header.refreshWorkspace" title="Refresh workspace">
280
292
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -394,6 +406,21 @@
394
406
  </ul>
395
407
  </div>
396
408
 
409
+ <!-- Issues Section -->
410
+ <div class="mb-2" id="issuesNav">
411
+ <div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
412
+ <i data-lucide="clipboard-list" class="nav-section-icon mr-2"></i>
413
+ <span class="nav-section-title" data-i18n="nav.issues">Issues</span>
414
+ </div>
415
+ <ul class="space-y-0.5">
416
+ <li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="issue-manager" data-tooltip="Issue Manager">
417
+ <i data-lucide="list-checks" class="nav-icon"></i>
418
+ <span class="nav-text flex-1" data-i18n="nav.issueManager">Manager</span>
419
+ <span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeIssues">0</span>
420
+ </li>
421
+ </ul>
422
+ </div>
423
+
397
424
  <!-- MCP Servers Section -->
398
425
  <div class="mb-2" id="mcpServersNav">
399
426
  <div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
@@ -578,6 +605,34 @@
578
605
  <div class="drawer-overlay hidden fixed inset-0 bg-black/50 z-40" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
579
606
  </div>
580
607
 
608
+ <!-- CLI Stream Viewer Panel -->
609
+ <div class="cli-stream-viewer" id="cliStreamViewer">
610
+ <div class="cli-stream-header">
611
+ <div class="cli-stream-title">
612
+ <i data-lucide="terminal"></i>
613
+ <span data-i18n="cliStream.title">CLI Stream</span>
614
+ <span class="cli-stream-count-badge" id="cliStreamCountBadge">0</span>
615
+ </div>
616
+ <div class="cli-stream-actions">
617
+ <button class="cli-stream-action-btn" onclick="clearCompletedStreams()" data-i18n="cliStream.clearCompleted">
618
+ <i data-lucide="trash-2"></i>
619
+ <span>Clear</span>
620
+ </button>
621
+ <button class="cli-stream-close-btn" onclick="toggleCliStreamViewer()" title="Close">&times;</button>
622
+ </div>
623
+ </div>
624
+ <div class="cli-stream-tabs" id="cliStreamTabs">
625
+ <!-- Dynamic tabs -->
626
+ </div>
627
+ <div class="cli-stream-content" id="cliStreamContent">
628
+ <!-- Terminal output -->
629
+ </div>
630
+ <div class="cli-stream-status" id="cliStreamStatus">
631
+ <!-- Status bar -->
632
+ </div>
633
+ </div>
634
+ <div class="cli-stream-overlay" id="cliStreamOverlay" onclick="toggleCliStreamViewer()"></div>
635
+
581
636
  <!-- Markdown Preview Modal -->
582
637
  <div id="markdownModal" class="markdown-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
583
638
  <div class="markdown-modal-backdrop absolute inset-0 bg-black/60" onclick="closeMarkdownModal()"></div>
@@ -42,7 +42,7 @@ export interface ClaudeCliToolsConfig {
42
42
  nativeResume: boolean;
43
43
  recursiveQuery: boolean;
44
44
  cache: ClaudeCacheSettings;
45
- codeIndexMcp: 'codexlens' | 'ace'; // Code Index MCP provider
45
+ codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
46
46
  };
47
47
  }
48
48
 
@@ -308,7 +308,7 @@ export function getClaudeCliToolsInfo(projectDir: string): {
308
308
  */
309
309
  export function updateCodeIndexMcp(
310
310
  projectDir: string,
311
- provider: 'codexlens' | 'ace'
311
+ provider: 'codexlens' | 'ace' | 'none'
312
312
  ): { success: boolean; error?: string; config?: ClaudeCliToolsConfig } {
313
313
  try {
314
314
  // Update config
@@ -319,21 +319,28 @@ export function updateCodeIndexMcp(
319
319
  // Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
320
320
  const globalClaudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
321
321
 
322
+ // Define patterns for all formats
323
+ const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
324
+ const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
325
+ const nonePattern = /@~\/\.claude\/workflows\/context-tools-none\.md/g;
326
+
327
+ // Determine target file based on provider
328
+ const targetFile = provider === 'ace'
329
+ ? '@~/.claude/workflows/context-tools-ace.md'
330
+ : provider === 'none'
331
+ ? '@~/.claude/workflows/context-tools-none.md'
332
+ : '@~/.claude/workflows/context-tools.md';
333
+
322
334
  if (!fs.existsSync(globalClaudeMdPath)) {
323
335
  // If global CLAUDE.md doesn't exist, check project-level
324
336
  const projectClaudeMdPath = path.join(projectDir, '.claude', 'CLAUDE.md');
325
337
  if (fs.existsSync(projectClaudeMdPath)) {
326
338
  let content = fs.readFileSync(projectClaudeMdPath, 'utf-8');
327
339
 
328
- // Define patterns for both formats
329
- const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
330
- const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
331
-
332
- if (provider === 'ace') {
333
- content = content.replace(codexlensPattern, '@~/.claude/workflows/context-tools-ace.md');
334
- } else {
335
- content = content.replace(acePattern, '@~/.claude/workflows/context-tools.md');
336
- }
340
+ // Replace any existing pattern with the target
341
+ content = content.replace(codexlensPattern, targetFile);
342
+ content = content.replace(acePattern, targetFile);
343
+ content = content.replace(nonePattern, targetFile);
337
344
 
338
345
  fs.writeFileSync(projectClaudeMdPath, content, 'utf-8');
339
346
  console.log(`[claude-cli-tools] Updated project CLAUDE.md to use ${provider} (no global CLAUDE.md found)`);
@@ -342,14 +349,10 @@ export function updateCodeIndexMcp(
342
349
  // Update global CLAUDE.md (primary target)
343
350
  let content = fs.readFileSync(globalClaudeMdPath, 'utf-8');
344
351
 
345
- const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
346
- const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
347
-
348
- if (provider === 'ace') {
349
- content = content.replace(codexlensPattern, '@~/.claude/workflows/context-tools-ace.md');
350
- } else {
351
- content = content.replace(acePattern, '@~/.claude/workflows/context-tools.md');
352
- }
352
+ // Replace any existing pattern with the target
353
+ content = content.replace(codexlensPattern, targetFile);
354
+ content = content.replace(acePattern, targetFile);
355
+ content = content.replace(nonePattern, targetFile);
353
356
 
354
357
  fs.writeFileSync(globalClaudeMdPath, content, 'utf-8');
355
358
  console.log(`[claude-cli-tools] Updated global CLAUDE.md to use ${provider}`);
@@ -365,7 +368,21 @@ export function updateCodeIndexMcp(
365
368
  /**
366
369
  * Get current Code Index MCP provider
367
370
  */
368
- export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' {
371
+ export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
369
372
  const config = loadClaudeCliTools(projectDir);
370
373
  return config.settings.codeIndexMcp || 'codexlens';
371
374
  }
375
+
376
+ /**
377
+ * Get the context-tools file path based on provider
378
+ */
379
+ export function getContextToolsPath(provider: 'codexlens' | 'ace' | 'none'): string {
380
+ switch (provider) {
381
+ case 'ace':
382
+ return 'context-tools-ace.md';
383
+ case 'none':
384
+ return 'context-tools-none.md';
385
+ default:
386
+ return 'context-tools.md';
387
+ }
388
+ }
@@ -24,6 +24,39 @@ import {
24
24
  import type { ProgressInfo } from './codex-lens.js';
25
25
  import { getProjectRoot } from '../utils/path-validator.js';
26
26
 
27
+ // Timing utilities for performance analysis
28
+ const TIMING_ENABLED = process.env.SMART_SEARCH_TIMING === '1' || process.env.DEBUG?.includes('timing');
29
+
30
+ interface TimingData {
31
+ [key: string]: number;
32
+ }
33
+
34
+ function createTimer(): { mark: (name: string) => void; getTimings: () => TimingData; log: () => void } {
35
+ const startTime = performance.now();
36
+ const marks: { name: string; time: number }[] = [];
37
+ let lastMark = startTime;
38
+
39
+ return {
40
+ mark(name: string) {
41
+ const now = performance.now();
42
+ marks.push({ name, time: now - lastMark });
43
+ lastMark = now;
44
+ },
45
+ getTimings(): TimingData {
46
+ const timings: TimingData = {};
47
+ marks.forEach(m => { timings[m.name] = Math.round(m.time * 100) / 100; });
48
+ timings['_total'] = Math.round((performance.now() - startTime) * 100) / 100;
49
+ return timings;
50
+ },
51
+ log() {
52
+ if (TIMING_ENABLED) {
53
+ const timings = this.getTimings();
54
+ console.error(`[TIMING] smart-search: ${JSON.stringify(timings)}`);
55
+ }
56
+ }
57
+ };
58
+ }
59
+
27
60
  // Define Zod schema for validation
28
61
  const ParamsSchema = z.object({
29
62
  // Action: search (content), find_files (path/name pattern), init, status
@@ -48,6 +81,9 @@ const ParamsSchema = z.object({
48
81
  regex: z.boolean().default(true), // Use regex pattern matching (default: enabled)
49
82
  caseSensitive: z.boolean().default(true), // Case sensitivity (default: case-sensitive)
50
83
  tokenize: z.boolean().default(true), // Tokenize multi-word queries for OR matching (default: enabled)
84
+ // File type filtering
85
+ excludeExtensions: z.array(z.string()).optional().describe('File extensions to exclude from results (e.g., ["md", "txt"])'),
86
+ codeOnly: z.boolean().default(false).describe('Only return code files (excludes md, txt, json, yaml, xml, etc.)'),
51
87
  // Fuzzy matching is implicit in hybrid mode (RRF fusion)
52
88
  });
53
89
 
@@ -254,6 +290,8 @@ interface SearchMetadata {
254
290
  tokenized?: boolean; // Whether tokenization was applied
255
291
  // Pagination metadata
256
292
  pagination?: PaginationInfo;
293
+ // Performance timing data (when SMART_SEARCH_TIMING=1 or DEBUG includes 'timing')
294
+ timing?: TimingData;
257
295
  // Init action specific
258
296
  action?: string;
259
297
  path?: string;
@@ -1086,7 +1124,8 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
1086
1124
  * Requires index with embeddings
1087
1125
  */
1088
1126
  async function executeHybridMode(params: Params): Promise<SearchResult> {
1089
- const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false } = params;
1127
+ const timer = createTimer();
1128
+ const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false, excludeExtensions, codeOnly = false } = params;
1090
1129
 
1091
1130
  if (!query) {
1092
1131
  return {
@@ -1097,6 +1136,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1097
1136
 
1098
1137
  // Check CodexLens availability
1099
1138
  const readyStatus = await ensureCodexLensReady();
1139
+ timer.mark('codexlens_ready_check');
1100
1140
  if (!readyStatus.ready) {
1101
1141
  return {
1102
1142
  success: false,
@@ -1106,6 +1146,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1106
1146
 
1107
1147
  // Check index status
1108
1148
  const indexStatus = await checkIndexStatus(path);
1149
+ timer.mark('index_status_check');
1109
1150
 
1110
1151
  // Request more results to support split (full content + extra files)
1111
1152
  const totalToFetch = maxResults + extraFilesCount;
@@ -1114,8 +1155,10 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1114
1155
  args.push('--enrich');
1115
1156
  }
1116
1157
  const result = await executeCodexLens(args, { cwd: path });
1158
+ timer.mark('codexlens_search');
1117
1159
 
1118
1160
  if (!result.success) {
1161
+ timer.log();
1119
1162
  return {
1120
1163
  success: false,
1121
1164
  error: result.error,
@@ -1150,6 +1193,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1150
1193
  symbol: item.symbol || null,
1151
1194
  };
1152
1195
  });
1196
+ timer.mark('parse_results');
1153
1197
 
1154
1198
  initialCount = allResults.length;
1155
1199
 
@@ -1159,14 +1203,15 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1159
1203
  allResults = baselineResult.filteredResults;
1160
1204
  baselineInfo = baselineResult.baselineInfo;
1161
1205
 
1162
- // 1. Filter noisy files (coverage, node_modules, etc.)
1163
- allResults = filterNoisyFiles(allResults);
1206
+ // 1. Filter noisy files (coverage, node_modules, etc.) and excluded extensions
1207
+ allResults = filterNoisyFiles(allResults, { excludeExtensions, codeOnly });
1164
1208
  // 2. Boost results containing query keywords
1165
1209
  allResults = applyKeywordBoosting(allResults, query);
1166
1210
  // 3. Enforce score diversity (penalize identical scores)
1167
1211
  allResults = enforceScoreDiversity(allResults);
1168
1212
  // 4. Re-sort by adjusted scores
1169
1213
  allResults.sort((a, b) => b.score - a.score);
1214
+ timer.mark('post_processing');
1170
1215
  } catch {
1171
1216
  return {
1172
1217
  success: true,
@@ -1184,6 +1229,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1184
1229
 
1185
1230
  // Split results: first N with full content, rest as file paths only
1186
1231
  const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
1232
+ timer.mark('split_results');
1187
1233
 
1188
1234
  // Build metadata with baseline info if detected
1189
1235
  let note = 'Hybrid mode uses RRF fusion (exact + fuzzy + vector) for best results';
@@ -1191,6 +1237,10 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1191
1237
  note += ` | Filtered ${initialCount - allResults.length} hot-spot results with baseline score ~${baselineInfo.score.toFixed(4)}`;
1192
1238
  }
1193
1239
 
1240
+ // Log timing data
1241
+ timer.log();
1242
+ const timings = timer.getTimings();
1243
+
1194
1244
  return {
1195
1245
  success: true,
1196
1246
  results,
@@ -1203,22 +1253,82 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1203
1253
  note,
1204
1254
  warning: indexStatus.warning,
1205
1255
  suggested_weights: getRRFWeights(query),
1256
+ timing: TIMING_ENABLED ? timings : undefined,
1206
1257
  },
1207
1258
  };
1208
1259
  }
1209
1260
 
1210
- const RRF_WEIGHTS = {
1211
- code: { exact: 0.7, fuzzy: 0.2, vector: 0.1 },
1212
- natural: { exact: 0.4, fuzzy: 0.2, vector: 0.4 },
1213
- default: { exact: 0.5, fuzzy: 0.2, vector: 0.3 },
1214
- };
1261
+ /**
1262
+ * Query intent used to adapt RRF weights (Python parity).
1263
+ *
1264
+ * Keep this logic aligned with CodexLens Python hybrid search:
1265
+ * `codex-lens/src/codexlens/search/hybrid_search.py`
1266
+ */
1267
+ export type QueryIntent = 'keyword' | 'semantic' | 'mixed';
1268
+
1269
+ // Python default: vector 60%, exact 30%, fuzzy 10%
1270
+ const DEFAULT_RRF_WEIGHTS = {
1271
+ exact: 0.3,
1272
+ fuzzy: 0.1,
1273
+ vector: 0.6,
1274
+ } as const;
1275
+
1276
+ function normalizeWeights(weights: Record<string, number>): Record<string, number> {
1277
+ const sum = Object.values(weights).reduce((acc, v) => acc + v, 0);
1278
+ if (!Number.isFinite(sum) || sum <= 0) return { ...weights };
1279
+ return Object.fromEntries(Object.entries(weights).map(([k, v]) => [k, v / sum]));
1280
+ }
1281
+
1282
+ /**
1283
+ * Detect query intent using the same heuristic signals as Python:
1284
+ * - Code patterns: `.`, `::`, `->`, CamelCase, snake_case, common code keywords
1285
+ * - Natural language patterns: >5 words, question marks, interrogatives, common verbs
1286
+ */
1287
+ export function detectQueryIntent(query: string): QueryIntent {
1288
+ const trimmed = query.trim();
1289
+ if (!trimmed) return 'mixed';
1290
+
1291
+ const lower = trimmed.toLowerCase();
1292
+ const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
1293
+
1294
+ const hasCodeSignals =
1295
+ /(::|->|\.)/.test(trimmed) ||
1296
+ /[A-Z][a-z]+[A-Z]/.test(trimmed) ||
1297
+ /\b\w+_\w+\b/.test(trimmed) ||
1298
+ /\b(def|class|function|const|let|var|import|from|return|async|await|interface|type)\b/i.test(lower);
1299
+
1300
+ const hasNaturalSignals =
1301
+ wordCount > 5 ||
1302
+ /\?/.test(trimmed) ||
1303
+ /\b(how|what|why|when|where)\b/i.test(trimmed) ||
1304
+ /\b(handle|explain|fix|implement|create|build|use|find|search|convert|parse|generate|support)\b/i.test(trimmed);
1305
+
1306
+ if (hasCodeSignals && hasNaturalSignals) return 'mixed';
1307
+ if (hasCodeSignals) return 'keyword';
1308
+ if (hasNaturalSignals) return 'semantic';
1309
+ return 'mixed';
1310
+ }
1311
+
1312
+ /**
1313
+ * Intent → weights mapping (Python parity).
1314
+ * - keyword: exact-heavy
1315
+ * - semantic: vector-heavy
1316
+ * - mixed: keep defaults
1317
+ */
1318
+ export function adjustWeightsByIntent(
1319
+ intent: QueryIntent,
1320
+ baseWeights: Record<string, number>,
1321
+ ): Record<string, number> {
1322
+ if (intent === 'keyword') return normalizeWeights({ exact: 0.5, fuzzy: 0.1, vector: 0.4 });
1323
+ if (intent === 'semantic') return normalizeWeights({ exact: 0.2, fuzzy: 0.1, vector: 0.7 });
1324
+ return normalizeWeights({ ...baseWeights });
1325
+ }
1215
1326
 
1216
- function getRRFWeights(query: string): Record<string, number> {
1217
- const isCode = looksLikeCodeQuery(query);
1218
- const isNatural = detectNaturalLanguage(query);
1219
- if (isCode) return RRF_WEIGHTS.code;
1220
- if (isNatural) return RRF_WEIGHTS.natural;
1221
- return RRF_WEIGHTS.default;
1327
+ export function getRRFWeights(
1328
+ query: string,
1329
+ baseWeights: Record<string, number> = DEFAULT_RRF_WEIGHTS,
1330
+ ): Record<string, number> {
1331
+ return adjustWeightsByIntent(detectQueryIntent(query), baseWeights);
1222
1332
  }
1223
1333
 
1224
1334
  /**
@@ -1231,7 +1341,29 @@ const FILE_EXCLUDE_REGEXES = [...FILTER_CONFIG.exclude_files].map(pattern =>
1231
1341
  new RegExp('^' + pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*') + '$')
1232
1342
  );
1233
1343
 
1234
- function filterNoisyFiles(results: SemanticMatch[]): SemanticMatch[] {
1344
+ // Non-code file extensions (for codeOnly filter)
1345
+ const NON_CODE_EXTENSIONS = new Set([
1346
+ 'md', 'txt', 'json', 'yaml', 'yml', 'xml', 'csv', 'log',
1347
+ 'ini', 'cfg', 'conf', 'toml', 'env', 'properties',
1348
+ 'html', 'htm', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'webp',
1349
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
1350
+ 'lock', 'sum', 'mod',
1351
+ ]);
1352
+
1353
+ interface FilterOptions {
1354
+ excludeExtensions?: string[];
1355
+ codeOnly?: boolean;
1356
+ }
1357
+
1358
+ function filterNoisyFiles(results: SemanticMatch[], options: FilterOptions = {}): SemanticMatch[] {
1359
+ const { excludeExtensions = [], codeOnly = false } = options;
1360
+
1361
+ // Build extension filter set
1362
+ const excludedExtSet = new Set(excludeExtensions.map(ext => ext.toLowerCase().replace(/^\./, '')));
1363
+ if (codeOnly) {
1364
+ NON_CODE_EXTENSIONS.forEach(ext => excludedExtSet.add(ext));
1365
+ }
1366
+
1235
1367
  return results.filter(r => {
1236
1368
  const filePath = r.file || '';
1237
1369
  if (!filePath) return true;
@@ -1249,6 +1381,14 @@ function filterNoisyFiles(results: SemanticMatch[]): SemanticMatch[] {
1249
1381
  return false;
1250
1382
  }
1251
1383
 
1384
+ // Extension filter check
1385
+ if (excludedExtSet.size > 0) {
1386
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
1387
+ if (excludedExtSet.has(ext)) {
1388
+ return false;
1389
+ }
1390
+ }
1391
+
1252
1392
  return true;
1253
1393
  });
1254
1394
  }
@@ -1396,10 +1536,11 @@ function filterDominantBaselineScores(
1396
1536
  */
1397
1537
  function applyRRFFusion(
1398
1538
  resultsMap: Map<string, any[]>,
1399
- weights: Record<string, number>,
1539
+ weightsOrQuery: Record<string, number> | string,
1400
1540
  limit: number,
1401
1541
  k: number = 60,
1402
1542
  ): any[] {
1543
+ const weights = typeof weightsOrQuery === 'string' ? getRRFWeights(weightsOrQuery) : weightsOrQuery;
1403
1544
  const pathScores = new Map<string, { score: number; result: any; sources: string[] }>();
1404
1545
 
1405
1546
  resultsMap.forEach((results, source) => {
@@ -103,6 +103,11 @@ class Config:
103
103
  # Indexing/search optimizations
104
104
  global_symbol_index_enabled: bool = True # Enable project-wide symbol index fast path
105
105
 
106
+ # Optional search reranking (disabled by default)
107
+ enable_reranking: bool = False
108
+ reranking_top_k: int = 50
109
+ symbol_boost_factor: float = 1.5
110
+
106
111
  # Multi-endpoint configuration for litellm backend
107
112
  embedding_endpoints: List[Dict[str, Any]] = field(default_factory=list)
108
113
  # List of endpoint configs: [{"model": "...", "api_key": "...", "api_base": "...", "weight": 1.0}]