§
    øžyjDg  ã                  óÊ  — U 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 ddlmZmZmZ ddlmZmZ ddlmZ did„ZdZ ej        d¦  «        Zdjd„Zdkd„Zdld„Zdmd„Zdnd„Z  e!¦   «         Z"de#d<   dodpd„Z$ej%        dodqd„¦   «         Z&d Z'drd#„Z(e
 G d$„ d%¦  «        ¦   «         Z)e
 G d&„ d'¦  «        ¦   «         Z*dsd*„Z+dtd-„Z,dud/„Z-dvd1„Z.dddddddd2œdwd:„Z/d;d<œdxd@„Z0dydC„Z1ddddddDœdzdE„Z2dd;dFœd{dI„Z3d|dJ„Z4d}dK„Z5d|dL„Z6d~dM„Z7d~dN„Z8d~dO„Z9dPZ:dQZ;ddR„Z<d€dS„Z=d€dT„Z>d;dUœd�dX„Z?ddYœd‚dZ„Z@d;dd[œdƒd_„ZAd„da„ZBd;d<œd…db„ZC ej        dc¦  «        ZDdddeœd†dh„ZEdS )‡u­  Per-profile first-class Project store.

A **Project** is a human-named, multi-folder workspace. Unlike the desktop's
old inferred "workspaces" (derived from each session's ``cwd`` + a git probe)
and unlike kanban's self-generated worktrees, a Project is an explicit,
persisted entity the user creates and names. It anchors:

- **Desktop session grouping** â€” a session belongs to a project when its
  ``cwd`` lives under one of the project's folders (longest-prefix match).
- **Kanban task worktrees** â€” a task linked to a project creates its worktree
  under the project's primary repo with a deterministic branch name, instead
  of the random ``wt/<task-id>`` fallback.

Scope: **per-profile**, stored at ``$HERMES_HOME/projects.db`` (resolved via
``get_hermes_home()``), mirroring sessions / config / cron. This deliberately
differs from kanban, whose board DB is root-anchored and shared across
profiles. A Project may *bind* a kanban board (``board_slug``) so the two
systems agree on the repo + branch convention without merging their stores.

The schema is intentionally small and additive: column additions go through
:func:`_add_column_if_missing` so opening an old DB is always safe.
é    )ÚannotationsN)Ú	dataclassÚfield)ÚPath)ÚIterableÚListÚOptional)Úadd_column_if_missingÚ	write_txn©Úget_hermes_homeÚreturnr   c                 ó$   — t          ¦   «         dz  S )z×The per-profile projects DB path (``$HERMES_HOME/projects.db``).

    Profile-aware: ``get_hermes_home()`` already points at the active profile's
    home. Tests pass an explicit ``db_path`` to :func:`connect`.
    úprojects.dbr   © ó    ú</home/ragecks/.hermes/hermes-agent/hermes_cli/projects_db.pyÚprojects_db_pathr   ,   s   € õ ÑÔ˜}Ñ,Ð,r   a”  
CREATE TABLE IF NOT EXISTS projects (
    id            TEXT PRIMARY KEY,
    slug          TEXT NOT NULL UNIQUE,
    name          TEXT NOT NULL,
    description   TEXT,
    icon          TEXT,
    color         TEXT,
    board_slug    TEXT,
    primary_path  TEXT,
    created_at    INTEGER NOT NULL,
    archived      INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS project_folders (
    project_id  TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    path        TEXT NOT NULL,
    label       TEXT,
    is_primary  INTEGER NOT NULL DEFAULT 0,
    added_at    INTEGER NOT NULL,
    PRIMARY KEY (project_id, path)
);

CREATE INDEX IF NOT EXISTS idx_project_folders_path
    ON project_folders(path);

CREATE TABLE IF NOT EXISTS project_meta (
    key    TEXT PRIMARY KEY,
    value  TEXT
);

-- Git repos found by scanning the filesystem (desktop "repo-first" discovery).
-- Cached here so the overview is instant after the first scan instead of
-- re-walking the disk every time the Projects view opens.
CREATE TABLE IF NOT EXISTS discovered_repos (
    root          TEXT PRIMARY KEY,
    label         TEXT,
    last_seen     INTEGER NOT NULL
);
z^[a-z0-9][a-z0-9\-_]{0,63}$ÚnameÚstrc                ó   — t          | pd¦  «                             ¦   «                              ¦   «         }t          j        dd|¦  «                             d¦  «        }|dd…                              d¦  «        }|pdS )z8Derive a slug candidate from a human name (best-effort).Ú z
[^a-z0-9]+ú-ú-_Né@   Úproject)r   ÚstripÚlowerÚreÚsub)r   Úss     r   Ú_slugifyr"   n   sm   € åˆDˆJ�B‰Œ×ÒÑÔ×%Ò%Ñ'Ô'€AÝ
Œˆ}˜c 1Ñ%Ô%×+Ò+¨DÑ1Ô1€AØ	ˆ#ˆ2ˆ#Œ�Š�TÑÔ€AØˆ>�	Ðr   ÚslugúOptional[str]c                óÖ   — | €dS t          | ¦  «                             ¦   «                              ¦   «         }|sdS t                               |¦  «        st          d| ›d�¦  «        ‚|S )z>Lowercase + strip a slug; validate; return ``None`` for empty.Nzinvalid project slug zc: must be 1-64 chars, lowercase alphanumerics / hyphens / underscores, not starting with '-' or '_')r   r   r   Ú_SLUG_REÚmatchÚ
ValueError)r#   r!   s     r   Únormalize_slugr)   v   s}   € à€|ØˆtÝˆD‰	Œ	�ŠÑÔ×ÒÑ!Ô!€AØð ØˆtÝ�>Š>˜!ÑÔð 
Ýð Dð ð ð ñ
ô 
ð 	
ð
 €Hr   c                 ó0   — dt          j        d¦  «        z   S )NÚp_é   )ÚsecretsÚ	token_hexr   r   r   Ú_new_project_idr/   †   s   € Ø•'Ô# AÑ&Ô&Ñ&Ð&r   Úintc                 óB   — t          t          j        ¦   «         ¦  «        S ©N)r0   Útimer   r   r   Ú_nowr4   Š   s   € Ý�tŒy‰{Œ{ÑÔÐr   Úpathc                óæ   — t           j                             t           j                             t	          | ¦  «                             ¦   «         ¦  «        ¦  «        }|                     d¦  «        p|S )zEAbsolute, user-expanded, separator-normalized path (no trailing sep).ú/\)Úosr5   ÚabspathÚ
