
    epj3                         d Z ddlZddlZddlZddlZddlmZ ddlmZm	Z	m
Z
mZ ddlmZ  ej        e          Zg dZdZh dZd	hZd
Z eh d          ZdededefdZ G d d          ZdS )a  Progressive subdirectory hint discovery.

As the agent navigates into subdirectories via tool calls (read_file, terminal,
search_files, etc.), this module discovers and loads project context files
(AGENTS.md, CLAUDE.md, .cursorrules) from those directories.  Discovered hints
are appended to the tool result so the model gets relevant context at the moment
it starts working in a new area of the codebase.

This complements the startup context loading in ``prompt_builder.py`` which only
loads from the CWD.  Subdirectory hints are discovered lazily and injected into
the conversation without modifying the system prompt (preserving prompt caching).

Inspired by Block/goose's SubdirectoryHintTracker.
    N)Path)DictAnyOptionalSet)_scan_context_content)z	AGENTS.mdz	agents.mdz	CLAUDE.mdz	claude.mdz.cursorrulesi@  >   pathworkdir	file_pathterminal   >   .hg.git.svn.tox.venv.Trash.cache.backups.mypy_cache.pytest_cachedist-packagessite-packagesvenvbackupvendorbackups__pycache__third_partynode_modulesabreturnc                 T    	 |                     |            dS # t          $ r Y dS w xY w)zKCheck if *a* is the same as or an ancestor of *b* (parent directory check).TF)relative_to
ValueError)r!   r"   s     >/home/thesage/.hermes/hermes-agent/agent/subdirectory_hints.py_is_ancestor_or_samer(   >   s@    	at   uus    
''c                       e Zd ZdZddee         fdZddZdedeee	f         dee         fd	Z
ded
eee	f         defdZdedee         fdZdedee         fdZdedefdZdedefdZdedee         fdZdS )SubdirectoryHintTrackeras  Track which directories the agent visits and load hints on first access.

    Usage::

        tracker = SubdirectoryHintTracker(working_dir="/path/to/project")

        # After each tool call:
        hints = tracker.check_tool_call("read_file", {"path": "backend/src/main.py"})
        if hints:
            tool_result += hints  # append to the tool result string
    Nworking_dirc                 *   t          |pt          j                                                              | _        t                      | _        t                      | _        | j                            | j                   | 	                                 d S N)
