@@ -385,49 +385,144 @@ def invalidate_cache(self, task_id: str | None = None) -> None:
385385 async def search_context (self , query : str ) -> dict [str , str | list [str ] | int ]:
386386 """Search across all sources by keyword.
387387
388- This is a placeholder implementation that will be enhanced
389- when adapters support search functionality.
388+ Searches Jira, meetings, and local docs in parallel for freeform queries
389+ like "how do we handle retries?" or "what was decided about payments?".
390+
391+ No LLM synthesis - returns formatted search results directly.
392+ No caching - queries are too varied.
390393
391394 Args:
392395 query: The search query.
393396
394397 Returns:
395- Dictionary with search results and metadata.
398+ Dictionary with formatted results and metadata.
396399 """
397400 start_time = time .monotonic ()
398- sources : list [ str ] = []
401+ logger . info ( "Search context" , extra = { "query" : query })
399402
400- if self ._config .sources .jira .enabled :
401- sources .append ("jira" )
402- if self ._config .sources .fireflies .enabled :
403- sources .append ("fireflies" )
404- if self ._config .sources .docs .enabled :
405- sources .append ("docs" )
403+ # Search all sources in parallel
404+ jira_coro = self ._search_jira (query )
405+ meetings_coro = self ._search_meetings (query )
406+ docs_coro = self ._search_docs (query )
406407
407- # TODO: Implement real search across adapters
408- logger .info ("Search context" , extra = {"query" : query , "sources" : sources })
408+ results = await asyncio .gather (jira_coro , meetings_coro , docs_coro , return_exceptions = True )
409409
410- duration_ms = int (( time . monotonic () - start_time ) * 1000 )
410+ jira_results , meeting_results , docs_results = results
411411
412- results = f"""## Search Results
412+ # Format results into markdown
413+ sections : list [str ] = [f'## Search Results for "{ query } "' ]
414+ sources_used : list [str ] = []
415+ total_results = 0
416+
417+ # Format Jira results
418+ if isinstance (jira_results , list ) and jira_results :
419+ sources_used .append ("jira" )
420+ total_results += len (jira_results )
421+ sections .append (self ._format_jira_search_results (jira_results ))
422+ elif isinstance (jira_results , BaseException ):
423+ logger .warning ("Jira search failed" , extra = {"error" : str (jira_results )})
424+
425+ # Format meeting results
426+ if isinstance (meeting_results , MeetingContext ) and meeting_results .meetings :
427+ sources_used .append ("fireflies" )
428+ total_results += len (meeting_results .meetings )
429+ sections .append (self ._format_meeting_search_results (meeting_results ))
430+ elif isinstance (meeting_results , BaseException ):
431+ logger .warning ("Meeting search failed" , extra = {"error" : str (meeting_results )})
432+
433+ # Format docs results
434+ if isinstance (docs_results , DocsContext ) and docs_results .sections :
435+ sources_used .append ("docs" )
436+ total_results += len (docs_results .sections )
437+ sections .append (self ._format_docs_search_results (docs_results ))
438+ elif isinstance (docs_results , BaseException ):
439+ logger .warning ("Docs search failed" , extra = {"error" : str (docs_results )})
440+
441+ # Handle no results
442+ if total_results == 0 :
443+ sections .append ("\n No results found." )
413444
414- No real search implemented yet. Query: " { query } "
445+ duration_ms = int (( time . monotonic () - start_time ) * 1000 )
415446
416- This will search across:
417- - Jira tickets (title, description, comments)
418- - Meeting transcripts (full text search)
419- - Local documentation (keyword matching)
420- """
447+ logger .info (
448+ "Search completed" ,
449+ extra = {
450+ "query" : query ,
451+ "result_count" : total_results ,
452+ "sources" : sources_used ,
453+ "duration_ms" : duration_ms ,
454+ },
455+ )
421456
422457 return {
423458 "query" : query ,
424- "results" : results ,
425- "sources" : sources if sources else ["none configured " ],
426- "result_count" : 0 ,
459+ "results" : " \n \n " . join ( sections ) ,
460+ "sources" : sources_used if sources_used else ["none" ],
461+ "result_count" : total_results ,
427462 "duration_ms" : duration_ms ,
428463 }
429464
430- async def get_standards (self , area : str | None = None ) -> dict [str , str | None | int ]:
465+ async def _search_jira (self , query : str ) -> list [JiraTicket ]:
466+ """Search Jira for matching issues."""
467+ jira = self ._get_jira ()
468+ if jira is None :
469+ return []
470+ return await jira .search_issues (query , max_results = 5 )
471+
472+ async def _search_meetings (self , query : str ) -> MeetingContext :
473+ """Search meeting transcripts for query."""
474+ fireflies = self ._get_fireflies ()
475+ if fireflies is None :
476+ return MeetingContext (meetings = [])
477+ return await fireflies .get_meeting_context (query )
478+
479+ async def _search_docs (self , query : str ) -> DocsContext :
480+ """Search local documentation for query."""
481+ docs = self ._get_docs ()
482+ if docs is None :
483+ return DocsContext (sections = [])
484+ return await docs .search_docs (query , max_results = 5 )
485+
486+ def _format_jira_search_results (self , tickets : list [JiraTicket ]) -> str :
487+ """Format Jira search results as markdown."""
488+ parts = ["### Jira Tickets" ]
489+ for ticket in tickets :
490+ status_badge = f"[{ ticket .status } ]"
491+ assignee = f" — { ticket .assignee } " if ticket .assignee else ""
492+ parts .append (f"- **{ ticket .ticket_id } **: { ticket .title } { status_badge } { assignee } " )
493+ return "\n " .join (parts )
494+
495+ def _format_meeting_search_results (self , context : MeetingContext ) -> str :
496+ """Format meeting search results as markdown."""
497+ parts = ["### Meeting Discussions" ]
498+ for meeting in context .meetings :
499+ date_str = meeting .meeting_date .strftime ("%Y-%m-%d" )
500+ parts .append (f"\n **{ meeting .meeting_title } ** ({ date_str } )" )
501+ # Truncate excerpt for search results
502+ excerpt = meeting .excerpt
503+ if len (excerpt ) > 300 :
504+ excerpt = excerpt [:300 ] + "..."
505+ parts .append (excerpt )
506+ return "\n " .join (parts )
507+
508+ def _format_docs_search_results (self , context : DocsContext ) -> str :
509+ """Format docs search results as markdown."""
510+ parts = ["### Documentation" ]
511+ for section in context .sections :
512+ title = section .section_title or section .file_path
513+ doc_type = f"[{ section .doc_type } ]"
514+ parts .append (f"\n **{ title } ** { doc_type } " )
515+ parts .append (f"*Source: { section .file_path } *" )
516+ # Truncate content for search results
517+ content = section .content
518+ if len (content ) > 200 :
519+ content = content [:200 ] + "..."
520+ parts .append (content )
521+ return "\n " .join (parts )
522+
523+ async def get_standards (
524+ self , area : str | None = None
525+ ) -> dict [str , str | None | int | list [str ]]:
431526 """Get coding standards from local documentation.
432527
433528 Args:
@@ -440,23 +535,42 @@ async def get_standards(self, area: str | None = None) -> dict[str, str | None |
440535 logger .info ("Get standards" , extra = {"area" : area })
441536
442537 docs = self ._get_docs ()
538+ available_areas : list [str ] = []
539+
443540 if docs is None :
444- content = self ._get_standards_not_configured_message (area )
541+ content = self ._get_standards_not_configured_message ()
445542 section_count = 0
446543 else :
447544 docs_context = await docs .get_standards (area )
545+ available_areas = await docs .list_standards_areas ()
546+
448547 if docs_context .sections :
449- # Format sections into markdown
450- parts : list [str ] = []
548+ # Format sections into markdown with header
549+ title = f"# Coding Standards: { area } " if area else "# Coding Standards"
550+ parts : list [str ] = [title , "" ]
551+
451552 for section in docs_context .sections :
553+ # Add source file info
554+ source_info = f"*Source: { section .file_path } *"
452555 if section .section_title :
453- parts .append (f"## { section .section_title } \n \n { section .content } " )
556+ parts .append (f"## { section .section_title } " )
557+ parts .append (source_info )
558+ parts .append ("" )
559+ parts .append (section .content )
454560 else :
561+ parts .append (source_info )
562+ parts .append ("" )
455563 parts .append (section .content )
456- content = "\n \n ---\n \n " .join (parts )
564+ parts .append ("" ) # Blank line between sections
565+
566+ content = "\n " .join (parts )
457567 section_count = len (docs_context .sections )
568+ elif area and available_areas :
569+ # Area specified but no matches - show available areas
570+ content = self ._get_no_matching_standards_message (area , available_areas )
571+ section_count = 0
458572 else :
459- content = self ._get_standards_not_configured_message (area )
573+ content = self ._get_standards_not_configured_message ()
460574 section_count = 0
461575
462576 duration_ms = int ((time .monotonic () - start_time ) * 1000 )
@@ -469,28 +583,57 @@ async def get_standards(self, area: str | None = None) -> dict[str, str | None |
469583 "area" : area ,
470584 "content" : content ,
471585 "section_count" : section_count ,
586+ "available_areas" : available_areas ,
587+ "duration_ms" : duration_ms ,
472588 }
473589
474- def _get_standards_not_configured_message (self , area : str | None ) -> str :
590+ def _get_standards_not_configured_message (self ) -> str :
475591 """Generate message when no standards are configured."""
476- area_filter = f" for { area } " if area else ""
477- return f"""## Coding Standards{ area_filter }
592+ return """# Coding Standards
478593
479594No standards documents found.
480595
481- To add standards:
596+ ## How to Configure
597+
4825981. Create markdown files in your docs directory
483- 2. Configure `sources.docs.paths` in .devscontext.yaml
599+ 2. Configure `sources.docs.paths` in ` .devscontext.yaml`
4846003. Place files in a `standards/` subdirectory
485601
486- Example structure:
602+ ### Example Structure
603+
487604```
488605docs/
489606 standards/
490607 typescript.md
491608 testing.md
492609 api-design.md
493610```
611+
612+ ### Example .devscontext.yaml
613+
614+ ```yaml
615+ sources:
616+ docs:
617+ enabled: true
618+ paths:
619+ - ./docs
620+ ```
621+
622+ Files in the `standards/` directory will be automatically recognized as coding standards.
623+ """
624+
625+ def _get_no_matching_standards_message (self , area : str , available_areas : list [str ]) -> str :
626+ """Generate message when no standards match the requested area."""
627+ areas_list = "\n " .join (f"- `{ a } `" for a in available_areas )
628+ return f"""# Coding Standards: { area }
629+
630+ No standards found for "{ area } ".
631+
632+ ## Available Areas
633+
634+ { areas_list }
635+
636+ Try one of the areas above, or omit the area parameter to see all standards.
494637"""
495638
496639 async def close (self ) -> None :
0 commit comments