expanduserr   r   Úrstrip)r5   Úps     r   Ú_normalize_pathr=   Ž   sJ   € å
Œ�Š�œ×*Ò*­3¨t©9¬9¯?ª?Ñ+<Ô+<Ñ=Ô=Ñ>Ô>€AØ�8Š8�E‰?Œ?Ð˜aÐr   zset[str]Ú_INITIALIZED_PATHSÚdb_pathúOptional[Path]úsqlite3.Connectionc                ó:  — | �| nt          ¦   «         }|j                             dd¬¦  «         t          |                     ¦   «         ¦  «        }t          j        t          |¦  «        ¦  «        }	 t
          j        |_        ddl	m
}  ||d¬¦  «         |                     d¦  «         |t          vrC|                     t          ¦  «         t          |¦  «         t                               |¦  «         n## t"          $ r |                     ¦   «          ‚ w xY w|S )	a  Open (and initialize if needed) the per-profile projects DB.

    WAL with DELETE fallback for network filesystems (shared helper from
    ``hermes_state``). Schema init is idempotent (``CREATE TABLE IF NOT
    EXISTS`` + additive migrations) and cached per-path per-process.
    NT)ÚparentsÚexist_okr   )Úapply_wal_with_fallbackr   )Údb_labelzPRAGMA foreign_keys=ON)r   ÚparentÚmkdirr   ÚresolveÚsqlite3ÚconnectÚRowÚrow_factoryÚhermes_staterE   Úexecuter>   ÚexecutescriptÚ
SCHEMA_SQLÚ_migrate_add_optional_columnsÚaddÚ	ExceptionÚclose)r?   r5   ÚresolvedÚconnrE   s        r   rK   rK   ›   s  € ð Ð)ˆ7ˆ7Õ/?Ñ/AÔ/A€DØ„K×Ò˜d¨TÐÑ2Ô2Ð2Ý�4—<’<‘>”>Ñ"Ô"€HÝŒ?�3˜t™9œ9Ñ%Ô%€DðÝ"œ;ˆÔØ8Ð8Ð8Ð8Ð8Ð8àÐ ¨}Ð=Ñ=Ô=Ð=Ø�ŠÐ-Ñ.Ô.Ð.ØÕ-Ð-Ð-Ø×Ò�zÑ*Ô*Ð*Ý)¨$Ñ/Ô/Ð/Ý×"Ò" 8Ñ,Ô,Ð,øøÝð ð ð Ø�
Š
‰ŒˆØðøøøð €Ks   Á2BC8 Ã8 Dc              #  óÔ   K  — t          | ¬¦  «        }	 |V — 	 |                     ¦   «          dS # t          $ r Y dS w xY w# 	 |                     ¦   «          w # t          $ r Y w w xY wxY w)au  Open a projects DB connection and guarantee it is closed on exit.

    sqlite3's connection context manager only commits/rollbacks; it does NOT
    close the file descriptor. Long-lived processes (gateway, dashboard) route
    many project operations through ``connect()``; without closing, FDs to
    ``projects.db`` accumulate. Mirrors ``kanban_db.connect_closing``.
    )r?   N)rK   rU   rT   )r?   rW   s     r   Úconnect_closingrY   ¶   s–   è è € õ ˜7Ð#Ñ#Ô#€DðØˆ
ˆ
ˆ
ð	Ø�JŠJ‰LŒLˆLˆLˆLøÝð 	ð 	ð 	ØˆDˆDð	øøøøð	Ø�JŠJ‰LŒLˆLˆLøÝð 	ð 	ð 	ØˆDð	øøøøøøs;   ”A  ™/ ¯
=¼=Á A'ÁAÁA'Á
A$Á!A'Á#A$Á$A')Ú
board_slugÚprimary_pathÚiconÚcolorrW   ÚNonec                óŠ   — d„ |                       d¦  «        D ¦   «         }t          D ]}||vrt          | d||› d�¦  «         ŒdS )zCAdd columns introduced after v1 to legacy DBs (safe on every open).c                ó   — h | ]
}|d          ’ŒS )r   r   )Ú.0Úrows     r   ú	<setcomp>z0_migrate_add_optional_columns.<locals>.<setcomp>Ð   s   € ÐOÐOÐO˜CˆC�ŒKÐOÐOÐOr   zPRAGMA table_info(projects)Úprojectsz TEXTN)rO   Ú_OPTIONAL_PROJECT_COLUMNSÚ_add_column_if_missing)rW   ÚcolsÚcols      r   rR   rR   Î   sc   € àOÐO 4§<¢<Ð0MÑ#NÔ#NÐOÑOÔO€DÝ(ð Ið IˆØ�dˆ?ˆ?Ý" 4¨°S¸S¸-¸-¸-ÑHÔHÐHøðIð Ir   c                  óL   — e Zd ZU ded<   dZded<   dZded<   d	Zd
ed<   dd„ZdS )ÚProjectFolderr   r5   Nr$   ÚlabelFÚboolÚ
is_primaryr   r0   Úadded_atr   Údictc                óR   — | j         | j        t          | j        ¦  «        | j        dœS )N©r5   rk   rm   rn   )r5   rk   rl   rm   rn   ©Úselfs    r   Úto_dictzProjectFolder.to_dictâ   s-   € à”IØ”ZÝ˜tœÑ/Ô/Øœð	
ð 
ð 	
r   ©r   ro   )Ú__name__Ú
__module__Ú__qualname__Ú__annotations__rk   rm   rn   rt   r   r   r   rj   rj   Û   sf   € € € € € € à€I€I�IØ€EÐÐÐÑØ€JÐÐÐÑØ€HÐÐÐÑð
ð 
ð 
ð 
ð 
ð 
r   rj   c                  ó¶   — e Zd ZU ded<   ded<   ded<   ded<   dZded	<   dZded
<   dZded<   dZded<   dZded<   dZ	ded<    e
e¬¦  «        Zded<   dd„ZdS )ÚProjectr   Úidr#   r   r0   Ú
created_atNr$   Údescriptionr\   r]   rZ   r[   Frl   Úarchived)Údefault_factoryúList[ProjectFolder]Úfoldersr   ro   c                óº   — | j         | j        | j        | j        | j        | j        | j        | j        t          | j	        ¦  «        | j
        d„ | j        D ¦   «         dœS )Nc                ó6   — g | ]}|                      ¦   «         ‘ŒS r   )rt   )ra   Úfs     r   ú