r   osgetcwdresolver+   set_loaded_dirs_loaded_digestsadd_seed_working_dir_digest)selfr+   s     r'   __init__z SubdirectoryHintTracker.__init__T   su     :ry{{;;CCEE'*uu *-d.///%%'''''    r#   c                    t           D ]}| j        |z  }	 |                                s"|                    d                                          }n# t
          t          f$ r Y ^w xY w|rQ| j                            t          j
        |                    d                                                                dS dS )at  Record the CWD context file's digest so it is never re-injected.

        ``prompt_builder`` already loads the working directory's context file at
        startup.  Seeding its digest here means the same content reached through
        a different path (a symlink farm, a shared workspace) is recognised as a
        duplicate instead of being sent a second time.
        utf-8encodingN)_HINT_FILENAMESr+   is_file	read_textstripOSErrorUnicodeDecodeErrorr3   r4   hashlibsha256encode	hexdigest)r6   filename	candidatecontents       r'   r5   z0SubdirectoryHintTracker._seed_working_dir_digest^   s     ( 	 	H(83I ((** #--w-??EEGG/0    $((N7>>'#:#:;;EEGG   EE	 	s   A(AA'&A'	tool_name	tool_argsc                     |                      ||          }|sdS g }|D ].}|                     |          }|r|                    |           /|sdS dd                    |          z   S )zCheck tool call arguments for new directories and load any hint files.

        Returns formatted hint text to append to the tool result, or None.
        N

)_extract_directories_load_hints_for_directoryappendjoin)r6   rJ   rK   dirs	all_hintsdhintss          r'   check_tool_callz'SubdirectoryHintTracker.check_tool_callt   s     ((I>> 	4	 	( 	(A22155E (  ''' 	4I....r8   argsc                    t                      }t          D ]V}|                    |          }t          |t                    r*|                                r|                     ||           W|t          v rA|                    dd          }t          |t                    r|                     ||           t          |          S )z1Extract directory paths from tool call arguments.command )
r1   _PATH_ARG_KEYSget
isinstancestrr@   _add_path_candidate_COMMAND_TOOLS_extract_paths_from_commandlist)r6   rJ   rW   
candidateskeyvalcmds          r'   rN   z,SubdirectoryHintTracker._extract_directories   s     !$
 " 	: 	:C((3--C#s## :		 :((j999 &&((9b))C#s## B00jAAAJr8   raw_pathrc   c                    	 t          |                                          }|                                s
| j        |z  }|                                }|j        s(|                                r|                                r|j        }t          t                    D ]J}|| j        v r dS |                     |          r|                    |           |j        }||k    r dS |}KdS # t          t          t           f$ r Y dS w xY w)a  Resolve a raw path and add its directory + ancestors to candidates.

        Walks up from the resolved directory toward the filesystem root,
        stopping at the first directory already in ``_loaded_dirs`` (or after
        ``_MAX_ANCESTOR_WALK`` levels).  This ensures that reading
        ``project/src/main.py`` discovers ``project/AGENTS.md`` even when
        ``project/src/`` has no hint files of its own.
        N)r   
expanduseris_absoluter+   r0   suffixexistsr>   parentrange_MAX_ANCESTOR_WALKr2   _is_valid_subdirr4   rA   r&   RuntimeError)r6   rg   rc   p_rm   s         r'   r_   z+SubdirectoryHintTracker._add_path_candidate   s   	X))++A==?? )$q(		Ax AHHJJ 199;; H-..  )))EE((++ &NN1%%%Q;;EE  \2 	 	 	DD	s   B)C, -8C, 'C, ,DDrf   c                    	 t          j        |          }n$# t          $ r |                                }Y nw xY w|D ]M}|                    d          rd|vrd|vr!|                    d          r7|                     ||           NdS )z5Extract path-like tokens from a shell command string.-/.)zhttp://zhttps://zgit@N)shlexsplitr&   
startswithr_   )r6   rf   rc   tokenstokens        r'   ra   z3SubdirectoryHintTracker._extract_paths_from_command   s    	![%%FF 	! 	! 	!YY[[FFF	!  
	8 
	8E$$ %Cu$4$4 ?@@ $$UJ7777
	8 
	8s    88r	   c                 6   	 |                                 sdS n# t          $ r Y dS w xY w|| j        v rdS 	 |                    | j                  sdS n/# t          t
          f$ r t          | j        |          sY dS Y nw xY w|                     |          rdS dS )aT  Check if path is a valid directory to scan for hints.

        Only allow subdirectories within the working directory tree.
        This prevents loading AGENTS.md from outside the active workspace
        (e.g. ~/.codex/AGENTS.md, ~/.claude/CLAUDE.md), which causes
        cross-agent context contamination and instruction mixup.
        FT)is_dirrA   r2   is_relative_tor+   r&   r(   _is_excluded)r6   r	   s     r'   rp   z(SubdirectoryHintTracker._is_valid_subdir   s    	;;== u 	 	 	55	4$$$5
	&&t'788 u$ 	 	 	 ((8$?? uu 	
 T"" 	5ts    
''A &A?>A?c                     	 |                     | j                  j        }n# t          $ r Y dS w xY wt	          d |D                       S )u:  True when the path sits inside a directory that holds copies, not context.

        Directories the user is deliberately working inside are never excluded —
        if ``working_dir`` is itself under ``vendor/``, that segment is legitimate
        and only segments *below* the working dir are screened.
        Tc              3   (   K   | ]}|t           v V  d S r-   )_EXCLUDED_DIR_NAMES).0parts     r'   	<genexpr>z7SubdirectoryHintTracker._is_excluded.<locals>.<genexpr>   s(      EE44..EEEEEEr8   )r%   r+   partsr&   any)r6   r	   	rel_partss      r'   r   z$SubdirectoryHintTracker._is_excluded   se    	(()9::@II 	 	 	 44	 EE9EEEEEEs   " 
00	directoryc                 b   | j                             |           	 |                    | j                  s#t                              d|| j                   dS nP# t          t          f$ r< t          | j        |          s$t                              d|| j                   Y dS Y nw xY wg }t          D ]}||z  }	 |
                                sn# t          $ r Y +w xY w	 |                    d                                          }|s[t          j        |                    d                                                    }|| j        v r't                              d||dd                     nV| j                            |           t%          ||          }t'          |          t(          k    r'|dt(                   d| dt'          |          d	d
z   }t+          |          }	 t+          |                    | j                            }nh# t          t.          f$ rT 	 t+          |                    t1          j                                        }d|z   }n# t          t.          f$ r Y nw xY wY nw xY w|                    ||f            n4# t6          $ r'}t                              d||           Y d}~d}~ww xY w|sdS g }	|D ] \  }}|	                    d| d|            !t                              d|d |D                        d                    |	          S )zLoad hint files from a directory. Returns formatted text or None.

        Only loads hints from directories within the working directory tree.
        u4   Skipping hint files in %s — outside working_dir %sNr:   r;   z1Skipping duplicate hint content at %s (digest %s)   z

[...truncated z: ,z chars total]z~/zCould not read %s: %sz"[Subdirectory context discovered: z]
z%Loaded subdirectory hints from %s: %sc                     g | ]
}|d          S )r    )r   hs     r'   
<listcomp>zESubdirectoryHintTracker._load_hints_for_directory.<locals>.<listcomp>R  s    '''aQqT'''r8   rM   )r2   r4   r   r+   loggerdebugrA   r&   r(   r=   r>   r?   r@   rC   rD   rE   rF   r3   r   len_MAX_HINT_CHARSr^   r%   rq   r   homerP   	ExceptionrQ   )
r6   r   found_hintsrG   	hint_pathrI   digestrel_pathexcsectionss
             r'   rO   z1SubdirectoryHintTracker._load_hints_for_directory   s   
 	i(((	++D,<== Jt/   t $ 	 	 	'(8)DD Jt/   tt 	 ' -	F -	FH!H,I ((**    &F#--w-??EEGG 
 !w(?(?@@JJLLT111LLK!ss  
 E$((000/BBw<</11 0 01XxXX3w<<XXXXY 
 y>>"9#8#89I#J#JKKHH"L1   #&y'<'<TY[['I'I#J#J#'(?&5   	 ""Hg#6777 F F F4iEEEEEEEEF  	4!, 	 	HgOOKXKK'KK    	3'';'''	
 	
 	

 {{8$$$s   ;A AB'&B'<C
CC#*JA&J7A8J0'HJI=*8I#"I=#I74I=6I77I=:J<I==J
K
#KK
r-   )r#   N)__name__
__module____qualname____doc__r   r^   r7   r5   r   r   rV   rb   rN   r   r   r_   ra   boolrp   r   rO   r   r8   r'   r*   r*   G   s       
 
( (HSM ( ( ( (   ,// S>/ 
#	/ / / /0  $(cN 	       (C SY    <8s 8D	 8 8 8 8&T d    >F F$ F F F FU%4 U%HSM U% U% U% U% U% U%r8   r*   )r   rC   loggingr.   rx   pathlibr   typingr   r   r   r   agent.prompt_builderr   	getLoggerr   r   r=   r   r[   r`   ro   	frozensetr   r   r(   r*   r   r8   r'   <module>r      sM      				        + + + + + + + + + + + + 6 6 6 6 6 6		8	$	$
    211   
  i ! ! !   D T d    M% M% M% M% M% M% M% M% M% M%r8   