
    Pmj[                      d Z ddlmZ ddlZddlZddlZddlZddlZddlZ	ddl
m
Z
mZ ddlmZmZ ddlmZmZmZmZmZmZ  ej        e          ZdZdZd	Zd
ZdZdZdZdZ dZ!dZ"dZ#dZ$ddZ%ddZ&ddZ'ddZ(dZ) G d  d!e*          Z+dd#Z,dd$Z-dd&Z.d'Z/dd)Z0h d*Z1h d+Z2dd/Z3dd3Z4dd4Z5dd5Z6dd6Z7dd8Z8dd:Z9dd<Z:dd=Z; G d> d?          Z<ddAZ=ddFZ>ddGddNZ?ddOZ@ddPZAddRZB G dS dTe*          ZC G dU dVe*          ZD G dW dX          ZEddYZFddZZGdd[ZHdd]ZId^d_ddcZJddeZKedfddiZLddjddmZMddnZNddpZOd^d_ddsZPd^d_ddvZQddwZRddyZS	 ddddzd{ddZTddZUddZVddZWddZX	 dddddZYddZZdzdddZ[ddZ\ddZ]ddZ^dZ_ddZ`ddZadZbddZcddZdddZe	 dddddZfddZgddZhddZiddZjddZkdddZlddZmddZn	 ddddddZoddZpddZqdS )u  
Skill Sync client -- the low-level sync layer.

This is the LOW-LEVEL sync layer. It builds content-addressed objects
(blob/tree/commit) from local skills, talks the sync wire contract to a sync
plane (push objects + CAS a ref, pull the owner's HEAD, three-way merge on a
409), and is driven by:

  * a debounced push hook in ``skill_manage`` (after the write-gate passes),
  * a periodic pull hook (``maybe_pull_skills``) at the curator tick sites,
  * the ``hermes sync status|pull|push|now`` CLI.

It lives beside ``tools/skills_sync.py`` (NOT under ``hermes_cli/``) so the
low-level sync layer never imports the CLI -- same rule the bundled-skills
sync module documents at ``skills_sync.py:43-50``.

Contract: the Skill Sync wire contract (version 1, frozen
for Milestone 1). Endpoint shapes, object model, canonicalization, and status
codes below all trace to that document.

--- ACCESS GATE (pre-launch) ---------------------------------------------
Client sync is INERT (no push, no pull, no-op) unless the signed-in user is a
**Nous admin**. We read that off the access token, which rides on the same
bearer ``resolve_nous_runtime_credentials()`` returns; we decode the JWT
payload (no signature verification -- the server re-verifies) and check the
claim before doing any sync work.

NAMING: the claim on the wire is ``tool_gateway_admin``, which is misleading
-- it is NOT a tool-gateway-specific right. NAS populates it from
``Permissions.ADMIN_ACCESS`` (access-token-issuer.ts), the same global portal
admin permission that guards ``/admin/*``; the claim is simply named for its
first consumer. We keep the wire name (other services read it) but call it
what it means everywhere on this side.

This gate is pre-launch containment, not the shipping entitlement. Admin
status conflates "may administer Nous" with "has Skill Sync enabled", and has
no middle setting for a beta cohort -- opening it up would mean handing out
portal admin. Replace it with a real entitlement (a ``sync:*`` scope, a tier
check, or a per-cohort feature flag) before shipping to users.

--- OPT-IN DEFAULT (M1-D, provisional) -----------------------------------
Nothing syncs unless the user marks a skill for sync. The user's local intent
is toggled via ``hermes sync enable/disable`` (a ``sync`` flag on the skill's
``.usage.json`` sidecar, alongside ``pinned``/``created_by``), but the DURABLE,
CROSS-DEVICE opt-in state is a committed ``sync-manifest`` object in the sync
plane (design.md §2.8): a root-level blob in the tree at
``refs/user/<owner>/HEAD`` recording per-skill ``{name, enabled}``. Push writes
the manifest from local intent; pull reconciles local intent FROM it, so a skill
opted in on one device becomes opted in on the others. The plane manifest is
authoritative; the local flag is just the editable intent. Only agent-created +
user-authored skills under ``~/.hermes/skills/`` are eligible; bundled and
hub-installed skills are excluded.
    )annotationsN)datetimetimezone)PathPurePosixPath)AnyCallableDictListOptionalTuple1i  blobtreecommitfileexecdirskillzsync-manifest   skillsDict[str, bool]returnbytesc                    t           t          d t          |                                           D             d}t	          |          S )a5  Serialize the per-skill opt-in map into canonical ``sync-manifest`` bytes.

    ``skills`` maps skill name -> enabled. Emits the shape gateway-gateway's
    ``parseSyncManifest`` validates: ``{type, version:1, skills:[{name,enabled}]}``.
    Skill entries are sorted by name for a stable content address.
    c                8    g | ]\  }}|t          |          d S ))nameenabled)bool).0r   r   s      >/home/thesage/.hermes/hermes-agent/tools/skills_sync_client.py
<listcomp>z-build_sync_manifest_bytes.<locals>.<listcomp>   s:     
 
 
g d7mm44
 
 
    )typeversionr   )SYNC_MANIFEST_TYPESYNC_MANIFEST_VERSIONsorteditemscanonical_json_bytes)r   manifests     r!   build_sync_manifest_bytesr,   v   sR     #(
 
!'!7!7
 
 
 H  )))r#   dataOptional[Dict[str, bool]]c                   	 t          j        |                     d                    }n# t          $ r Y dS w xY wt	          |t
                    sdS |                    d          t          k    rdS |                    d          t          k    rdS |                    d          }t	          |t                    sdS i }|D ]{}t	          |t
                    s dS |                    d          }|                    d          }t	          |t                    r|s dS t	          |t                    s dS |||<   ||S )u  Parse ``sync-manifest`` bytes into ``{name: enabled}``, or ``None`` if the
    bytes are not a well-formed manifest.

    Strict (mirrors gateway-gateway ``parseSyncManifest``): an unknown ``type``,
    a missing/!=1 ``version``, a non-array ``skills``, or a malformed skill entry
    all reject rather than being coerced — a malformed manifest must not be
    mistaken for "no skills opted in."
    utf-8Nr$   r%   r   r   r   )jsonloadsdecode	Exception
isinstancedictgetr&   r'   liststrr   )r-   value
raw_skillsoutrawr   r   s          r!   parse_sync_manifestr>      sU   
4;;w//00   tteT"" tyy...tyy444t8$$Jj$'' tC 	 	#t$$ 	44wwv'')$$$$$ 	D 	44'4(( 	44D		Js   '* 
88r9   c                T    dt          j        |                                           z   S )z;Return ``sha256:<64-hex>`` -- the wire address of ``data``.zsha256:)hashlibsha256	hexdigest)r-   s    r!   wire_addressrC      s#    w~d++557777r#   objDict[str, Any]c                X    t          j        | ddd                              d          S )a  Canonical JSON serialization for tree/commit hashing (sync contract).

    UTF-8, keys sorted lexicographically, no insignificant whitespace
    (``separators=(",", ":")``), no trailing newline. Arrays must already be
    in the contract-specified order by the caller (tree entries by ``name``,
    commit ``parents`` in significance order). Both client and server MUST
    produce byte-identical output or a push fails ``422 hash_mismatch``.
    T),:F)	sort_keys
separatorsensure_asciir0   )r1   dumpsencode)rD   s    r!   r*   r*      s6     :	  
 fWoor#   tool_gateway_adminc                      e Zd ZdZdS )SyncInertErrorzRaised (and caught by the gate-and-swallow hooks) when sync must no-op:

    not logged in, no bearer, or the caller is not a Nous admin.
    N)__name__
__module____qualname____doc__ r#   r!   rP   rP      s           r#   rP   tokenc                    	 ddl }|                    | ddd          pi S # t          $ r'}t                              d|           i cY d}~S d}~ww xY w)a9  Decode a JWT payload WITHOUT signature verification.

    Safe here: we never trust these claims for authz -- the server re-verifies
    every call. We only read the dev-gate claim to decide whether to attempt
    sync at all. Mirrors the diagnostic decode in
    plugins/dashboard_auth/nous/__init__.py:463.
    r   NF)verify_signature
verify_exp)optionsz1skills_sync_client: JWT payload decode failed: %s)jwtr3   r4   loggerdebug)rV   r[   es      r!   _decode_jwt_payload_unverifiedr_      s    	


zz).eDD  
 
  	    H!LLL						s   " 
AAAAc                    	 ddl m}   |             }n%# t          $ r}t          d|           |d}~ww xY w|pi                     d          }|st          d          t          |          }|                    d          p+|                    d          p|                    d	          pd
}|                    t                    du }||pi                     d          t          |          ||dS )us  Resolve the Nous bearer + owner + dev-gate flag.

    Returns a dict: ``{api_key, base_url, owner, nous_admin, claims}``.
    Raises :class:`SyncInertError` if not logged in / no bearer.

    ``owner`` is the token-verified subject; the server derives the real owner
    from the bearer regardless (contract §0.4), so this is advisory for local
    ref naming only.
    r   ) resolve_nous_runtime_credentialszno Nous credentials: Napi_keyzno bearer token availablesub	privy_didtidunknownTbase_url)rb   rg   owner
nous_adminclaims)hermes_cli.authra   r4   rP   r7   r_   NOUS_ADMIN_CLAIMr9   )ra   credsr^   rb   rj   rh   ri   s          r!   resolve_identityrn      s8   ADDDDDD0022 A A A8Q8899q@A {	**G :8999+G44F

5 	::k""	::e	 	 
 ,--5J[b%%j11U   s    
505r   c                     	 t          t                                          d                    S # t          $ r Y dS t          $ r&} t
                              d|            Y d} ~ dS d} ~ ww xY w)z3Whether the access gate permits sync. Never raises.ri   Fz2skills_sync_client: dev_gate_open check failed: %sN)r   rn   r7   rP   r4   r\   r]   r^   s    r!   dev_gate_openrq     s    $&&**<88999   uu   I1MMMuuuuus   -0 
A,	A,A''A,z(https://gateway-gateway.nousresearch.comOptional[str]c                 :   t          j        d          } | r;|                                 r'|                                                     d          S 	 ddlm}  |            pi }|                    d          pi }|                    d          }t          |t                    r;|                                r'|                                                    d          S n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wt          pdS )	u  Resolve the sync-plane base URL.

    Order: HERMES_SYNC_BASE_URL env bridge -> config.yaml ``sync.base_url`` ->
    the production plane. Returns a base without a trailing slash (e.g.
    ``https://host``); the ``/v1/sync/`` prefix is appended by the client.

    The production default means a normal user never configures a URL — the
    env var and config key exist to point a dev/staging build at another
    plane. Returns None only if the default is somehow blanked out.
    HERMES_SYNC_BASE_URL/r   load_configsyncrg   z8skills_sync_client: config sync.base_url read failed: %sN)osgetenvstriprstriphermes_cli.configrw   r7   r5   r9   r4   r\   r]   DEFAULT_SYNC_BASE_URL)envrw   cfgsync_cfgbaser^   s         r!   resolve_sync_base_urlr   3  s'    )*
+
+C
 'syy{{ 'yy{{!!#&&&T 	211111kmm!r776??(b||J''dC   	,TZZ\\ 	,::<<&&s+++ T T TOQRSSSSSSSST (D(s   BC" "
D,DD>   r   onyestrue>    0noofffalser:   r   Optional[bool]c                    t          | t                    r| S | dS t          |                                                                           }|t
          v rdS |t          v rdS dS )zParse a config/env bool. Returns None if unrecognized (so callers can
    fall through to the next precedence layer). Accepts real bools + strings.NTF)r5   r   r9   r{   lower_TRUE_FALSE)r:   ss     r!   _parse_boolr   f  si     % }tE

  ""AEzztF{{u4r#   env_var
config_keydefaultc               X   t          t          j        |                     }||S 	 ddlm}  |            pi }|                    d          pi }t          |                    |                    }||S n3# t          $ r&}t                              d||           Y d}~nd}~ww xY w|S )zMResolve a boolean sync knob: ``env_var`` -> ``sync.<config_key>`` -> default.Nr   rv   rx   z2skills_sync_client: config sync.%s read failed: %s)	r   ry   rz   r}   rw   r7   r4   r\   r]   )	r   r   r   env_valrw   r   r   cfg_valr^   s	            r!   _sync_config_boolr   u  s    ")G,,--G	Z111111kmm!r776??(bhll:6677N  Z Z ZI:WXYYYYYYYYZNs   AA7 7
B'B""B'c                 &    t          ddd          S )u  Whether the sync feature is turned on for this instance (env-first).

    ``HERMES_SYNC_ENABLED`` -> ``sync.enabled`` -> False. This is the master
    switch a Hermes Cloud deployment sets to opt its instances into sync by
    default. It is checked by the gate-and-swallow entrypoints IN ADDITION to
    the Nous-admin token gate and a configured base URL — all three must hold for
    background sync to run.
    HERMES_SYNC_ENABLEDr   Fr   r   rU   r#   r!   sync_feature_enabledr     s     2IuMMMMr#   c                 &    t          ddd          S )a~  Whether an agent/user edit to an org skill is proposed automatically.

    ``HERMES_SYNC_ORG_AUTO_PROPOSE`` -> ``sync.org_auto_propose`` -> False.

    False (default): edits to an org-shared skill stay LOCAL until the user
    runs ``hermes sync propose <skill>``. The skill keeps working with the
    edit applied; the organisation just doesn't see it yet.

    True: every local edit to an org skill is submitted to the org as a
    proposal right away (an admin still approves it, unless the editor is an
    admin). Suits a small, high-trust team that wants improvements to flow
    back without anyone remembering to push them.
    HERMES_SYNC_ORG_AUTO_PROPOSEorg_auto_proposeFr   r   rU   r#   r!   sync_org_auto_proposer     s#     &(:E   r#   c                 &    t          ddd          S )u  The personal sync default opt-in policy (env-first).

    ``HERMES_SYNC_DEFAULT_OPT_IN`` -> ``sync.default_opt_in`` -> False.

    False (default): opt-IN — a skill syncs only after an explicit
    ``hermes sync enable`` (or a plane manifest that opted it in). True: opt-OUT
    — every sync-eligible skill is treated as opted in unless explicitly
    disabled, which is the "your skills follow you with no setup" default a
    Hermes Cloud deployment wants. Per the design notes, this default is
    provisional and expected to flip; exposing it as env config lets the
    operator choose per deployment without a protocol change.
    HERMES_SYNC_DEFAULT_OPT_INdefault_opt_inFr   r   rU   r#   r!   sync_default_opt_inr     s     9;KUZ[[[[r#   r   c                 (    ddl m}   |             dz  S )Nr   get_hermes_homer   )hermes_constantsr   r   s    r!   _skills_dirr     s'    000000?x''r#   
skill_namec                   	 ddl m}m}m} ddlm} n# t          $ r Y dS w xY w ||           s ||           rdS  ||           }|dS  ||          rdS 	 |                                                    t                                                                }|j
        r|j
        d         t          k    rdS n# t          t          f$ r Y nw xY wdS )u  Whether *skill_name* is a candidate for sync (before the opt-in check).

    Eligible = present locally under ~/.hermes/skills/, NOT bundled, NOT
    hub-installed, NOT an external-dir skill, and NOT under the org mirror
    (``_org/`` — enterprise-managed content pulls from the org HEAD and must
    never ride a personal push; the sync contract / the design notes). Mirrors the
    exclusion logic used by the curator (tools/skill_usage.py).
    r   )
is_bundledis_hub_installed_find_skill_dir)is_external_skill_pathFNT)tools.skill_usager   r   r   agent.skill_utilsr   r4   resolverelative_tor   partsORG_DIR_NAMEOSError
ValueError)r   r   r   r   r   	skill_dirrels          r!   is_sync_eligibler     s6   SSSSSSSSSS<<<<<<<   uuz* !1!1*!=!= u
++Iui(( u!!--kmm.C.C.E.EFF9 	1555Z    4s    
!!A"B> >CC	List[str]c                    	 ddl m}  n# t          $ r g cY S w xY w |             pi }t                      rg }t	                      D ]h}|                    |          }t          |t                    r|                    d          du rDt          |          r|	                    |           it          t          |                    S g }|                                D ]U\  }}t          |t                    r;|                    d          du r$t          |          r|	                    |           Vt          t          |                    S )u]  Return the names of skills that should sync, honoring the opt-in policy.

    Two policies (``sync_default_opt_in()``, env-first — see that function):

    - **opt-in (default):** a skill syncs only when its usage record carries
      ``sync: true`` AND it is eligible. Nothing syncs by default.
    - **opt-out (Hermes Cloud "on by default"):** every *eligible* skill syncs
      UNLESS its usage record explicitly carries ``sync: false``. This is what a
      deployment sets (via ``HERMES_SYNC_DEFAULT_OPT_IN``) so a user's skills
      follow them with no per-skill setup.

    Sorted, deduped.
    r   )
load_usagerx   FT)r   r   r4   r   _all_local_skill_namesr7   r5   r6   r   appendr(   setr)   )r   usagenamesr   recs        r!   list_synced_skill_namesr     se   0000000   			JLLBE 	"*,, 	# 	#D))D//C#t$$ E)A)A%% #T"""c%jj!!! E[[]]  	cc4   	SWWV__%<%<AQRVAWAW%<LL#e**s   	 c                    g } t                      }	 |                                sg S |                    d          D ]j}|                                rd}	 ddlm}  |||j        j                  }n# t          $ r |j        j        }Y nw xY w|r| 	                    |           kn2# t          $ r%}t                              d|           Y d}~nd}~ww xY wt          t          |                     S )ae  Best-effort enumeration of every locally-present skill name (used by the
    opt-out policy). A skill is any directory under ~/.hermes/skills/ containing
    a ``SKILL.md``; the name is its frontmatter ``name`` (falling back to the
    directory name). Eligibility (bundled/hub/external exclusion) is applied by
    the caller via ``is_sync_eligible``.
    SKILL.mdNr   )_read_skill_namez6skills_sync_client: local skill enumeration failed: %s)r   existsrglob
is_symlinkr   r   parentr   r4   r   r   r\   r]   r(   r   )r   rootskill_mdr   r   r^   s         r!   r   r   	  s:    E==DR{{}} 	I

:.. 	# 	#H""$$ "&D,>>>>>>''(/2FGG , , ,+, #T"""	#  R R RMqQQQQQQQQR#e**s@   B) .B) A43B) 4B
B) BB) )
C3CCc                  *    e Zd ZdZddZdd	ZddZdS )	ObjectSetzAccumulates objects to push: hash -> (kind, bytes).

    Deduped by content address, so identical blobs across skills upload once.
    r   Nonec                    i | _         d S N)objectsselfs    r!   __init__zObjectSet.__init__4  s    57r#   kindr9   r-   r   c                ^    t          |          }| j                            |||f           |S r   )rC   r   
setdefault)r   r   r-   addrs       r!   addzObjectSet.add7  s0    D!!tTl333r#   intc                *    t          | j                  S r   )lenr   r   s    r!   __len__zObjectSet.__len__<  s    4<   r#   Nr   r   )r   r9   r-   r   r   r9   )r   r   )rQ   rR   rS   rT   r   r   r   rU   r#   r!   r   r   .  sZ         
8 8 8 8   
! ! ! ! ! !r#   r   pathc                    	 |                                  j        t          j        t          j        z  t          j        z  z  rt          S n# t          $ r Y nw xY wt          S )u   Return the tree mode for a regular file: ``exec`` if +x else ``file``
    (contract §2.3). No symlinks / other modes are emitted.)	statst_mode_statS_IXUSRS_IXGRPS_IXOTH	MODE_EXECr   	MODE_FILE)r   s    r!   
_file_moder   @  s`    99;;%-%-"?%-"OP 		   s   AA
 

AAdir_pathr   max_object_bytesr   c          	     B   g }t          |                                 d           D ]/}|                                rt                              d|           3|                                r<t          |||          }|                    |j        t          |t          d           |                                r|                                }t          |          |k    r%t          d| dt          |           d|           |                    t           |          }|                    |j        t           |t#          |          d           1|                    d	            t          |d
}|                    t          t'          |                    S )uv  Recursively build objects for *dir_path*; return the tree address.

    Regular files become blobs; subdirectories become nested trees. Symlinks,
    sockets, and other special files are skipped (contract §2.3 security: no
    symlinks). Blobs over *max_object_bytes* raise :class:`ValueError` so the
    caller can surface / skip the artifact (contract §4.3 -> 413).
    c                    | j         S r   )r   )ps    r!   <lambda>zbuild_tree.<locals>.<lambda>T  s    !& r#   keyz'skills_sync_client: skipping symlink %sr   r   r   hashmodezfile  is z bytes > max_object_bytes c                    | d         S Nr   rU   rp   s    r!   r   zbuild_tree.<locals>.<lambda>o  
    qy r#   r$   entries)r(   iterdirr   r\   r]   is_dir
build_treer   r   	KIND_TREEMODE_DIRis_file
read_bytesr   r   r   	KIND_BLOBr   sortr*   )	r   r   r   r   childsub_hashr-   	blob_hashtree_objs	            r!   r  r  K  s    %'G((**0@0@AAA   	LLBEJJJ<<>> 	!%CSTTTHNNYRZ[[    ]]__ 	##%%D4yy+++ *E * *s4yy * *'* *    It44INN!J%%&u--	    LL((L)))!g66H;;y"6x"@"@AAAr#   )ts	tree_hashparentsrh   devicemessager  c                   t           | t          |          ||d|p0t          j        t          j                                      d          |t          d}|                    t           t          |                    S )a   Build a commit object (sync contract) and return its address.

    ``parents``: 0 for first commit, 1 for a normal edit, 2 for a merge commit
    (order significant: parents[0] = base fast-forwarded from, parents[1] =
    the other head being merged).
    )rh   r  z%Y-%m-%dT%H:%M:%SZ)r$   r   r  authorr  r  artifact_type)
KIND_COMMITr8   r   nowr   utcstrftimeARTIFACT_TYPE_SKILLr   r*   )r  r  rh   r  r  r   r  
commit_objs           r!   build_commitr  t  sr    " ==!V44MHL..778LMM, J ;;{$8$D$DEEEr#   c                    ddl } ddl}|                                j        dd         }	 |                                 pd}n# t
          $ r d}Y nw xY w|                    d          d                                         }d                    d |D                       pd}|r| d| n|                                j        S )zA human-friendly default device label: the short hostname plus a short
    random suffix for uniqueness (two machines can share a hostname). Falls back
    to a bare uuid if the hostname is unavailable/unusable.r   N   r   .c              3  J   K   | ]}|                                 s|d v |V  dS )z-_N)isalnum)r    cs     r!   	<genexpr>z(_default_device_label.<locals>.<genexpr>  s3      AA!		AqDyyAyyyyAAr#   -)	socketuuiduuid4hexgethostnamer   splitr{   join)r$  r%  suffixhostshorts        r!   _default_device_labelr.    s     MMMKKKZZ\\bqb!F!!##)r    JJsOOA$$&&EGGAAuAAAAAGRE"'=efTZZ\\-==s   A AAc                 "   t                      dz  } 	 |                                 r,|                     d                                          }|r|S n# t          $ r Y nw xY wddl}|j                            d          pd                                }|r|nt                      }	 | j	        
                    dd	           |                     |d           n2# t          $ r%}t                              d
|           Y d}~nd}~ww xY w|S )uD  Return a stable per-device label for commit ``author.device`` (contract
     -- advisory, never an auth input). Persisted under
    ~/.hermes/skills/.sync_device_id.

    New devices are seeded with a HUMAN-FRIENDLY default (short hostname + a
    short random suffix, e.g. ``bens-macbook-a1b2c3``) so the sync console shows
    something recognizable instead of an opaque hash. Existing ``.sync_device_id``
    files are honored verbatim (backward-compatible — a machine keeps its id).
    Use ``set_device_name()`` / ``hermes sync device --name`` to set an explicit
    label..sync_device_idr0   encodingr   NHERMES_SYNC_DEVICE_NAMEr   Tr  exist_okz3skills_sync_client: could not persist device id: %s)r   r   	read_textr{   r   ry   environr7   r.  r   mkdir
write_textr\   r]   )r   valry   env_namer^   s        r!   stable_device_idr<    s@    ==,,D;;== 	..'.2288::C 
    III
899?RFFHHH
;(($9$;$;CO$666g.... O O OJANNNNNNNNOJs)   ?A 
A! A!)3C 
D'DDr   c                    | pd                                 }|st          d          t                      dz  }|j                            dd           |                    |d           |S )uo  Set the human-friendly device label used for commit ``author.device``.

    Writes the (trimmed) name to ~/.hermes/skills/.sync_device_id, overwriting
    any previous value. The label is advisory metadata only — never an auth
    input (contract §2.4) — so any non-empty string is accepted. Returns the
    stored value. Raises ValueError on an empty name.
    r   z&device name must be a non-empty stringr0  Tr4  r0   r1  )r{   r   r   r   r8  r9  )r   cleanedr   s      r!   set_device_namer?    sv     zr  ""G CABBB==,,DKdT222OOGgO...Nr#   c                  *     e Zd ZdZddd	 fdZ xZS )
	SyncErrorz?A non-recoverable wire error (4xx that the client can't retry).Nstatusr  r9   rC  Optional[int]c               X    t                                          |           || _        d S r   )superr   rC  )r   r  rC  	__class__s      r!   r   zSyncError.__init__  s&    !!!r#   )r  r9   rC  rD  rQ   rR   rS   rT   r   __classcell__rG  s   @r!   rA  rA    sP        II@D            r#   rA  c                  $     e Zd ZdZd fdZ xZS )SyncConflicta[  CAS lost (409). NOT a rejection -- pushed objects are already durable.

    ``actual`` is the current head to merge against, or **None** when the ref
    does not exist server-side (the server reports that as an empty string).
    None means "there is nothing to merge against, retry as a create" -- it
    must never be fetched as an object.
    actualrr   c                ~    |pd | _         t                                          | j         r
d| j          nd           d S )NzCAS conflict; actual head z(CAS conflict; the ref does not exist yet)rM  rF  r   )r   rM  rG  s     r!   r   zSyncConflict.__init__  sS     &,^t{<6666;	
 	
 	
 	
 	
r#   )rM  rr   rH  rJ  s   @r!   rL  rL    sG         	
 	
 	
 	
 	
 	
 	
 	
 	
 	
r#   rL  c                  ~    e Zd ZdZddd&d	Zd'dZd(dZddd)dZddd*dZddd+dZ	ddd,dZ
ddd-dZd.d$Zd%S )/
SyncClientzJSync client bound to a base URL + bearer (routes under
    ``/v1/sync/``).g      >@timeoutrg   r9   rb   rR  floatc                   |                     d          | _        || _        || _        dd l}|                                | _        d| | j        j        d<   d S )Nru   r   zBearer Authorization)r|   r   rb   rR  requestsSession_sessionheaders)r   rg   rb   rR  rV  s        r!   r   zSyncClient.__init__  s\    OOC((	 ((**1D71D1Do...r#   r   r   c                @    | j          d|                    d           S )Nz	/v1/sync/ru   )r   lstrip)r   r   s     r!   _urlzSyncClient._url  s$    )88dkk#&6&6888r#   rE   c                    | j                             |                     d          | j                  }|j        dk    rt          d|j         |j                  |                                S )z<GET /v1/sync/capabilities (sync contract). No auth required.capabilitiesrQ     zcapabilities failed: rB  rX  r7   r\  rR  status_coderA  r1   )r   rs     r!   r^  zSyncClient.capabilities  sd    Mdii77NN=CCAMCCAMZZZZvvxxr#   F	org_scopeprefixrd  r   List[Dict[str, str]]c               T   |rdnd}|rdndi}| j                             |                     |          || j                  }|j        dk    rt          d|j         |j                  |                                pi                     dg           }|rfd	|D             }|S )
a  GET /v1/sync/refs?prefix=... (or the org route when ``org_scope``).

        Org refs live behind a SEPARATE endpoint, not behind a prefix filter on
        the personal one: the personal route is hard-scoped to the token's own
        owner, so asking it for ``refs/org/<id>/`` silently returns the
        caller's personal refs instead of an error. Callers reading an org ref
        MUST pass ``org_scope=True``.
        zorg/refsrefsNre  )paramsrR  r_  zget_refs failed: rB  c                    g | ]:}t          |                    d d                                                  8|;S )r   r   )r9   r7   
startswith)r    r_re  s     r!   r"   z'SyncClient.get_refs.<locals>.<listcomp>*  sB    TTT2RVVFB-?-?)@)@)K)KF)S)STBTTTr#   r`  )r   re  rd  r   ri  rb  rh  s    `     r!   get_refszSyncClient.get_refs  s     '2zzF":6(:MdiioofdlSS=C???VVVVB##FB// 	U UTTTTTTDr#   obj_hashTuple[str, bytes]c                  |rd| nd| }| j                             |                     |          | j                  }|j        dk    rt          d| dd          |j        dk    rt          d| d	d          |j        d
k    rt          d|j         |j                  |j                            d          pt          }||j        fS )a  GET /v1/sync/objects/:hash (or the org route when ``org_scope``).

        Kind comes from the object-type response header for tree/commit; a blob
        (application/octet-stream) is returned as ``blob``.

        Org objects are stored under the ``org:<org_id>`` scope key and are NOT
        readable through the personal route (it scopes to the token's owner),
        so walking an org commit requires ``org_scope=True`` on every hop.
        zorg/objects/zobjects/rQ  i  zobject z
 not foundrB    z not readabler_  zget_object failed: zX-HSP-Object-Type)	rX  r7   r\  rR  ra  rA  rY  r  content)r   rn  rd  r   rb  r   s         r!   
get_objectzSyncClient.get_object-  s     -6P(h(((;Ph;P;PMdiioot|DD=C:h:::3GGGG=C=h===cJJJJ=CA!-AA!-XXXXy}}011>YQYr#   commit_hashc                   |                      ||          \  }}|t          k    rt          | d| d          t          j        |                    d                    S )z3Fetch a commit object and parse its canonical JSON.rc  r   z, expected commitr0   )rs  r  rA  r1   r2   r3   )r   rt  rd  r   r-   s        r!   get_commit_jsonzSyncClient.get_commit_jsonB  sc     __[I_FF
d;{GGGGGHHHz$++g..///r#   r  c                   |                      ||          \  }}|t          k    rt          | d| d          t          j        |                    d                    S )z1Fetch a tree object and parse its canonical JSON.rc  r   z, expected treer0   )rs  r  rA  r1   r2   r3   )r   r  rd  r   r-   s        r!   get_tree_jsonzSyncClient.get_tree_jsonK  sc     __Y)_DD
d9yCCdCCCDDDz$++g..///r#   r   Dict[str, Tuple[str, bytes]]c                  d |                                 D             }| j                            |                     d          ||rddind| j                  }|j        dk    rt          dd	          |j        d
k    rt          d|j         d
	          |j        dvrt          d|j         |j        	          |j        r|	                                ni S )u  POST /v1/sync/objects (sync contract). Batch multi-object upload.

        Contract §1 requires raw object bytes on the wire (NOT base64-in-JSON),
        and  specifies "a length-prefixed or multipart stream of
        {hash, type, bytes}". We use multipart/form-data: one part per object,
        the part's field name = the claimed ``sha256:<hex>`` hash, its
        ``filename`` carries the object ``type`` (blob|tree|commit), and the
        part body is the raw object bytes. The server recomputes each hash from
        the received bytes and rejects the whole batch with 422 on mismatch.
        Idempotent: a known hash is a no-op ``already_present``.

        M2 (contract §11.5): ``org_scope=True`` adds ``?scope=org`` so the
        objects land in the ORG scope (org-readable; required before an org
        CAS/propose). Gated server-side on the token's org_role claim.

        NOTE (framing choice within contract latitude): §4.2 says "length-
        prefixed OR multipart"; this picks multipart/form-data with
        (field=hash, filename=type, body=raw-bytes). The server strand must
        parse the same framing -- flagged for cross-strand alignment.
        c                (    g | ]\  }\  }}|||d ffS )zapplication/octet-streamrU   )r    hr   r-   s       r!   r"   z*SyncClient.put_objects.<locals>.<listcomp>q  s=     
 
 
<D$ t789
 
 
r#   r   scopeorgN)filesri  rR  i  zobject too large (413)rB  i  zhash_mismatch (422): )r_     zput_objects failed: )
r)   rX  postr\  rR  ra  rA  textrr  r1   )r   r   rd  r  rb  s        r!   put_objectszSyncClient.put_objectsV  s   6
 
#*==??
 
 
 MIIi  '0:GU##dL	  
 
 =C4SAAAA=C<AF<<SIIII=
**B1=BB1=YYYY9,qvvxxx",r#   r   	from_hashrr   to_hashc                   | j                             |                     d|           ||d| j                  }|j        dk    r#|j        r|                                ni }ddi|S |j        dk    r7t          |                                pi                     dd	                    |j        d
k    rt          dd
          |j        dk    rt          d|j         |j                  |j        r|                                ni S )uM  POST /v1/sync/refs/:name -- atomic compare-and-swap (sync contract).

        Raises :class:`SyncConflict` (carrying the actual head) on 409.

        M2 (contract §11.5): a non-admin member's CAS on an org HEAD is never
        rejected — the server converts it to a proposal and returns
        ``202 {proposal_id, ref}``. Surfaced as
        ``{"proposal_pending": True, ...}`` so callers can tell "merged" (200)
        from "proposed, awaiting review" (202) without exceptions — a 202 is a
        SUCCESS-shaped outcome, never to be presented as live (error table §5).
        zrefs/)fromto)r1   rR     proposal_pendingT  rM  r   rq  z#forbidden (403) -- owner/permissionrB  r_  zcas_ref failed: )
rX  r  r\  rR  ra  rr  r1   rL  r7   rA  )r   r   r  r  rb  bodys         r!   cas_refzSyncClient.cas_ref  s    MIIndnn%%#733L  
 

 =C y0166888bD&555=C B33HbAABBB=CA#NNNN=C>q}>>q}UUUU9,qvvxxx",r#   N)rg   r9   rb   r9   rR  rS  )r   r9   r   r9   r   rE   )re  r9   rd  r   r   rf  )rn  r9   rd  r   r   ro  )rt  r9   rd  r   r   rE   )r  r9   rd  r   r   rE   )r   ry  rd  r   r   rE   )r   r9   r  rr   r  r9   r   rE   )rQ   rR   rS   rT   r   r\  r^  rm  rs  rv  rx  r  r  rU   r#   r!   rP  rP    s6         IM E E E E E E9 9 9 9
    :?      * >C      , 6;0 0 0 0 0 0 490 0 0 0 0 0  	+- +- +- +- +- +-Z- - - - - -r#   rP  c                 $    t                      dz  S )Nz.sync_stater   rU   r#   r!   _sync_state_pathr    s    ===((r#   c                 $    t                      dz  S )Nz.sync_manifestr  rU   r#   r!   _legacy_sync_state_pathr    s    ==+++r#   c                    t                      } |                                 st                      }|                                r	 t          j        |                    d                    }t          |t                    rc|                    dd           |                    di            t          |           	 |
                                 n# t          $ r Y nw xY w|S n># t          t          j        f$ r%}t                              d|           Y d}~nd}~ww xY wdi dS 	 t          j        |                     d                    }t          |t                    r.|                    dd           |                    di            |S n># t          t          j        f$ r%}t                              d|           Y d}~nd}~ww xY wdi dS )	a  Read the local sync state. Returns a default on missing/corrupt.

    Shape: ``{"head": "sha256:...|null", "skills": {name: {tree, commit}}}``.
    ``head`` is the last profile-root HEAD commit we reconciled with.

    Migrates a legacy ``.sync_manifest`` file (pre-rename) transparently: if the
    new ``.sync_state`` is absent but the legacy file exists, it is read and
    rewritten to the new path so an existing device keeps its head record.
    r0   r1  headNr   z8skills_sync_client: legacy sync state migrate failed: %s)r  r   z.skills_sync_client: sync state read failed: %s)r  r   r  r1   r2   r6  r5   r6   r   write_sync_stateunlinkr   JSONDecodeErrorr\   r]   )r   legacyr-   r^   s       r!   read_sync_stater    s    D;;== ,(**==?? 	\\z&"2"2G"2"D"DEEdD))  OOFD111OOHb111$T***"   K  T12 \ \ \WYZ[[[[[[[[\+++Jz$..'.::;;dD!! 	OOFD)))OOHb)))K	 T)* J J JEqIIIIIIIIJB'''sV   A8C(  C C( 
C"C( !C""C( (D#>DD#,A*F G.GGr   c                   ddl }t                      }	 |j                            dd           |                    t          |j                  dd          \  }}	 t          j        |dd	
          5 }t          j	        | |ddd           |
                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        ||           dS # t          $ r( 	 t          j        |           n# t           $ r Y nw xY w w xY w# t"          $ r&}t$                              d|           Y d}~dS d}~ww xY w)z3Write the local sync state atomically. Best-effort.r   NTr4  z.sync_state_z.tmp)r   re  r+  wr0   r1     F)indentrI   rK   z/skills_sync_client: sync state write failed: %s)tempfiler  r   r8  mkstempr9   ry   fdopenr1   dumpflushfsyncfilenoreplaceBaseExceptionr  r   r4   r\   r]   )r-   r  r   fdtmpfr^   s          r!   r  r    s   OOODK$666""s4;'7'7W]"^^C	2sW555 %	$!t%PPPP			$$$% % % % % % % % % % % % % % % JsD!!!!! 	 	 		#   	  K K KFJJJJJJJJJKss   A	D, C7 5AC	C7 CC7 CC7 7
D)DD)
D$!D)#D$$D))D, ,
E6EEFrc  clientdestrd  c                  |                     dd           |                     ||          }|                    dg           D ]E}|                    dd          }|rd|v s|dv rt                              d	|           ?||z  }|                    d
          }|t
          k    rt          | |d         ||           ~|t          k    r|                     |d         |          \  }	}
|	                    |
           |                    d          t          k    ri	 |                                j        }|                    |t          j        z  t          j        z  t          j        z             4# t$          $ r Y Aw xY wGdS )a)  Write the tree at *tree_hash* into *dest* (created if needed).

    Blobs become files (with +x restored for ``exec`` mode), nested trees
    become subdirectories. Does NOT delete files absent from the tree -- the
    caller decides removal semantics. Refuses path traversal via entry names.
    Tr4  rc  r   r   r   ru   )r  z..z1skills_sync_client: skipping unsafe tree entry %rr   r   r   N)r8  rx  r7   r\   warningr  materialize_treer  rs  write_bytesr   r   r   chmodr   r   r   r   r   )r  r  r  rd  r   entryr   targetr   _r-   sts               r!   r  r    s    	JJtdJ+++	Y??D)R((  yy$$ 	sd{{dk&9&9NNNPTUUUyy  9VU6]FiPPPPPY''f'KKGAtt$$$yy  I--.BLLem!3em!Cem!STTTT   D! s   "AE99
FFOptional[PurePosixPath]c                R   	 ddl m} n# t          $ r Y dS w xY w ||           }|dS 	 |                                                    t                                                                }n# t          t          f$ r Y dS w xY wt          |	                                          S )zGReturn the skill's path relative to ~/.hermes/skills/ (posix), or None.r   r   N)
r   r   r4   r   r   r   r   r   r   as_posix)r   r   r   r   s       r!   _skill_rel_pathr  %  s    5555555   tt
++It!!--kmm.C.C.E.EFFZ    tt(((s   	 
AA0 0BBr   skill_names%Tuple[ObjectSet, str, Dict[str, str]]c               h   ddl m} t                      }i }i }t          t	          |                     D ]}t          |          } ||          }||!	 t          |||          }	n3# t          $ r&}
t          	                    d||
           Y d}
~
`d}
~
ww xY w|	||<   t          |j                  }|}|dd         D ]}|                    |i           }d|	i||d         <   d |D             }|                    t          t          |                    }t!          |||	          }|||fS )
u?  Build all objects for *skill_names* + the profile-root tree.

    Returns ``(objects, root_tree_hash, skill_tree_map)`` where
    ``skill_tree_map`` is ``{skill_name: tree_hash}``. Skills whose blobs
    exceed *max_object_bytes* are skipped (surfaced via logger).

    The root tree nests category directories: a skill at ``devops/foo`` yields
    a root entry ``devops`` (tree) containing ``foo`` (tree). Flat skills yield
    a direct root entry.

    The root tree also carries a ``sync-manifest`` BLOB (design.md §2.8)
    recording the per-skill opt-in state, so opt-in is durable + cross-device
    rather than a device-local ``.usage.json`` flag. Every skill in
    ``skill_names`` is recorded ``enabled: true`` (they ARE the opted-in set);
    the manifest is the authoritative record the plane + other devices read.
    r   r  Nr   z#skills_sync_client: skipping %s: %s__tree__c                    i | ]}|d S )TrU   )r    r   s     r!   
<dictcomp>z$snapshot_profile.<locals>.<dictcomp>e  s    :::4D$:::r#   manifest_hash)r   r   r   r(   r   r  r  r   r\   r  r8   r   r   r   r  r,   _build_root_tree)r  r   r   r   skill_tree_mapr   r   r   r   r  r^   r   nodepartmanifest_mapr  	root_hashs                    r!   snapshot_profiler  5  s   & 211111kkG%'NDs;''(( 2 2d###OD))	;)+	"9gHXYYYII 	 	 	NN@$JJJHHHH	  )tSY#2#J 	- 	-D??4,,DD%y1U2Y ;:>:::LKK,\:: M !wmLLLII~--s   A**
B4BBr  r  r  c               8   g }|                                  D ]\  }}t          |t                    rBd|v r>t          |          dk    r+|                    |t
          |d         t          d           \t          ||          }|                    |t
          |t          d           |)|                    t          t          |t          d           |                    d            t
          |d}|                    t
          t          |                    S )u.  Recursively canonicalize the nested root structure into trees.

    ``manifest_hash`` (only passed at the top level) adds a root-level
    ``sync-manifest`` BLOB entry (design.md §2.8) alongside the skill subtrees.
    It cannot collide with a skill dir (skill entries are trees; this is a blob).
    r  r   r   Nc                    | d         S r   rU   rp   s    r!   r   z"_build_root_tree.<locals>.<lambda>  r   r#   r   r   )r)   r5   r6   r   r   r  r  r  SYNC_MANIFEST_ENTRY_NAMEr  r   r  r   r*   )r  r   r  r   r   r	  r
  r  s           r!   r  r  n  s1    %'Gzz|| 	 	eeT"" 	zU':':s5zzQNNy%
:KU]^^    (w77HNNy(HUU     0!%!	 	
 	
 	
 LL((L)))!g66H;;y"6x"@"@AAAr#   c                    d|  dS )N
refs/user//HEADrU   )rh   s    r!   user_head_refr    s    $$$$$r#   nc                    d|  d| S )Nr  
/conflict/rU   )rh   r  s     r!   user_conflict_refr    s    ,,,,,,r#   'SyncClient'rt  c               <    |                      ||          d         S )z,Return the tree hash referenced by a commit.rc  r   )rv  )r  rt  rd  s      r!   _root_tree_of_commitr    s"     !!+!CCFKKr#   root_tree_hashDict[str, str]c               <     i d fd |d           S )	a  Flatten a profile-root tree into ``{posix_rel_path: skill_tree_hash}``.

    A skill tree is any tree containing a ``SKILL.md`` blob entry. We walk the
    root tree; a subtree with a SKILL.md is treated as a skill leaf keyed by
    its path, so category nesting is preserved.
    r  r9   re  r   r   c                B                        | 	          }|                    dg           }t          d |D                       }|r	|r| 
|<   d S |D ]I}|                    d          t          k    r)|r| d|d          n|d         } |d         |           Jd S )Nrc  r   c              3     K   | ];}|                     d           dk    o|                     d          t          k    V  <dS )r   r   r   N)r7   r  )r    r^   s     r!   r"  z6_skill_trees_of_root.<locals>._walk.<locals>.<genexpr>  sV       
 
KLAEE&MMZ'FAEE&MMY,F
 
 
 
 
 
r#   r   ru   r   r   )rx  r7   anyr  )r  re  r   r   has_skill_mdr^   child_prefix_walkr  rd  results          r!   r  z#_skill_trees_of_root.<locals>._walk  s    ##I#CC((9b)) 
 
PW
 
 
 
 
  	F 	&F6NF 	/ 	/AuuV}}	)):@O&661V9666aiai...	/ 	/r#   r   )r  r9   re  r9   r   r   rU   )r  r  rd  r  r  s   ` `@@r!   _skill_trees_of_rootr    sS      F/ / / / / / / / / 
E."Mr#   c                   	 |                      |          }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w|                    dg           D ]}|                    d          t
          k    r|                    d          t          k    re	 |                     |d                   \  }}n4# t          $ r'}t                              d|           Y d}~ dS d}~ww xY wt          |          c S dS )uw  Read the ``sync-manifest`` blob at the root of *root_tree_hash* into
    ``{name: enabled}`` (design.md §2.8), or ``None`` if there is no manifest
    entry / it is malformed.

    The manifest is a root-level BLOB entry named ``sync-manifest`` (never a
    skill subtree). This is how a device learns the cross-device opt-in state
    written by another device's push.
    z1skills_sync_client: manifest root read failed: %sNr   r   r   r   z2skills_sync_client: manifest blob fetch failed: %s)	rx  r4   r\   r]   r7   r  r  rs  r>   )r  r  r   r^   _kindr-   exs          r!   read_manifest_of_rootr    s(   ##N33   H!LLLttttt XXi$$ - -55==444v)9S9S$//&	::tt   QSUVVVtttttt 't,,,,,4s,    
AAA B??
C0	C++C0capsc                    t          |                     d          pd          }|                    dd          d         }|t          k    rt	          d|dt           d          d	S )
z<Reject an incompatible server major version (sync contract).hsp_versionr   r  r   r   z this server speaks sync version z, but this Hermes speaks u"    — update Hermes to sync with itN)r9   r7   r)  WIRE_VERSIONrA  )r  vermajors      r!   _check_versionr    s    
dhh}%%+
,
,CIIc1a E@s @ @@ @ @
 
 	
 r#   zhermes skill sync)r  identityr  Optional['SyncClient']Optional[List[str]]r  Optional[Dict[str, Any]]c               >   |t                      }|d         }| ,t                      }|sddddS t          ||d                   } |t                      }|sddddS |                                 }t          |           t          |                    d	          pt                    }t          ||
          \  }}	}
t                      }|                    d          }|r |                    d          |	k    rd|dddS t                      }|r|gng }t          |	|||||          }|                     |j                   t          |          }	 |                     |||           ||d<   |	|d<   t#          |           d|t%          |          dS # t&          $ rv}|j        sI|                     |d|           ||d<   |	|d<   t#          |           d|t%          |          ddcY d}~S t+          | ||j        |	|||||	  	        cY d}~S d}~ww xY w)u#  Push opted-in skills to the owner's HEAD (sync contract).

    Uploads all new objects, then CAS-es ``refs/user/<owner>/HEAD``. On a 409,
    fetches the actual head, three-way merges, and retries once (§4.4 / M1-C).
    Returns a result dict; never raises for the inert / no-op cases.
    Nrh   Fno sync base url configuredTokreasonnooprb   zno skills opted into syncr   r   r  r   	unchanged)r  r  r  r  rh   r  r  r   )r  r  pushed_objects)r  r  r  recovered_stale_head)rn   r   rP  r   r^  r  r   r7   DEFAULT_MAX_OBJECT_BYTESr  r  r<  r  r  r   r  r  r  r   rL  rM  _resolve_push_conflict)r  r  r  r  rh   r   r  	max_bytesr   r  r  r+   	base_headr  r  rt  refconflicts                     r!   push_skillsr    s    #%%WE~$&& 	X+HRVWWWD(9"566-// Q&A4PPP  D4DHH/00L4LMMI,[9UUUGY  HV$$I
  TX\\&))Y66IdSSSF&.ykkBG7%QX  K w'''


C
sI{333&$"""K3w<<PPP 
 
 
 	 NN3k222*HV(HVX&&&#"%g,,(,	        &Hhoy+['9
 
 	
 	
 	
 	
 	
 	

s,   AF 
H&A
H0H6HHHactual_headour_root
our_commit'ObjectSet'r   c	           	     r   |d         }	t                      }
t          | |          }|rt          | |          nd }t          | |          }t          | |          }|rt          | |          ni }i }g }t          |          t          |          z  t          |          z  }|D ]}|                    |          }|                    |          }|                    |          }t          |||          }|dk    r|                    |           ||||<   u|dk    r||||<   |dk    r||||<   |dk    r	||n|||<   |rrt          | |	          }t          |	|          }	 | 	                    |d |           n# t          $ r Y nw xY wdd|t          |          |t          |           d| d	d
S t                      }|j                                        D ]\  }\  }}||f|j        |<   t!          | ||          }t#          |||g|	|
d| |          }|                     |j                   	 | 	                    t'          |	          ||           n,# t          $ r} ddd| j         d| j        dcY d } ~ S d } ~ ww xY wt+                      }!||!d<   ||!d<   t-          |!           d|ddS )Nrh   overlapourstheirseitherFTz' skill(s) changed on both sides; wrote z-. Resolve out-of-band (hermes sync / NAS UI).)r  r  conflict_refoverlapping_skillsr  r  zmerge: r  zmerge CAS lost again (head now z); retry sync.)r  r  r  r  r  r   )r  r  merged)r<  r  r  r   r7   _merge_skillr   _next_conflict_indexr  r  rL  r(   r   r   r   r)   _assemble_root_from_skill_treesr  r  r  rM  r  r  )"r  r  r  r  r  r   r  r  r   rh   r  theirs_root	base_root
ours_treestheirs_trees
base_treesr  overlaps	all_pathsr   otbdecisionr  r  merge_objectsr|  r   r-   merged_rootmerge_commitc2r+   s"                                     r!   r  r  L  s    WEF&v{;;K;DN$VY777$I%fh77J'<<L<EM%fi8882JFHJ#l"3"33c*ooEI 5 5NN4  T""NN4  1a((y  OOD!!! } tAMF4LL!!amF4LL!! !111F4L  
 //(22	NN<z:::: 	 	 	D	 ("("2"2&x== O OO O O

 

 
	
 KKM"?0022 0 0<D$$($<a  1&&-PPK	j!#'##  L },---
}U++[,GGGG 
 
 
RRRR9	
 
 	
 	
 	
 	
 	
 	

   H#HV"HVX===s0   (F   
FF;$I   
J	*J>J	J	r   r
  r  c                N    ||k    r|dndS || k    }|| k    }|r|sdS |r|sdS dS )aa  Three-way decision for one skill's tree hash.

    Returns one of: ``ours``, ``theirs``, ``either``, ``overlap``, ``none``.
    Mirrors the origin/user/incoming decision block of skills_sync.py:619-643:
    a side "modified" the skill when its hash differs from the common base
    (analogous to ``_is_tracked_user_modification(origin, current)``).
    Nr  noner
  r  r	  rU   )r   r
  r  ours_changedtheirs_changeds        r!   r  r    s[     v~~+xx74<Lt^N N v l x9r#   skill_treesc                    i }|                                 D ]K\  }}t          |          j        }|}|dd         D ]}|                    |i           }d|i||d         <   Lt	          ||          S )a(  Build a profile-root tree object from ``{posix_rel_path: tree_hash}``.

    Rebuilds the intermediate category trees. The referenced skill trees are
    assumed already durable (they came from either side of the merge); only
    the new intermediate/root tree objects are added to *objects*.
    Nr  r  )r)   r   r   r   r  )	r  r&  r   r   r   r  r   r  r  s	            r!   r  r    s     D&,,.. 2 2id##)#2#J 	- 	-D??4,,DD%y1U2YD'***r#   c                d   	 |                      d| d          }n# t          $ r Y dS w xY wg }|D ]j}|                    dd          }|                    dd          d         }|                                r"|                    t          |                     k|rt          |          dz   ndS )z4Pick the next free conflict ref index for the owner.r  r  r   r   r   ru   r  )rm  rA  r7   rsplitisdigitr   r   max)r  rh   rh  usedrb  r   tails          r!   r  r    s    =E===>>   qqD # #uuVR  {{3""2&<<>> 	#KKD		"""")CIIMM)s    
**r  c                  |t                      }|d         }| ,t                      }|sddddS t          ||d                   } |                                 }t	          |           |                     t          |                    }d}|D ]?}|                    d          t          |          k    r|                    d	          } n@|sdd
ddS t                      }||                    d          k    rdd|ddS t          | |          }	t          | |	          }
g }t          | |	          }|r	 ddlm}m}m} |                                D ]@\  }}|s ||          s ||          s! ||d           |                    |           An2# t$          $ r%}t&                              d|           Y d}~nd}~ww xY wt+          t-                                }g }|
                                D ]C\  }}|r||vrt/                      |z  }t1          | ||           |                    |           D||d<   t3          |           d|t5          |          t5          |          dS )a  Pull the owner's HEAD and materialize opted-in skills to disk.

    Fetches ``refs/user/<owner>/HEAD``; if it advanced past our recorded head,
    walks the profile-root tree and writes each skill tree into
    ~/.hermes/skills/. Only paths the user has opted into (``sync: true``) are
    materialized, so a pull never resurrects a skill the user hasn't chosen.
    Best-effort; returns a result dict.
    Nrh   Fr  Tr  rb   r   r   zno remote HEAD yetr  zalready up to date)r  r  r  r  r   )set_syncis_curation_eligibleis_sync_enabledz8skills_sync_client: manifest opt-in reconcile failed: %s)r  r  updatedopt_in_adopted)rn   r   rP  r^  r  rm  r  r7   r  r  r  r  r   r0  r1  r2  r)   r   r4   r\   r]   r   _opted_in_rel_pathsr   r  r  r(   )r  r  rh   r   r  rh  r  rb  r+   	root_treeremote_treesreconciled_from_manifestremote_manifestr0  r1  r2  snamer   r^   opted_inr3  r   r  r  s                           r!   pull_skillsr<    s$    #%%WE~$&& 	X+HRVWWWD(9"566  D4??=//00DD  55==M%000055==DE 1  J&:DIII  Hx||F####&:DRVWWW$VT22I'	::L +-+FI>>O X	XYYYYYYYYYY"1"7"7"9"9 ; ;w ++E22 &u-- ;HUD))),33E:::;  	X 	X 	XLLSUVWWWWWWWW	X &(())HG'--//  i  	H,,}}t#D111tHVX'?? !9::	  s   ;AF 
G
%GG
c                     g } t                      D ]:}t          |          }|'|                     |                                           ;| S )z<Relative posix paths of skills the user has opted into sync.)r   r  r   r  )pathsr   r   s      r!   r5  r5  :  sN    E')) ) )d##?LL(((Lr#   )r  c                :   	 t                      }|                    d          sdS t                      sdS t                      sdS t	                      sdS t          ||           S # t          $ r(}t                              d|d           Y d}~dS d}~ww xY w)zBest-effort push if all gates pass. Returns a result dict or None.
    Never raises. Called from the debounced skill_manage push hook.ri   Nr  r  z0skills_sync_client: maybe_push_skills failed: %sTexc_info)	rn   r7   r   r   r   r  r4   r\   r]   )r  r  r^   s      r!   maybe_push_skillsrC  M  s    #%%||L)) 	4#%% 	4$&& 	4&(( 	4Hg>>>>   GUYZZZttttts-   #A( A( A( A( A( (
B2BBc                    	 t                      } |                     d          sdS t                      sdS t                      sdS t	          |           S # t
          $ r(}t                              d|d           Y d}~dS d}~ww xY w)zBest-effort pull if all gates pass. Returns a result dict or None.
    Never raises. Invoked at the curator tick sites (gateway housekeeping loop
    + CLI startup).ri   Nr.  z0skills_sync_client: maybe_pull_skills failed: %sTrA  )rn   r7   r   r   r<  r4   r\   r]   )r  r^   s     r!   maybe_pull_skillsrE  `  s    #%%||L)) 	4#%% 	4$&& 	4H----   GUYZZZttttts'   #A A A A 
B	!BB	c                 p   ddt                      t                      t                      g dddddg g d} 	 t                      }d| d<   |                    d          | d<   t          |                    d                    | d<   n=# t          $ r Y n1t          $ r%}t          	                    d|           Y d}~nd}~ww xY w	 t                      | d	<   t                                          d
          | d<   n# t          $ r Y nw xY w	 t                      }d| d<   |                    d          | d<   |                    d          | d<   t                      | d<   t          | d                   | d<   n=# t          $ r Y n1t          $ r%}t          	                    d|           Y d}~nd}~ww xY w| S )zBReturn a status snapshot for ``hermes sync status``. Never raises.FN)ri   	logged_infeature_enabledr   rg   opted_in_skills
local_headrh   org_availableorg_idorg_role
org_skillsorg_skills_modifiedTrG  rh   ri   z3skills_sync_client: sync_status identity failed: %srI  r  rJ  rK  rL  rM  rN  rO  z5skills_sync_client: sync_status org lookup failed: %s)r   r   r   rn   r7   r   rP   r4   r\   r]   r   r  resolve_org_identitylist_org_skill_names list_locally_modified_org_skills)rC  r  r^   org_identitys       r!   sync_statusrT  r  s1    /11-//)++ !# F&O#%%"{",,w//w#HLL$>$>??|    O O OJANNNNNNNNO$;$=$= !.0044V<<|   Q+--"&'++H55x)--j99z355|(H8)
 )
$%%     Q Q QLaPPPPPPPPQMsO   AB 
C	CB<<C5C; ;
DDA,E9 9
F3	F3F..F3c                    g } 	 ddl m}  |t                                }|s| S t                      |z  }|                                s| S |                    d          D ]Y}|j                            |          }|j        r6| 	                    t          |                              dd                     Zn2# t          $ r%}t                              d|           Y d}~nd}~ww xY wt          |           S )zESkill names present in the local org mirror (empty when none pulled).r   read_active_org_idr   \ru   z0skills_sync_client: org skill listing failed: %sN)r   rW  r   _org_dirr  r   r   r   r   r   r9   r  r4   r\   r]   r(   )r   rW  rL  r   r   r   r^   s          r!   rQ  rQ    s    EL888888##KMM22 	LzzF"{{}} 	L

:.. 	: 	:H/--d33Cy :SXX--dC88999	:  L L LGKKKKKKKKL%==s#    B< &B< A/B< <
C+C&&C+_orgc                 @   t                      } |                     d          pi }|                    d          }|                    d          }|st          d          t          |t                    r|st          d          t	          |          | d<   || d<   | S )u^  Resolve identity + org context for org-skill operations.

    Returns ``resolve_identity()``'s dict extended with ``org_id`` and
    ``org_role``. Raises :class:`SyncInertError` when the token carries no
    ``org_role`` claim (personal org / issuer predates org support) — the
    caller should treat org sync as unavailable, NOT as an error.
    rj   rL  rM  z,no organisation associated with this accountz4this account isn't a member of a shared organisation)rn   r7   rP   r5   r9   )r  rj   rL  rM  s       r!   rP  rP    s      !!H\\(##)rFZZ!!Fzz*%%H MKLLLh$$ 
H 
B
 
 	
 VHX#HZOr#   c                 F    	 t                       dS # t          $ r Y dS w xY w)zETrue iff this token can see the org-skill surface (multi-member org).TF)rP  r4   rU   r#   r!   org_sync_availabler]    s:    t   uus    
     rL  c                r    |                      d dd          }t          fd|D             d          S )ay  Current ``refs/org/<org_id>/HEAD``, or None if the org has no content.

    Reads through the ORG endpoint. The personal refs route is scoped to the
    caller's own owner and answers an ``refs/org/...`` prefix with the caller's
    PERSONAL refs, so a personal-route read here silently reports "no org head"
    and every subsequent CAS races against a head it never saw.
    	refs/org/ru   Trc  c              3  t   K   | ]2}|                     d           t                    k    (|d         V  3dS )r   r   N)r7   org_head_ref)r    rb  rL  s     r!   r"  z!_read_org_head.<locals>.<genexpr>   sC      JJqAEE&MM\&5I5I$I$I6$I$I$I$IJJr#   N)rm  next)r  rL  rh  s    ` r!   _read_org_headrd    sQ     ??0v000D?AADJJJJDJJJD  r#   c                    d|  dS )Nr`  r  rU   rL  s    r!   rb  rb    s    $v$$$$r#   c                 .    t                      t          z  S )z<Local mirror root for org skills (read-only by convention ).)r   r   rU   r#   r!   rY  rY    s    ==<''r#   c                  |pt                      }d|vrt          d          |d         }| 5t                      }|st          d          t          ||d                   } |                                 }t          |           d|                    d          pg vrt          d          t          | |          }t          |           |sd	|dg d
S | 	                    |d	          }|d         }t          | |d	          }t                      |z  }	g }
g }t          |          }t          |                                          D ]#\  }}|	t          |          z  }	 |                                rot#          ||          rF|                    |          pi }|                    d          |k    r|                    |           ddl}|                    |           |                    d	d	           t-          | ||d	           t/          |          |d||<   |
                    |           # t0          $ r'}t2                              d||           Y d}~d}~ww xY wt7          ||||                    d          pi                     dd          |                    d          pi                     dd          |                    dd          |
d           t9          ||           |r<t2                              dt;          |          d                    |                     d	|||
|dS )u  Pull the org canonical set into ``~/.hermes/skills/_org/<org_id>/``.

    Fast-forward only (design.md §2.6: no client merge on the org path): the
    mirror is replaced with the org HEAD's content. Local edits under _org/
    are NOT merged — they are overwritten on pull; a member's change of record
    is `propose_skill` (the fork lives in their personal skills, not _org/).
    Returns {ok, org_id, head, updated} (updated = skill rel-paths written).
    rL  z!no organisation context availableNno sync base URL configuredrb   r~  features.this server does not support org-shared skillsT)r  rL  r  r3  rc  r   r   r4  )fingerprintr   z;skills_sync_client: org skill materialize failed for %s: %sr  rh   r   r  r  )rL  r  author_user_idauthor_devicer  r   z]skills_sync_client: %d org skill(s) have local edits AND upstream changes; left untouched: %sz, )r  rL  r  r3  
conflicted)rP  rP   r   rP  r^  r  r7   rd  _write_active_org_markerrv  r  rY  _read_org_baseliner(   r)   r   r   org_skill_is_locally_modifiedr   shutilrmtreer8  r  _skill_dir_fingerprintr4   r\   r  _write_org_provenance_write_org_baseliner   r*  )r  r  rL  rg   r  r  head_commitr6  r&  	dest_rootr3  ro  baselinerel_pathr  r  prevrs  r^   s                      r!   pull_org_skillsr}    s    1/11Hx@AAAhF~(** 	@ !>???Hhy&9::  D4TXXj))/R00MNNN&&))D V$$$ KfdrJJJ(((>>KF#I&vyDIIIK

V#IG J!&))H%k&7&7&9&9::  )=222	{{}} $
 16BB #<<117RD xx''944"))(333d###JJtdJ333VYEEEE5d;;!" "HX NN8$$$$ 	 	 	NNM       	 *x88>BCCGRPP)ooh77=2BB8RPP//$++	
 	

 
 
 ))) 
*
OOIIj!!		
 	
 	
    s    %A)H=A-H==
I.I))I.c                t   t          j                    }	 t          d |                     d          D                       D ]}|                    t          |                    |                                         dd                              d                     |                    d           |                    |	                                           |                    d           n4# t          $ r'}t                              d| |           Y d}~d	S d}~ww xY w|                                S )
a  Stable content hash of a materialized skill directory.

    Used to tell "the user/agent edited this org skill" from "this is exactly
    what upstream shipped". Hashes every file's relative path + bytes, sorted,
    so it is independent of filesystem ordering and mtimes.
    c              3  B   K   | ]}|                                 |V  d S r   )r  )r    r   s     r!   r"  z)_skill_dir_fingerprint.<locals>.<genexpr>  s/      BBaaiikkBBBBBBBr#   *rX  ru   r0       z1skills_sync_client: fingerprint failed for %s: %sNr   )r@   rA   r(   r   updater9   r   r  rM   r  r   r\   r]   rB   )r   r|  r  r^   s       r!   ru  ru  {  s    	ABB4::c??BBBBB 	 	AHHSt,,--55dC@@GGPPQQQHHUOOOHHQ\\^^$$$HHUOOOO		
    H$PQRRRrrrrr ;;==s   CC2 2
D#<DD#c                6    ddl m} t                      | z  |z  S )zBSidecar recording the upstream fingerprint of each mirrored skill.r   )ORG_BASELINE_FILE)r   r  rY  )rL  r  s     r!   _org_baseline_pathr    s)    333333::!222r#   c                    	 t          j        t          |                               d                    S # t          $ r i cY S w xY w)Nr0   r1  )r1   r2   r  r6  r4   rf  s    r!   rq  rq    sU    z,V44>>>PPQQQ   			s   47 AArz  c                   	 t          |           }|j                            dd           |                    t	          j        |dd          d           d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)NTr4  r  )r  rI   r0   r1  z-skills_sync_client: baseline write failed: %s)	r  r   r8  r9  r1   rL   r4   r\   r]   )rL  rz  r   r^   s       r!   rw  rw    s    Iv&&	td333	TZdCCCgVVVVV I I IDaHHHHHHHHHIs   AA 
B%BBskill_rel_pathc                >   t                      |z  t          |           z  }|                                sdS t          |                              |           pi }t          |t                    r|                    d          n|}|sdS t          |          |k    S )zITrue when the local copy of an org skill differs from what upstream sent.Frl  )rY  r   r  rq  r7   r5   r6   ru  )r  rL  r  r  recordeds        r!   rr  rr    s    ::~!>!>>D;;== uv&&**>::@bE+5eT+B+BMuyy'''H  u!$''833r#   c                    	 ddl m}  p |t                                  sg S t                     }t	           fd|D                       S # t
          $ r'}t                              d|           g cY d}~S d}~ww xY w)z7Org skills with local edits that upstream has not seen.r   rV  c              3  <   K   | ]}t          |          |V  d S r   )rr  )r    r   rL  s     r!   r"  z3list_locally_modified_org_skills.<locals>.<genexpr>  sG       
 
'DS&'Q'Q

 
 
 
 
 
r#   z,skills_sync_client: modified-scan failed: %sN)r   rW  r   rq  r(   r4   r\   r]   )rL  rW  rz  r^   s   `   r!   rR  rR    s    888888<--kmm<< 	I%f-- 
 
 
 
#
 
 
 
 
 	
    CQGGG						s"   "A )A 
BA<6B<Bc                    	 ddl m} t                      }|                    dd           ||z                      | d           dS # t
          $ r&}t                              d|           Y d}~dS d}~ww xY w)	zBRecord which org's mirror may resolve (best-effort, never raises).r   ORG_ACTIVE_MARKERTr4  r0   r1  z6skills_sync_client: active-org marker write failed: %sN)r   r  rY  r8  r9  r4   r\   r]   )rL  r  r   r^   s       r!   rp  rp    s    R777777zz

4$
///	!	!--fw-GGGGG R R RMqQQQQQQQQQRs   AA	 	
A9A44A9c                &   	 ddl m} t                      | z  }|                    dd           ||z                      t          j        |d          d           d
S # t          $ r&}t          	                    d	|           Y d
}~d
S d
}~ww xY w)zDPersist the org HEAD provenance sidecar (best-effort, never raises).r   )ORG_PROVENANCE_FILETr4  r  )r  r0   r1  z3skills_sync_client: org provenance write failed: %sN)
r   r  rY  r8  r9  r1   rL   r4   r\   r]   )rL  r-   r  r  r^   s        r!   rv  rv    s    	O999999zzF"

4$
///	#	#//JtA&&& 	0 	
 	
 	
 	
 	
  O O OJANNNNNNNNNOs   AA   
B*BBr@  c                  |pt                      }|d         }|5t                      }|st          d          t          ||d                   }|                                }t          |           d|                    d          pg vrt          d          t          |                    d          pt                    }t          |           }|t          d	|  d
          t                      |z  }	|	dz                                  st          d	|  d          t                      }
t          |	|
|          }d}	 |dz  }t          ||          }|r%t!          ||d          }t#          ||d          }ni }||t%          |          <   t'          |||
          }t)          ||r|gng |d         t+                      |pd|  |
          }|                    |
j        d           	 |                    t3          |          ||          }nZ# t4          $ rM}|t6          k    rt          d| dd          |t8                              d|j        |           Y d}~'d}~ww xY w|                    d          r/dd|                    d          |                    d          ||dS dd|                    d|          ||dS ) u  Propose a local skill's current content to the org canonical set.

    Snapshots the LOCAL (personal) skill directory as an org-scoped commit
    layered on the current org HEAD tree (splice/replace that one skill
    subtree), uploads the objects with ``?scope=org``, then CAS-es the org
    HEAD (contract §11.5):

    - ADMIN/OWNER token → the server merges directly → ``{ok, merged: True}``.
    - MEMBER token → the server converts to a proposal (202) →
      ``{ok, proposal_pending: True, proposal_id, ref}``. NEVER presented as
      live/merged.

    Non-interactive by design — an automated submitter (curator hook) drives
    this exact function later (Ben's automation trajectory).
    rL  Nri  rb   r~  rj  rk  r   zskill 'z ' not found under the skills dirr   z' has no SKILL.mdr   r   Tr   rc  rh   zpropose r  zEthe organisation's skills changed while this was being proposed, and uA    attempts to catch up all lost the race — run the command againr  rB  zCpropose_skill: org HEAD moved (actual=%r), re-splicing (attempt %d)r  proposal_idr  )r  r  r  r  r   rL  r   )r  r  r  r   rL  )rP  r   rP   rP  r^  r  r7   r   r  r  rA  r   r   r   r  rd  r  r  r9   r  r  r<  r  r   r  rb  rL  _ORG_CAS_MAX_ATTEMPTSr\   r]   rM  )r   r  r  r  rL  rg   r  r  r   r   r   
skill_treeattemptsr   r  	skill_mapr  rt  r  r  s                       r!   propose_skillr    sm   , 1/11HhF~(** 	@ !>???Hhy&9::  D4TXXj))/R00MNNNDHH/00L4LMMI *
%
%C
{N*NNNOOO#I
"**,, A?*???@@@ kkGIwKKKJ H%A"6622	 	,VY$OOOI,VY$OOOIII(	#c((3FIwOO	"$,YKK"7##%%66*66
 
 
 	7?d;;;	^^L$8$8)[QQF 	 	 	0009%-9 9 9 	  
    LLU  
 HHHH	 zz$%% 
 $!::m44::e$$!
 
 	
 

6;//  s   5$H 
I1$AI,,I1c                    	 t                      } n# t          $ r^ 	 t                      }|                    d          pi }|                    d          st	                       n# t
          $ r Y nw xY wY dS t
          $ r&}t                              d|           Y d}~dS d}~ww xY w	 t                      sdS t                      sdS t          |           S # t
          $ r&}t                              d|           Y d}~dS d}~ww xY w)u  Best-effort org pull if all gates pass. Never raises; None when inert.

    Gates (all must hold): logged in, org_role claim present (multi-member
    org), feature enabled, base URL configured. Personal orgs are inert here
    by construction — resolve_org_identity raises SyncInertError without the
    claim.

    Marker hygiene: when the token VERIFIABLY lacks the org claim (logged in,
    personal org / left the org), the active-org marker is cleared so
    previously-mirrored org skills stop resolving. When we simply cannot
    resolve identity (offline, logged out), the marker is left alone —
    offline grace keeps already-pulled org skills working.
    rj   rM  Nz:skills_sync_client: maybe_pull_org_skills inert/failed: %sr.  )rP  rP   rn   r7   _clear_active_org_markerr4   r\   r]   r   r   r}  )r  base_identityrj   r^   s       r!   maybe_pull_org_skillsr  R  sd   ')) 	 	 		,..M"&&x006BF::j)) +(*** 	 	 	D	tt   H!	
 	
 	
 ttttt	

#%% 	4$&& 	41111   H!	
 	
 	
 ttttt	sc    
B'AA%$B'%
A2/B'1A22B'8	B'B""B'+C ;C C 
D%DDc                 $   	 ddl m}  t                      | z  }|                                r0|                                 t
                              d           dS dS # t          $ r&}t
                              d|           Y d}~dS d}~ww xY w)z9Remove the active-org marker (org skills stop resolving).r   r  zgskills_sync_client: cleared active-org marker (token has no org workflow); org skills no longer resolvez+skills_sync_client: marker clear failed: %sN)	r   r  rY  r   r  r\   infor4   r]   )r  markerr^   s      r!   r  r  ~  s    G777777//==?? 	MMOOOKKL    	 	  G G GBAFFFFFFFFFGs   AA 
B)B

B)r   r   r   r   )r-   r   r   r.   )r-   r   r   r9   )rD   rE   r   r   )rV   r9   r   rE   r  )r   r   )r   rr   )r:   r   r   r   )r   r9   r   r9   r   r   r   r   )r   r   )r   r9   r   r   )r   r   )r   r   r   r9   )r   r   r   r   r   r   r   r9   )r  r9   r  r   rh   r9   r  r9   r  r9   r   r   r  rr   r   r9   )r   r9   )r   r9   r   r9   )r-   rE   r   r   )
r  rP  r  r9   r  r   rd  r   r   r   )r   r9   r   r  )r  r   r   r   r   r  )r  rE   r   r   r  rr   r   r9   )rh   r9   r   r9   )rh   r9   r  r   r   r9   )r  r  rt  r9   rd  r   r   r9   )r  r  r  r9   rd  r   r   r  )r  r  r  r9   r   r.   )r  rE   r   r   r   )
r  r  r  r  r  r  r  r9   r   rE   )r  r  r  rE   r  r9   r  r9   r  r9   r   r  r  r   r  r9   r   rr   r   rE   )r   rr   r
  rr   r  rr   r   r9   )r  r  r&  r  r   r  r   r9   )r  r  rh   r9   r   r   )r  r  r  r  r   rE   )r  r9   r   r  )r   r  )r  r  rL  r9   r   rr   )rL  r9   r   r9   )rL  r9   r   r   )rL  r9   r   rE   )rL  r9   rz  rE   r   r   )r  r9   rL  r9   r   r   )rL  rr   r   r   )rL  r9   r   r   )rL  r9   r-   rE   r   r   )
r   r9   r  r  r  r  r  rr   r   rE   r   )rrT   
__future__r   r@   r1   loggingry   timer   r   r   r   pathlibr   r   typingr   r	   r
   r   r   r   	getLoggerrQ   r\   r  r  r  r  r  r   r   r  r  r  r&   r'   r,   r>   rC   r*   rl   RuntimeErrorrP   r_   rn   rq   r~   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r.  r<  r?  rA  rL  rP  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r<  r5  rC  rE  rT  rQ  r   rP  r]  r  rd  rb  rY  r}  ru  r  rq  rw  rr  rR  rp  rv  r  r  r  rU   r#   r!   <module>r     s  4 4l # " " " " "    				      ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' = = = = = = = = = = = = = = = =		8	$	$ #  		 		 2 + $  * * * *$! ! ! !Z8 8 8 8
   < (     \      (# # # #L   * C ) ) ) )^ 	#""	(	(	(      $	N 	N 	N 	N   &\ \ \ \0( ( ( (   <$ $ $ $N   J! ! ! ! ! ! ! !$   &B &B &B &Bb F F F F F F8> > > >&# # # #L   2       
 
 
 
 
< 
 
 
*a- a- a- a- a- a- a- a-f) ) ) ), , , ,$( $( $( $(NK K K K: JO     P) ) ) )" 8P6. 6. 6. 6. 6. 6.t QUB B B B B BL% % % %- - - -
 BGL L L L L L EJ     :   6
 
 
 
  &*K
 (,)-&K
 K
 K
 K
 K
 K
D\> \> \> \>~   (+ + + +&* * * *( &*R *.R R R R R Rj   & )<      &   $0 0 0 0f   P    .        % % % %( ( ( ( &*k *.k k k k k k\   (3 3 3 3   I I I I4 4 4 4    "	R 	R 	R 	RO O O O  &*p *.!p p p p p pf) ) ) )XG G G G G Gr#   