<listcomp>z#Project.to_dict.<locals>.<listcomp>  s    € Ð:Ð:Ð:¨˜Ÿ	š	™œÐ:Ð:Ð:r   )r|   r#   r   r~   r\   r]   rZ   r[   r   r}   r‚   )r|   r#   r   r~   r\   r]   rZ   r[   rl   r   r}   r‚   rr   s    r   rt   zProject.to_dictù   s`   € à”'Ø”IØ”IØÔ+Ø”IØ”ZØœ/Ø Ô-Ý˜Tœ]Ñ+Ô+Øœ/Ø:Ð:¨T¬\Ð:Ñ:Ô:ð
ð 
ð 	
r   ru   )rv   rw   rx   ry   r~   r\   r]   rZ   r[   r   r   Úlistr‚   rt   r   r   r   r{   r{   ë   sÒ   € € € € € € à€G€G�GØ€I€I�IØ€I€I�IØ€O€O�OØ!%€KÐ%Ð%Ð%Ñ%Ø€DÐÐÐÑØ€EÐÐÐÑØ $€JÐ$Ð$Ð$Ñ$Ø"&€LÐ&Ð&Ð&Ñ&Ø€HÐÐÐÑØ#( 5¸Ð#>Ñ#>Ô#>€GÐ>Ð>Ð>Ñ>ð
ð 
ð 
ð 
ð 
ð 
r   r{   rb   úsqlite3.Rowc                ó6  — |                       ¦   «         }t          | d         | d         | d         | d         d|v r| d         nd d|v r| d         nd d|v r| d         nd d|v r| d         nd d	|v r| d	         nd d
|v rt          | d
         ¦  «        nd¬¦
  «
        S )Nr|   r#   r   r}   r~   r\   r]   rZ   r[   r   F)
r|   r#   r   r}   r~   r\   r]   rZ   r[   r   )Úkeysr{   rl   )rb   rŠ   s     r   Ú_project_from_rowr‹   	  sÌ   € Ø�8Š8‰:Œ:€DÝØˆtŒ9Ø�Œ[Ø�Œ[Ø�|Ô$Ø*7¸4Ð*?Ð*?�C˜Ô&Ð&ÀTØ" d˜N˜NˆS�Œ[ˆ[°Ø%¨˜o˜oˆc�'Œlˆl°4Ø(4¸Ð(<Ð(<�3�|Ô$Ð$À$Ø,:¸dÐ,BÐ,B�S˜Ô(Ð(ÈØ*4¸Ð*<Ð*<•�c˜*”oÑ&Ô&Ð&À%ðñ ô ð r   Ú
project_idr�   c                ól   — |                       d|f¦  «                             ¦   «         }d„ |D ¦   «         S )NzySELECT path, label, is_primary, added_at FROM project_folders WHERE project_id = ? ORDER BY is_primary DESC, added_at ASCc           
     ó~   — g | ]:}t          |d          |d         t          |d         ¦  «        |d         ¬¦  «        ‘Œ;S )r5   rk   rm   rn   rq   )rj   rl   ©ra   Úrs     r   r†   z!_load_folders.<locals>.<listcomp>  s]   € ð ð ð ð õ 	Ø�6”Ø�G”*Ý˜A˜lœOÑ,Ô,Ø�z”]ð		
ñ 	
ô 	
ðð ð r   ©rO   Úfetchall)rW   rŒ   Úrowss      r   Ú_load_foldersr”     sQ   € Ø�<Š<ð	Fà	ˆñô ÷ ‚h�j„jð	 	ð
ð ð ðñ ô ð r   r   c                ó:   — t          | |j        ¦  «        |_        |S r2   )r”   r|   r‚   )rW   r   s     r   Ú_attach_foldersr–   *  s   € Ý# D¨'¬*Ñ5Ô5€G„OØ€Nr   Ú	candidatec                ó*  — |}d}|}|                       d|f¦  «                             ¦   «         �c|dz  }d|› �}|ddt          |¦  «        z
  …                              d¦  «        |z   }|                       d|f¦  «                             ¦   «         ­c|S )z=Return ``candidate`` or ``candidate-2``, ``-3`` ... if taken.é   z%SELECT 1 FROM projects WHERE slug = ?Nr   r   r   )rO   ÚfetchoneÚlenr;   )rW   r—   ÚbaseÚnr#   Úsuffixs         r   Ú_unique_slugrŸ   4  sª   € à€DØ	€AØ€DØ
