
    `gj:                       U d Z ddlm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mZ ddlZddlmZ  ej"                  e      Z ej(                  d      Z ej(                  d      Zi ad	ed
<   daded<   ddZddZddZddZd dZd!dZd!dZ d"dZ!d#dZ"d$dZ#	 	 	 d%	 	 	 	 	 	 	 	 	 d&dZ$d'dZ%	 	 	 d(	 	 	 	 	 	 	 	 	 	 	 d)dZ&d'dZ'd*dZ(y)+u  Skill bundles — aliases that load multiple skills under one slash command.

A skill bundle is a small YAML file that names a set of skills to load
together. Invoking ``/<bundle-name>`` from the CLI or gateway loads every
referenced skill's full content into a single user message, the same way
``/<skill-name>`` does — but for N skills at once.

Storage
-------
Bundles live in ``~/.hermes/skill-bundles/*.yaml`` (and the equivalent
profile-aware directory under ``HERMES_HOME``). Each file looks like::

    name: backend-dev
    description: Backend feature work — code review, testing, PR workflow.
    skills:
      - github-code-review
      - test-driven-development
      - github-pr-workflow
    instruction: |
      Optional extra guidance to inject above the skill bodies.

The file's stem is treated as a fallback name when ``name:`` is absent, so
dropping a YAML into the directory is enough to register a new bundle.

Conflict resolution
-------------------
If a bundle and a skill share the same slash name, the bundle wins. The
slash command dispatch checks bundles first, then falls back to skills.
This is the intended behavior — a user who names a bundle ``research``
explicitly wants ``/research`` to mean their bundle, not whatever skill
happens to share the slug.

Public API
----------
- :func:`get_skill_bundles` — return ``{"/slug": bundle_info}``
- :func:`resolve_bundle_command_key` — map a user-typed command to its slug
- :func:`build_bundle_invocation_message` — produce the full user message
- :func:`reload_bundles` — re-scan disk and return a diff
- :func:`list_bundles` — return rich info for display (``hermes bundles``)
- :func:`save_bundle` / :func:`delete_bundle` — file-level operations
    )annotationsN)Path)AnyDictListOptionalTuple)get_hermes_homez
[^a-z0-9-]z-{2,}Dict[str, Dict[str, Any]]_bundles_cachezOptional[float]_bundles_cache_mtimec                     t         j                  j                  d      } | rt        |       j	                         S t               dz  S )zReturn the canonical bundles directory under HERMES_HOME.

    Honors ``HERMES_BUNDLES_DIR`` for tests; falls back to
    ``<HERMES_HOME>/skill-bundles``.
    HERMES_BUNDLES_DIRzskill-bundles)osenvirongetr   
expanduserr
   )overrides    F/root/.hermes/venv/lib/python3.12/site-packages/agent/skill_bundles.py_bundles_dirr   B   s;     zz~~23HH~((**..    c                    | j                         j                  dd      j                  dd      }t        j                  d|      }t        j                  d|      j                  d      }|S )N -_ )lowerreplace_BUNDLE_INVALID_CHARSsub_BUNDLE_MULTI_HYPHENstrip)namecmds     r   _slugifyr%   N   s[    
**,

sC
(
0
0c
:C

#
#B
,C

"
"3
,
2
23
7CJr   c                     t               } | j                         sg S g }dD ]+  }|j                  t        | j	                  |                   - |S )N)z*.yamlz*.yml)r   existsextendsortedglob)basefilesexts      r   _iter_bundle_filesr.   U   sI    >D;;=	E" -VDIIcN+,-Lr   c                J   t               }g }|j                         r*	 |j                  |j                         j                         | D ],  }	 |j                  |j                         j                         . |rt        |      S dS # t
        $ r Y Kw xY w# t
        $ r Y Xw xY w)zHighest mtime across the bundle files plus the dir itself.

    Watching the directory mtime catches deletions; watching individual
    files catches edits. Together they're a cheap freshness check.
    g        )r   r'   appendstatst_mtimeOSErrormax)r,   r+   mtimesfs       r   
_max_mtimer7   _   s     >DF{{}	MM$))+../  	MM!&&(++,
 !3v;)c)  		
  		s#   )B )B	BB	B"!B"c                ~   	 | j                  d      }	 t	        j
                  |      }t        |t              st        j                  d|        yt        |j                  d      xs | j                        j                         }|st        j                  d|        y|j                  d	      xs g }t        |t              r|st        j                  d
|        y|D cg c]5  }t        |      j                         st        |      j                         7 }}|st        j                  d|        yt        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }t        |      }	|	st        j                  d|        y||	|xs dt        |       d||t        |       dS # t        $ r!}t        j                  d| |       Y d}~yd}~ww xY w# t        j                  $ r!}t        j                  d| |       Y d}~yd}~ww xY wc c}w )u   Parse a single bundle YAML file. Returns ``None`` on any error.

    Errors are logged at WARNING level. We don't raise — a broken bundle
    shouldn't take down slash command discovery.
    utf-8encodingzCould not read bundle %s: %sNzInvalid YAML in bundle %s: %sz$Bundle %s is not a mapping; skippingr#   zBundle %s has no name; skippingskillsz&Bundle %s has no skills list; skippingz)Bundle %s has empty skills list; skippingdescriptionr   instructionz&Bundle %s yielded empty slug; skippingzLoad z skills as a bundle)r#   slugr=   r<   r>   path)	read_textr3   loggerwarningyaml	safe_load	YAMLError
isinstancedictstrr   stemr"   listr%   len)
r@   rawexcdatar#   r<   sr=   r>   r?   s
             r   _load_bundle_filerQ   t   s   nngn.~~c" dD!=tDtxx,499-335D8$?XXh%2Ffd#6?F&,?Ac!flln?F?BDIdhh}-34::<Kdhh}-34::<KD>D?F "Nc&k]:M&N"D	 E  5tSA
 >> 6cB  @s:   G H "H:H:	H G;;H H7H22H7c                     t               } i }| D ]@  }t        |      }|sd|d    }||v rt        j                  d||||   d          <|||<   B |at        |       a|S )u   Scan the bundles directory and rebuild the cache.

    Returns the same mapping as :func:`get_skill_bundles` — ``"/slug"`` →
    bundle info dict. Later bundles with a duplicate slug are skipped with
    a warning (first wins, alphabetical order).
    /r?   z,Duplicate bundle slug %s from %s; keeping %sr@   )r.   rQ   rB   rC   r   r7   r   )r,   outr6   infokeys        r   scan_bundlesrW      s      E%'C  #$v, #:NN>QC( C N%e,Jr   c                 j    t               } t        |       }t        r	t        |k7  r
t	                t        S )zReturn the current bundle mapping, rescanning when disk changed.

    Cheap to call repeatedly: only rescans when the bundles directory or
    any bundle file's mtime is newer than the cached snapshot.
    )r.   r7   r   r   rW   )r,   current_mtimes     r   get_skill_bundlesrZ      s,      Eu%M1]Br   c                R    | syd| j                  dd       }|t               v r|S dS )zResolve a user-typed command to its canonical bundle slash key.

    Hyphens and underscores are treated interchangeably to mirror the
    skill-command behavior (Telegram converts hyphens to underscores in
    bot command names).
    NrS   r   r   )r   rZ   )commandcmd_keys     r   resolve_bundle_command_keyr^      s8     '//#s+,-G!2!447>$>r   c                    dd}  | t               }t               } | |      }t        t        |      t        |      z
        }t        t        |      t        |      z
        }t        t        |      t        |      z        }|D cg c]
  }|||   d c}|D cg c]
  }|||   d c}|t	        |      dS c c}w c c}w )zRe-scan the bundles directory and return a diff.

    Mirrors :func:`agent.skill_commands.reload_skills` so callers can use
    the same display logic. Returns a dict with ``added``, ``removed``,
    ``unchanged``, and ``total`` keys.
    c           	         | j                         D ci c]+  \  }}|j                  d      |xs i j                  dd      - c}}S c c}}w )NrS   r=   r   )itemslstripr   )cmdskvs      r   	_snapshotz!reload_bundles.<locals>._snapshot   s<    LPJJLYDAqR}}]B??YYYs   0A)r#   r=   )addedremoved	unchangedtotal)rc   r   returnzDict[str, str])r   rW   r)   setrL   )rf   beforenewafteradded_namesremoved_namesri   ns           r   reload_bundlesrs      s    Z ~&F
.CcNEUc&k12K3v;U34Ms5zCK/0I ALL11U1X6LCPQaQvay9QU	 LQs   B;C c                 N    t               } t        | j                         d       S )z6Return a sorted list of bundle info dicts for display.c                    | d   S )Nr?    )bs    r   <lambda>zlist_bundles.<locals>.<lambda>   s
    !F) r   )rV   )rZ   r)   values)bundless    r   list_bundlesr{      s    !G'.."(;<<r   c           	     0   t               }|j                  |       }|syddlm}m} 	 ddlm}  ||      }	g }
g }g }g }t               }|d   }|d   }|j                  d      xs d	}|D ]  }|xs d	j                         }|r||v r|j                  |        |||
      }|s|j                  |       M|\  }}}||	v s||	v r|j                  |xs |       q	 ddlm}  ||       d| d}|j                   |||||             |
j                  |        |syd| dt        |
       dd	d| ddj                  |
       g}|r#|j                  ddj                  |              |r#|j                  ddj                  |              |r|j!                  d	d| g       |r|j!                  d	d| g       dj                  |      }dj                  |g|      |
|fS # t        $ r t               }	Y w xY w# t        $ r Y *w xY w)u  Build the user message content for a bundle slash command invocation.

    Returns ``(message, loaded_skill_names, missing_skill_names)`` or
    ``None`` if the bundle wasn't found.

    A bundle that references skills the user doesn't have installed still
    loads — the agent gets a note about which ones were skipped. This is
    the same forgiving stance ``build_preloaded_skills_prompt`` uses for
    ``-s`` CLI preloading.

    Disabled skills are also skipped: bundles load members via
    ``_load_skill_payload`` directly, bypassing the scan-time disabled
    filter in ``get_skill_commands()``, so the disabled list must be
    re-applied here.  ``platform`` scopes the check to a specific
    platform's ``skills.platform_disabled`` config (gateway dispatch
    passes it explicitly because the gateway handles multiple platforms
    in one process); when *None*, the platform resolves from session env
    vars and the global disabled list still applies.  Mirrors the
    stacked-skill gate in gateway dispatch (#58888).
    Nr   )_load_skill_payload_build_skill_message)get_disabled_skill_names)platformr#   r<   r>   r   )task_id)bump_usez[Loaded as part of the "z" skill bundle.])
session_idz&[IMPORTANT: The user has invoked the "z" skill bundle, loading zL skills together. Treat every skill below as active guidance for this turn.]zBundle: zSkills loaded: z, zSkills missing (skipped): z-Skills disabled for this platform (skipped): zBundle instruction: zUser instruction: 
z

)rZ   r   agent.skill_commandsr}   r~   agent.skill_utilsr   	Exceptionrl   r"   addr0   tools.skill_usager   rL   joinr(   )r]   user_instructionr   r   rz   rU   r}   r~   r   disabled_namesloaded_namesmissingdisabledskill_blocksseenbundle_namer<   extra_instructionskill_id
identifierloadedloaded_skill	skill_dir
skill_namer   activation_noteheader_linesheaders                               r   build_bundle_invocation_messager      s   4  !G;;wD O>18D !LGH LUDv,K(^F/52 #(n"++-
Z4/$ZANN:&.4+i ':+GOOJ4*5	2Z 
 '{m3CD 	 	 "		
 	J'G#(J 
 1 >|$% &-	- 	
;- 
$))L123L 879K8LMN;DIIh<O;PQ	
 R#78I7J!KLM%&6%789	
 YY|$FKK/,/0,HH[  D  		s#   G. H.HH	HHc                Z    t        |       }|st        d| d      t               | dz  S )z7Return the canonical filesystem path for a bundle name.zBundle name z normalizes to an empty slugz.yaml)r%   
ValueErrorr   r#   r?   s     r   bundle_path_forr   x  s6    D>D<x/KLMM>tfEN**r   c                   | xs dj                         } | st        d      |D cg c]5  }t        |      j                         st        |      j                         7 }}|st        d      t        |       }|j	                         r|st        d|       |j                  j                  dd       | |d}|r||d<   |r||d	<   |j                  t        j                  |d
d      d       t                |S c c}w )zWrite a bundle to disk and invalidate the cache.

    Raises ``FileExistsError`` if the target exists and ``overwrite`` is
    False. Raises ``ValueError`` if the inputs are unusable.
    r   zBundle name is requiredz(Bundle must reference at least one skillzBundle already exists at T)parentsexist_ok)r#   r<   r=   r>   F)	sort_keysallow_unicoder9   r:   )r"   r   rI   r   r'   FileExistsErrorparentmkdir
write_textrD   	safe_dumprW   )	r#   r<   r=   r>   	overwriterP   cleaned_skillsr@   payloads	            r   save_bundler     s     JBD233.4GAc!fllnGNGCDD4 D{{}Y 9$@AAKKdT2'+~FG!,!,OOw%tD   NK) Hs   C?C?c                    t        |       }|j                         st        d|       |j                          t	                |S )zvDelete a bundle by name. Returns the deleted path.

    Raises ``FileNotFoundError`` if the bundle doesn't exist.
    zNo bundle at )r   r'   FileNotFoundErrorunlinkrW   )r#   r@   s     r   delete_bundler     s<    
 4 D;;=-v 677KKMNKr   c                P    t        |       }t               j                  d|       S )z+Look up a bundle by name (slug-normalized).rS   )r%   rZ   r   r   s     r   
get_bundler     s%    D>D""Qtf:..r   )rk   r   )r#   rI   rk   rI   )rk   
List[Path])r,   r   rk   float)r@   r   rk   Optional[Dict[str, Any]])rk   r   )r\   rI   rk   zOptional[str])rk   zDict[str, Any])rk   zList[Dict[str, Any]])r   NN)
r]   rI   r   rI   r   
str | Noner   r   rk   z*Optional[Tuple[str, List[str], List[str]]])r#   rI   rk   r   )r   r   F)r#   rI   r<   z	List[str]r=   rI   r>   rI   r   boolrk   r   )r#   rI   rk   r   ))__doc__
__future__r   loggingr   repathlibr   typingr   r   r   r   r	   rD   hermes_constantsr
   	getLogger__name__rB   compiler   r!   r   __annotations__r   r   r%   r.   r7   rQ   rW   rZ   r^   rs   r{   r   r   r   r   r   rv   r   r   <module>r      sJ  (T #  	 	  3 3  ,			8	$ #

=1 !rzz(+ ,.) .(, o ,	/**1h6

?4= 	sIsIsI sI 	sI
 0sIv+ #
## # 	#
 # 
#L
/r   