�,Š,Ø/°$°ñô ç‚h�j„jðð 	
ˆQ‰ˆØ�Q��ˆØÐ'�r�C ™KœKÑ'Ð'Ô(×0Ò0°Ñ6Ô6¸Ñ?ˆð �,Š,Ø/°$°ñô ç‚h�j„jðð €Kr   )r#   r‚   r[   r~   r\   r]   rZ   r‚   úOptional[Iterable[str]]r[   r~   r\   r]   rZ   c               óâ  — t          |pd¦  «                             ¦   «         }|st          d¦  «        ‚|rt          |¦  «        nt	          |¦  «        }	t          ¦   «         }
t          ¦   «         }g }|pg D ],}t          |¦  «        }|r||vr|                     |¦  «         Œ-|rt          |¦  «        nd}|r||vr| 	                    d|¦  «         |€
|r|d         }t          | ¦  «        5  t          | |	¦  «        }|                      d|
||||||rt          |¦  «        nd||f	¦  «         |D ]%}|                      d|
|d||k    rdnd|f¦  «         Œ&	 ddd¦  «         n# 1 swxY w Y   |
S )z÷Create a project and return its id.

    ``folders`` are normalized to absolute paths. If ``primary_path`` is given
    it is added to the folder set (if not already present) and marked primary;
    otherwise the first folder becomes primary.
    r   úproject name must not be emptyNr   z–INSERT INTO projects (id, slug, name, description, icon, color, board_slug,  primary_path, created_at, archived) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)zbINSERT INTO project_folders (project_id, path, label, is_primary, added_at) VALUES (?, ?, ?, ?, ?)r™   )r   r   r(   r)   r"   r/   r4   r=   ÚappendÚinsertr   rŸ   rO   )rW   r   r#   r‚   r[   r~   r\   r]   rZ   Úslug_candidateÚpidÚnowÚfolder_pathsr…   ÚnormÚprimaryÚuniquer5   s                     r   Úcreate_projectr¬   B  s  € õ$ ˆtˆz�r‰?Œ?× Ò Ñ"Ô"€DØð ;ÝÐ9Ñ:Ô:Ð:à-1ÐE•^ DÑ)Ô)Ð)µxÀ±~´~€NÝ
Ñ
Ô
€CÝ
‰&Œ&€Cà €LØˆ]˜ð &ð &ˆÝ˜qÑ!Ô!ˆØð 	&�D Ð,Ð,Ø×Ò Ñ%Ô%Ð%øà/;ÐE�o˜lÑ+Ô+Ð+À€GØð (�7 ,Ð.Ð.Ø×Ò˜A˜wÑ'Ô'Ð'Ø€˜<€Ø˜q”/ˆå	�4‰Œð ð Ý˜d NÑ3Ô3ˆØ�Šð4ð
 ØØØØØØ.8ÐB•˜zÑ*Ô*Ð*¸dØØð
ñ	
ô 	
ð 	
ð" !ð 	ð 	ˆDØ�LŠLð)ð �d˜D t¨w¢  ! !¸A¸sÐCñ	ô ð ð ð	ð'ð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð4 €Js   Ã.A)E$Å$E(Å+E(F)Úinclude_archivedr­   rl   úList[Project]c               óŠ   ‡ — d}|s|dz  }|dz  }‰                       |¦  «                             ¦   «         }ˆ fd„|D ¦   «         S )NzSELECT * FROM projectsz WHERE archived = 0z ORDER BY created_at ASCc                óJ   •— g | ]}t          ‰t          |¦  «        ¦  «        ‘Œ S r   )r–   r‹   )ra   r�   rW   s     €r   r†   z!list_projects.<locals>.<listcomp>�  s,   ø€ ÐFÐFÐF¸A�O˜DÕ"3°AÑ"6Ô"6Ñ7Ô7ÐFÐFÐFr   r‘   )rW   r­   Úsqlr“   s   `   r   Úlist_projectsr²   …  s_   ø€ ð #€CØð %ØÐ$Ñ$ˆØÐ%Ñ%€CØ�<Š<˜ÑÔ×%Ò%Ñ'Ô'€DØFÐFÐFÐFÀÐFÑFÔFÐFr   Ú
id_or_slugúOptional[Project]c                ó*  — |                       d|f¦  «                             ¦   «         }|€H|                       dt          |¦  «                             ¦   «         f¦  «                             ¦   «         }|€dS t	          | t          |¦  «        ¦  «        S )z,Look up a project by id first, then by slug.z#SELECT * FROM projects WHERE id = ?Nz%SELECT * FROM projects WHERE slug = ?)rO   rš   r   r   r–   r‹   )rW   r³   rb   s      r   Úget_projectr¶   �  s�   € ð �,Š,Ø-°
¨}ñô ç‚h�j„jð ð €{Ø�lŠlØ3µc¸*±o´o×6KÒ6KÑ6MÔ6MÐ5Oñ
ô 
ç
Š(‰*Œ*ð 	ð €{ØˆtÝ˜4Õ!2°3Ñ!7Ô!7Ñ8Ô8Ð8r   )r   r~   r\   r]   rZ   c               ód  — g }g }|�\t          |¦  «                             ¦   «         }	|	st          d¦  «        ‚|                     d¦  «         |                     |	¦  «         |�*|                     d¦  «         |                     |¦  «         |�,|                     d¦  «         |                     |pd¦  «         |�,|                     d¦  «         |                     |pd¦  «         |�M|                     d¦  «         |                     |                     ¦   «         rt	          |¦  «        nd¦  «         |sdS |                     |¦  «         t          | ¦  «        5  |                      d	d
                     |¦  «        › d�|¦  «        }
ddd¦  «         n# 1 swxY w Y   |
j        dk    S )u  Patch top-level project fields. Only provided fields change.

    ``icon``, ``color``, and ``board_slug`` accept an empty string to clear
    (store NULL) â€” passing ``None`` leaves the field untouched, so callers that
    want to clear must send ``""``.
    Nr¢   zname = ?zdescription = ?zicon = ?z	color = ?zboard_slug = ?FzUPDATE projects SET z, z WHERE id = ?r   )	r   r   r(   r£   r)   r   rO   ÚjoinÚrowcount)rW   rŒ   r   r~   r\   r]   rZ   ÚsetsÚparamsr�   Úcurs              r   Úupdate_projectr½      sð  € ð  €DØ€FØÐÝ�‰IŒI�OŠOÑÔˆØð 	?ÝÐ=Ñ>Ô>Ð>Ø�Š�JÑÔÐØ�Š�aÑÔÐØÐØ�ŠÐ%Ñ&Ô&Ð&Ø�Š�kÑ"Ô"Ð"ØÐØ�Š�JÑÔÐØ�Š�d�l˜dÑ#Ô#Ð#ØÐØ�Š�KÑ Ô Ð Ø�Š�e�m˜tÑ$Ô$Ð$ØÐØ�ŠÐ$Ñ%Ô%Ð%Ø�Š°J×4DÒ4DÑ4FÔ4FÐP•n ZÑ0Ô0Ð0ÈDÑQÔQÐQØð ØˆuØ
‡M‚M�*ÑÔÐÝ	�4‰Œð 
ð 
Ø�lŠlØA 4§9¢9¨T¡?¤?ÐAÐAÐAÀ6ñ
ô 
ˆð
ð 
ð 
ñ 
ô 
ð 
ð 
ð 
ð 
ð 
ð 
øøøð 
ð 
ð 
ð 
ð Œ<˜!ÒÐs   Å".FÆF Æ#F )rk   rm   rk   rm   c               óþ  — t          |¦  «        }|st          d¦  «        ‚t          | |¦  «        €t          d|› �¦  «        ‚t          ¦   «         }t	          | ¦  «        5  |                      d||||f¦  «         |�|                      d|||f¦  «         |rt          | ||¦  «         n<|                      d|f¦  «                             ¦   «         }|€t          | ||¦  «         ddd¦  «         n# 1 swxY w Y   |S )zÙAdd a folder to a project. Returns the normalized path.

    When ``is_primary`` is set, the folder becomes the project's primary repo
    (the previous primary is demoted, and ``projects.primary_path`` updates).
    zfolder path must not be emptyNzno such project: zlINSERT OR IGNORE INTO project_folders (project_id, path, label, is_primary, added_at) VALUES (?, ?, ?, 0, ?)zFUPDATE project_folders SET label = ? WHERE project_id = ? AND path = ?zESELECT 1 FROM project_folders WHERE project_id = ? AND is_primary = 1)r=   r(   r¶   r4   r   rO   Ú_set_primary_lockedrš   )rW   rŒ   r5   rk   rm   r©   r§   Úexisting_primarys           r   Ú
add_folderrÁ   Î  s|  € õ ˜4Ñ Ô €DØð :ÝÐ8Ñ9Ô9Ð9Ý�4˜Ñ$Ô$Ð,ÝÐ9¨ZÐ9Ð9Ñ:Ô:Ð:Ý
‰&Œ&€CÝ	�4‰Œð <ð <Ø�Šð%ð ˜˜u cÐ*ñ		
ô 	
ð 	
ð ÐØ�LŠLð4à˜
 DÐ)ñô ð ð
 ð 
	<Ý  j°$Ñ7Ô7Ð7Ð7ð  $Ÿ|š|ð:à�ñ ô  ÷ Šh‰jŒjð	 ð
  Ð'Ý# D¨*°dÑ;Ô;Ð;ð/<ð <ð <ñ <ô <ð <ð <ð <ð <ð <ð <øøøð <ð <ð <ð <ð0 €Ks   Á BC2Ã2C6Ã9C6c                óÜ  — t          |¦  «        }t          | ¦  «        5  |                      d||f¦  «                             ¦   «         }|                      d||f¦  «        }|�h|d         r`|                      d|f¦  «                             ¦   «         }|r|d         nd}|rt	          | ||¦  «         n|                      d|f¦  «         ddd¦  «         n# 1 swxY w Y   |j        dk    S )	zCRemove a folder from a project. Repoints primary if it was primary.zHSELECT is_primary FROM project_folders WHERE project_id = ? AND path = ?z=DELETE FROM project_folders WHERE project_id = ? AND path = ?Nrm   zSSELECT path FROM project_folders WHERE project_id = ? ORDER BY added_at ASC LIMIT 1r5   z4UPDATE projects SET primary_path = NULL WHERE id = ?r   )r=   r   rO   rš   r¿   r¹   )rW   rŒ   r5   r©   Úwas_primaryr¼   ÚnxtÚnew_primarys           r   Úremove_folderrÆ   ü  sY  € å˜4Ñ Ô €DÝ	�4‰Œð ð Ø—l’lð0à˜Ðñ
ô 
÷ Š(‰*Œ*ð	 	ð
 �lŠlØKØ˜Ðñ
ô 
ˆð Ð" {°<Ô'@Ð"Ø—,’,ð0à�ñô ÷ Šh‰jŒjð	 ð
 *-Ð6˜#˜fœ+˜+°$ˆKØð Ý# D¨*°kÑBÔBÐBÐBà—’ØJØ�Mñô ð ð)ð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð ð0 Œ<˜!ÒÐs   ŸB-CÃCÃCc                ó”   — |                       d|f¦  «         |                       d||f¦  «         |                       d||f¦  «         dS )z:Set the primary folder (caller already holds a write txn).z>UPDATE project_folders SET is_primary = 0 WHERE project_id = ?zKUPDATE project_folders SET is_primary = 1 WHERE project_id = ? AND path = ?z1UPDATE projects SET primary_path = ? WHERE id = ?N)rO   )rW   rŒ   r5   s      r   r¿   r¿     st   € ð 	‡L‚LØHØ	ˆñô ð ð 	‡L‚Lð	,à	�TÐñô ð ð
 	‡L‚LØ;Ø	ˆzÐñô ð ð ð r   c                ó  — t          |¦  «        }t          | ¦  «        5  |                      d||f¦  «                             ¦   «         }|€	 d d d ¦  «         dS t	          | ||¦  «         d d d ¦  «         n# 1 swxY w Y   dS )Nz?SELECT 1 FROM project_folders WHERE project_id = ? AND path = ?FT)r=   r   rO   rš   r¿   )rW   rŒ   r5   r©   Úexistss        r   Úset_primaryrÊ   -  sé   € Ý˜4Ñ Ô €DÝ	�4‰Œð 4ð 4Ø—’ØMØ˜Ðñ
ô 
÷ Š(‰*Œ*ð 	ð ˆ>Øð4ð 4ð 4ñ 4ô 4ð 4ð 4ð 4õ 	˜D *¨dÑ3Ô3Ð3ð4ð 4ð 4ñ 4ô 4ð 4ð 4ð 4ð 4ð 4ð 4øøøð 4ð 4ð 4ð 4ð ˆ4s   Ÿ.A7ÁA7Á7A;Á>A;c                ó”   — t          | ¦  «        5  |                      d|f¦  «        }d d d ¦  «         n# 1 swxY w Y   |j        dk    S )Nz-UPDATE projects SET archived = 1 WHERE id = ?r   ©r   rO   r¹   ©rW   rŒ   r¼   s      r   Úarchive_projectrÎ   :  óŒ   € Ý	�4‰Œð 
ð 
Ø�lŠlØ;¸j¸]ñ
ô 
ˆð
ð 
ð 
ñ 
ô 
ð 
ð 
ð 
ð 
ð 
ð 
øøøð 
ð 
ð 
ð 
ð Œ<˜!ÒÐó   �4´8»8c                ó”   — t          | ¦  «        5  |                      d|f¦  «        }d d d ¦  «         n# 1 swxY w Y   |j        dk    S )Nz-UPDATE projects SET archived = 0 WHERE id = ?r   rÌ   rÍ   s      r   Úrestore_projectrÒ   B  rÏ   rÐ   c                ó”   — t          | ¦  «        5  |                      d|f¦  «        }ddd¦  «         n# 1 swxY w Y   |j        dk    S )z0Hard-delete a project and its folders (cascade).z!DELETE FROM projects WHERE id = ?Nr   rÌ   rÍ   s      r   Údelete_projectrÔ   J  s—   € å	�4‰Œð Oð OØ�lŠlÐ>ÀÀÑNÔNˆðOð Oð Oñ Oô Oð Oð Oð Oð Oð Oð Oøøøð Oð Oð Oð OàŒ<˜!ÒÐrÐ   Ú	active_idÚrepo_discovery_policyc                óÎ   — t          | ¦  «        5  |€|                      dt          f¦  «         n|                      dt          |f¦  «         ddd¦  «         dS # 1 swxY w Y   dS )z9Set (or clear, when ``None``) the active project pointer.Nz&DELETE FROM project_meta WHERE key = ?úiINSERT INTO project_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value)r   rO   Ú_ACTIVE_META_KEY)rW   rŒ   s     r   Ú
set_activerÚ   Z  s¸   € å	�4‰Œð ð ØÐØ�LŠLÐAÕDTÐCVÑWÔWÐWÐWà�LŠLðHå! :Ð.ñô ð ð	ð ð ñ ô ð ð ð ð ð ð ð øøøð ð ð ð ð ð s   �=AÁAÁ!Ac                óv   — |                       dt          f¦  «                             ¦   «         }|r|d         nd S ©Nz,SELECT value FROM project_meta WHERE key = ?Úvalue)rO   rÙ   rš   ©rW   rb   s     r   Úget_active_idrß   g  s?   € Ø
�,Š,Ø6Õ9IÐ8Kñô ç‚h�j„jð ð Ð(ˆ3ˆwŒ<ˆ< DÐ(r   c                óv   — |                       dt          f¦  «                             ¦   «         }|r|d         nd S rÜ   )rO   Ú_DISCOVERY_POLICY_META_KEYrš   rÞ   s     r   Úget_discovery_policy_keyrâ   n  s?   € Ø
�,Š,Ø6Õ9SÐ8Uñô ç‚h�j„jð ð Ð(ˆ3ˆwŒ<ˆ< DÐ(r   )Úpreserve_unversionedÚ
policy_keyrã   c               óø   — t          | ¦  «        }||k    rdS |dup| }t          | ¦  «        5  |r|                      d¦  «         |                      dt          |f¦  «         ddd¦  «         n# 1 swxY w Y   |S )zÃClear cached scan rows when their discovery policy changes.

    Existing pre-policy rows are retained only for the backward-compatible
    default policy. Returns whether rows were cleared.
    FNúDELETE FROM discovered_reposrØ   )râ   r   rO   rá   )rW   rä   rã   ÚcurrentÚcleareds        r   Ú!reconcile_discovered_repos_policyré   u  sÛ   € õ ' tÑ,Ô,€GØ�*ÒÐØˆuà˜TÐ!Ð=Ð)=Ð%=€GÝ	�4‰Œð 
ð 
Øð 	9Ø�LŠLÐ7Ñ8Ô8Ð8Ø�ŠðDå'¨Ð4ñ	
ô 	
ð 	
ð
ð 
ð 
ñ 
ô 
ð 
ð 
ð 
ð 
ð 
ð 
øøøð 
ð 
ð 
ð 
ð €Ns   ®5A/Á/A3Á6A3)rä   c               ó¾   — t          | ¦  «        5  |                      d¦  «         |�|                      dt          |f¦  «         d d d ¦  «         d S # 1 swxY w Y   d S )Nræ   rØ   )r   rO   rá   )rW   rä   s     r   Úclear_discovered_reposrë   �  s±   € õ 
�4‰Œð ð Ø�ŠÐ3Ñ4Ô4Ð4ØÐ!Ø�LŠLðHå+¨ZÐ8ñô ð ðð ð ñ ô ð ð ð ð ð ð ð øøøð ð ð ð ð ð s   �5AÁAÁA)Úreplacerä   Úreposú#Iterable[tuple[str, Optional[str]]]rì   c               óÐ  — t          ¦   «         }g }|D ]P\  }}t          |¦  «        }|sŒ|                     ||p t          j                             |¦  «        p||f¦  «         ŒQt          | ¦  «        5  |r|                      d¦  «         |r|                      d|¦  «         |�|                      dt          |f¦  «         ddd¦  «         n# 1 swxY w Y   t          |¦  «        S )a�  Persist scanned git repo roots into the cache.

    ``repos`` is an iterable of ``(root, label)``. Roots are normalized; the
    label falls back to the basename. Returns the number of rows written.

    When ``replace`` is true, this is the authoritative result of a fresh disk
    scan: delete stale rows first so old eval/worktree noise disappears instead
    of living forever in the cache.
    ræ   z�INSERT INTO discovered_repos (root, label, last_seen) VALUES (?, ?, ?) ON CONFLICT(root) DO UPDATE SET label = excluded.label, last_seen = excluded.last_seenNrØ   )r4   r=   r£   r8   r5   Úbasenamer   rO   Úexecutemanyrá   r›   )	rW   rí   rì   rä   r§   r“   Úrootrk   r©   s	            r   Úrecord_discovered_reposró   ¢  sU  € õ  ‰&Œ&€CØ€DØð Lð L‰ˆˆeÝ˜tÑ$Ô$ˆØð 	ØØ�Š�T˜EÐC¥R¤W×%5Ò%5°dÑ%;Ô%;ÐC¸tÀcÐJÑKÔKÐKÐKå	�4‰Œð ð Øð 	9Ø�LŠLÐ7Ñ8Ô8Ð8Øð 	Ø×Òð1ð ñ	ô ð ð Ð!Ø�LŠLðHå+¨ZÐ8ñô ð ðð ð ñ ô ð ð ð ð ð ð øøøð ð ð ð õ  ˆt‰9Œ9Ðs   Á3ACÃCÃCú
List[dict]c                óh   — |                       d¦  «                             ¦   «         }d„ |D ¦   «         S )z;All cached discovered repo roots, most-recently-seen first.zKSELECT root, label, last_seen FROM discovered_repos ORDER BY last_seen DESCc                ó>   — g | ]}|d          |d         |d         dœ‘ŒS )rò   rk   Ú	last_seen)rò   rk   r÷   r   r�   s     r   r†   z)list_discovered_repos.<locals>.<listcomp>Ò  s>   € ð ð ð àð �6” Q w¤Z¸aÀ¼nÐMÐMðð ð r   r‘   )rW   r“   s     r   Úlist_discovered_reposrø   Í  sE   € à�<Š<ØUñô ç‚h�j„jð 	ðð àðñ ô ð r   c               ó0  — t          |pd¦  «                             ¦   «         sdS t          |¦  «        }d}|s|dz  }d}d}|                      |¦  «                             ¦   «         D ]š}|d         }||k    s`|                     |                     d¦  «        t          j        z   ¦  «        s+|                     |                     d¦  «        dz   ¦  «        r*t          |¦  «        |k    rt          |¦  «        }|d	         }Œ›|€dS t          | |¦  «        S )
zûReturn the project owning ``path`` (longest-prefix folder match).

    A folder owns ``path`` when ``path`` equals the folder or is nested under
    it. The most specific (longest) folder wins, so nested projects resolve to
    the innermost one.
    r   NznSELECT pf.project_id AS pid, pf.path AS folder FROM project_folders pf JOIN projects p ON p.id = pf.project_idz WHERE p.archived = 0éÿÿÿÿÚfolderr7   ú/r¦   )r   r   r=   rO   r’   Ú
startswithr;   r8   Úsepr›   r¶   )	rW   r5   r­   Útargetr±   Úbest_pidÚbest_lenrb   rû   s	            r   Úproject_for_pathr  Ý  s'  € õ ˆtˆz�r‰?Œ?× Ò Ñ"Ô"ð ØˆtÝ˜TÑ"Ô"€Fð	Jð ð ð 'ØÐ&Ñ&ˆØ"€HØ€HØ�|Š|˜CÑ Ô ×)Ò)Ñ+Ô+ð &ð &ˆØ�X”ˆØ�VÒÐ˜v×0Ò0°·²¸uÑ1EÔ1EÍÌÑ1NÑOÔOÐØ×!Ò! &§-¢-°Ñ"6Ô"6¸Ñ"<Ñ=Ô=ð å�6‰{Œ{˜XÒ%Ð%Ý˜v™;œ;�Ø˜uœ:�øØÐØˆtÝ�t˜XÑ&Ô&Ð&r   z[^a-z0-9._-]+r   )ÚtitleÚtask_idr  c               óX  — | j         pt          | j        ¦  «        }|› d|› �}|r…t                               dt          |¦  «                             ¦   «                              ¦   «         ¦  «                             d¦  «        }|dd…                              d¦  «        }|r|› d|› �}|S )zÚDeterministic branch name for a project-linked kanban task.

    Shape: ``<project-slug>/<task-id>`` (optionally ``-<title-slug>``). Stable
    and human-meaningful, replacing the random ``wt/<task-id>`` fallback.
    rü   r   Né(   )r#   r"   r   Ú_BRANCH_SAFE_REr    r   r   r   )r   r  r  r#   rœ   Útslugs         r   Úbranch_name_forr	    s±   € ð Œ<Ð1�8 G¤LÑ1Ô1€DØÐÐ�WÐÐ€DØð %Ý×#Ò# C­¨U©¬×)9Ò)9Ñ);Ô);×)AÒ)AÑ)CÔ)CÑDÔD×JÒJÈ3ÑOÔOˆØ�c�r�c”
× Ò  Ñ%Ô%ˆØð 	%ØÐ$Ð$˜UÐ$Ð$ˆDØ€Kr   )r   r   )r   r   r   r   )r#   r$   r   r$   )r   r   )r   r0   )r5   r   r   r   r2   )r?   r@   r   rA   )r?   r@   )rW   rA   r   r^   )rb   rˆ   r   r{   )rW   rA   rŒ   r   r   r�   )rW   rA   r   r{   r   r{   )rW   rA   r—   r   r   r   )rW   rA   r   r   r#   r$   r‚   r    r[   r$   r~   r$   r\   r$   r]   r$   rZ   r$   r   r   )rW   rA   r­   rl   r   r®   )rW   rA   r³   r   r   r´   )rW   rA   rŒ   r   r   r$   r~   r$   r\   r$   r]   r$   rZ   r$   r   rl   )rW   rA   rŒ   r   r5   r   rk   r$   rm   rl   r   r   )rW   rA   rŒ   r   r5   r   r   rl   )rW   rA   rŒ   r   r5   r   r   r^   )rW   rA   rŒ   r   r   rl   )rW   rA   rŒ   r$   r   r^   )rW   rA   r   r$   )rW   rA   rä   r   rã   rl   r   rl   )rW   rA   rä   r$   r   r^   )
rW   rA   rí   rî   rì   rl   rä   r$   r   r0   )rW   rA   r   rô   )rW   rA   r5   r   r­   rl   r   r´   )r   r{   r  r   r  r   r   r   )FÚ__doc__Ú
__future__r   Ú
contextlibr8   r   r-   rJ   r3   Údataclassesr   r   Úpathlibr   Útypingr   r   r	   Úhermes_cli.sqlite_utilr
   rf   r   Úhermes_constantsr   r   rQ   Úcompiler&   r"   r)   r/   r4   r=   Úsetr>   ry   rK   ÚcontextmanagerrY   re   rR   rj   r{   r‹   r”   r–   rŸ   r¬   r²   r¶   r½   rÁ   rÆ   r¿   rÊ   rÎ   rÒ   rÔ   rÙ   rá   rÚ   rß   râ   ré   rë   ró   rø   r  r  r	  r   r   r   ú<module>r     s  ððð ð ð. #Ð "Ð "Ð "Ð "Ð "à Ð Ð Ð Ø 	€	€	€	Ø 	€	€	€	Ø €€€Ø €€€Ø €€€Ø (Ð (Ð (Ð (Ð (Ð (Ð (Ð (Ø Ð Ð Ð Ð Ð Ø +Ð +Ð +Ð +Ð +Ð +Ð +Ð +Ð +Ð +à ]Ð ]Ð ]Ð ]Ð ]Ð ]Ð ]Ð ]Ø ,Ð ,Ð ,Ð ,Ð ,Ð ,ð-ð -ð -ð -ð'€
ðd ˆ2Œ:Ð4Ñ5Ô5€ðð ð ð ðð ð ð ð 'ð 'ð 'ð 'ðð ð ð ð ð  ð  ð  ð  #˜s™uœuÐ Ð $Ð $Ð $Ñ $ðð ð ð ð ð6 Ôðð ð ð ñ Ôðð( LÐ ðIð Ið Ið Ið ð
ð 
ð 
ð 
ð 
ñ 
ô 
ñ „ð
ð ð
ð 
ð 
ð 
ð 
ñ 
ô 
ñ „ð
ð:ð ð ð ð ð ð ð ð"ð ð ð ðð ð ð ð$ Ø'+Ø"&Ø!%ØØØ $ð@ð @ð @ð @ð @ð @ðH ;@ðGð Gð Gð Gð Gð Gð9ð 9ð 9ð 9ð( Ø!%ØØØ $ð+ð +ð +ð +ð +ð +ðf  Øð+ð +ð +ð +ð +ð +ð\ð ð ð ð<ð ð ð ð&
ð 
ð 
ð 
ðð ð ð ðð ð ð ðð ð ð ð Ð Ø4Ð ð
ð 
ð 
ð 
ð)ð )ð )ð )ð)ð )ð )ð )ð "'ð	ð ð ð ð ð ð8 >Bð
ð 
ð 
ð 
ð 
ð 
ð, Ø $ð(ð (ð (ð (ð (ð (ðVð ð ð ð" FKð'ð 'ð 'ð 'ð 'ð 'ðB �"”*Ð-Ñ.Ô.€ð EGð ð ð ð ð ð ð ð r   