
    j?                       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mZm	Z	m
Z
 ddlmZmZ ddl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e G d d                      Z d?dZ!i Z"de#d<   d@dZ$dAdZ%dBdZ&dCd Z'dDd$Z( ej)        d%ej*                  Z+dEd&Z,dFd)Z-edd*dGd2Z. G d3 d4          Z/d5Z0d6Z1ed7dd8dHd=Z2g d>Z3dS )Iun  Persistent session goals — the Ralph loop for Hermes.

A goal is a free-form user objective that stays active across turns. After
each turn completes, a small judge call asks an auxiliary model "is this
goal satisfied by the assistant's last response?". If not, Hermes feeds a
continuation prompt back into the same session and keeps working until the
goal is done, turn budget is exhausted, the user pauses/clears it, or the
user sends a new message (which takes priority and pauses the goal loop).

State is persisted in SessionDB's ``state_meta`` table keyed by
``goal:<session_id>`` so ``/resume`` picks it up.

Design notes / invariants:

- The continuation prompt is just a normal user message appended to the
  session via ``run_conversation``. No system-prompt mutation, no toolset
  swap — prompt caching stays intact.
- Judge failures are fail-OPEN: ``continue``. A broken judge must not wedge
  progress; the turn budget is the backstop.
- When a real user message arrives mid-loop it preempts the continuation
  prompt and also pauses the goal loop for that turn (we still re-judge
  after, so if the user's message happens to complete the goal the judge
  will say ``done``).
- This module has zero hard dependency on ``cli.HermesCLI`` or the gateway
  runner — both wire the same ``GoalManager`` in.

Nothing in this module touches the agent's system prompt or toolset.
    )annotationsN)	dataclassfieldasdict)datetimetimezone)AnyDictListOptionalTuple   g      >@i   i     a  [Continuing toward your standing goal]
Goal: {goal}

Continue working toward this goal. Take the next concrete step. If you believe the goal is complete, state so explicitly and stop. If you are blocked and need input from the user, say so clearly and stop.a{  [Continuing toward your standing goal]
Goal: {goal}

Additional criteria the user added mid-loop:
{subgoals_block}

Continue working toward the goal AND all additional criteria. Take the next concrete step. If you believe the goal and every additional criterion are complete, state so explicitly and stop. If you are blocked and need input from the user, say so clearly and stop.u  You are a strict judge evaluating whether an autonomous agent has achieved a user's stated goal. You receive the goal text and the agent's most recent response. Your only job is to decide whether the goal is fully satisfied based on that response.

A goal is DONE only when:
- The response explicitly confirms the goal was completed, OR
- The response clearly shows the final deliverable was produced, OR
- The response explains the goal is unachievable / blocked / needs user input (treat this as DONE with reason describing the block).

Otherwise the goal is NOT done — CONTINUE.

Reply ONLY with a single JSON object on one line:
{"done": <true|false>, "reason": "<one-sentence rationale>"}zlGoal:
{goal}

Agent's most recent response:
{response}

Current time: {current_time}

Is the goal satisfied?u  Goal:
{goal}

Additional criteria the user added mid-loop (all must also be satisfied for the goal to be DONE):
{subgoals_block}

Agent's most recent response:
{response}

Current time: {current_time}

Decision: For each numbered criterion above, find concrete evidence in the agent's response that the criterion is satisfied. Do not accept generic phrases like 'all requirements met' or 'implying it was done' — require specific evidence (a file contents excerpt, an output line, a command result). If ANY criterion lacks specific evidence in the response, the goal is NOT done — return CONTINUE.

Is the goal AND every additional criterion satisfied?c                      e Zd ZU dZded<   dZded<   dZded<   e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<   dZded<    ee          Zded<   ddZedd            ZddZdS )	GoalStatez+Serializable goal state stored per session.strgoalactivestatusr   int
turns_used	max_turns        float
created_atlast_turn_atNOptional[str]last_verdictlast_reasonpaused_reasonconsecutive_parse_failures)default_factoryz	List[str]subgoalsreturnc                H    t          j        t          |           d          S )NF)ensure_ascii)jsondumpsr   selfs    5/home/ubuntu/.hermes/hermes-agent/hermes_cli/goals.pyto_jsonzGoalState.to_json   s    z&,,U;;;;    raw'GoalState'c                   t          j        |          }|                    d          pg }g }t          |t                    rd |D             } | |                    dd          |                    dd          t          |                    dd          pd          t          |                    d	t                    pt                    t          |                    d
d          pd          t          |                    dd          pd          |                    d          |                    d          |                    d          t          |                    dd          pd          |          S )Nr#   c                    g | ]D}t          |                                          #t          |                                          ES  )r   strip.0ss     r+   
<listcomp>z'GoalState.from_json.<locals>.<listcomp>   s9    OOO1AOAOOOr-   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'   loadsget
isinstancelistr   DEFAULT_MAX_TURNSr   )clsr.   dataraw_subgoalsr#   s        r+   	from_jsonzGoalState.from_json   sV   z#xx
++1r lD)) 	POOOOOHs&"%%88Hh//488L!449::$((;0ABBWFWXXTXXlC88?C@@txx<<CDD.11//((?33'*4884PRS+T+T+YXY'Z'Z
 
 
 	
r-   c                z    | j         sdS d                    d t          | j         d          D                       S )z\Render the subgoals as a numbered ``- N. text`` block. Empty
        when no subgoals exist.r8   
c              3  ,   K   | ]\  }}d | d| V  dS z- z. Nr2   r5   itexts      r+   	<genexpr>z2GoalState.render_subgoals_block.<locals>.<genexpr>   s7      [[ga)a))4))[[[[[[r-      start)r#   join	enumerater)   s    r+   render_subgoals_blockzGoalState.render_subgoals_block   sD     } 	2yy[[4=XY9Z9Z9Z[[[[[[r-   r$   r   )r.   r   r$   r/   )__name__
__module____qualname____doc____annotations__r   r   r=   r   r   r   r   r   r    r!   r   r<   r#   r,   classmethodrA   rO   r2   r-   r+   r   r      s-        55IIIFJ&I&&&&JL"&L&&&&!%K%%%%#'M''''&'''''  %555H5555< < < < 
 
 
 [
,\ \ \ \ \ \r-   r   
session_idr   r$   c                    d|  S )Nzgoal:r2   )rW   s    r+   	_meta_keyrY      s    :r-   Dict[str, Any]	_DB_CACHEOptional[Any]c                    	 ddl m}  ddlm} t	           |                       }n3# t
          $ r&}t                              d|           Y d}~dS d}~ww xY wt          	                    |          }||S 	  |            }n3# t
          $ r&}t                              d|           Y d}~dS d}~ww xY w|t          |<   |S )a  Return a SessionDB instance for the current HERMES_HOME.

    SessionDB has no built-in singleton, but opening a new connection per
    /goal call would thrash the file. We cache one instance per
    ``hermes_home`` path so profile switches still pick up the right DB.
    Defensive against import/instantiation failures so tests and
    non-standard launchers can still use the GoalManager.
    r   )get_hermes_home)	SessionDBz,GoalManager: SessionDB bootstrap failed (%s)Nz$GoalManager: SessionDB() raised (%s))
hermes_constantsr^   hermes_stater_   r   	Exceptionloggerdebugr[   r:   )r^   r_   homeexccacheddbs         r+   _get_session_dbri      s   444444******??$$%%   CSIIIttttt ]]4  FY[[   ;SAAAttttt IdOIs,   #& 
AAA8
B 
B3B..B3Optional[GoalState]c                   | sdS t                      }|dS 	 |                    t          |                     }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w|sdS 	 t                              |          S # t          $ r'}t                              d| |           Y d}~dS d}~ww xY w)z4Load the goal for a session, or None if none exists.Nz GoalManager: get_meta failed: %sz3GoalManager: could not parse stored goal for %s: %s)	ri   get_metarY   rb   rc   rd   r   rA   warning)rW   rh   r.   rf   s       r+   	load_goalrn      s     t			B	ztkk)J//00   7===ttttt  t""3'''   LjZ]^^^ttttts-   "; 
A+A&&A+3B 
B>B99B>stateNonec                   | sdS t                      }|dS 	 |                    t          |           |                                           dS # t          $ r&}t
                              d|           Y d}~dS d}~ww xY w)z5Persist a goal to SessionDB. No-op if DB unavailable.Nz GoalManager: set_meta failed: %s)ri   set_metarY   r,   rb   rc   rd   )rW   ro   rh   rf   s       r+   	save_goalrs     s     			B	z>
Ij))5==??;;;;; > > >7=========>s   5A 
A?A::A?c                Z    t          |           }|dS d|_        t          | |           dS )zDMark a goal cleared in the DB (preserved for audit, status=cleared).Ncleared)rn   r   rs   )rW   ro   s     r+   
clear_goalrv     s6    j!!E}ELj%     r-   rH   limitr   c                N    | sdS t          |           |k    r| S | d |         dz   S )Nr8   u   … [truncated])len)rH   rw   s     r+   	_truncaterz     s9     r
4yyE<+++r-   z\{.*?\}c                 
   	 ddl m}   |             }|                    d          pi                     di                               dt                    }t	          |          }|dk    r|S n# t
          $ r Y nw xY wt          S )a#  Resolve auxiliary.goal_judge.max_tokens, falling back to the default.

    ``load_config()`` is cached on the config file's (mtime, size), so calling
    this once per judge turn is cheap. A non-positive or non-int value falls
    back to the default rather than crashing the goal loop.
    r   )load_config	auxiliary
goal_judge
max_tokens)hermes_cli.configr|   r:   DEFAULT_JUDGE_MAX_TOKENSr   rb   )r|   cfgvalues      r+   _goal_judge_max_tokensr   *  s    111111kmmWW[!!'RSr""S788 	
 E

199L    ##s   A*A. .
A;:A;r.   Tuple[bool, str, bool]c                l   | sdS |                                  }|                    d          r=|                     d          }|                    d          }|dk    r||dz   d         }d}	 t          j        |          }ng# t
          $ rZ t                              |          }|r;	 t          j        |                    d                    }n# t
          $ r d}Y nw xY wY nw xY wt          |t                    sd	d
t          | d          dfS |                    d          }t          |t                    r)|                                                                 dv }nt          |          }t          |                    d          pd                                           }|sd}||d	fS )a  Parse the judge's reply. Fail-open to ``(False, "<reason>", parse_failed)``.

    Returns ``(done, reason, parse_failed)``. ``parse_failed`` is True when the
    judge returned output that couldn't be interpreted as the expected JSON
    verdict (empty body, prose, malformed JSON). Callers use that flag to
    auto-pause after N consecutive parse failures so a weak judge model
    doesn't silently burn the turn budget.
    )Fzjudge returned empty responseTz````rC   rJ   Nr   Fzjudge reply was not JSON:    Tdone>   1yesr   truereasonr8   zno reason provided)r3   
startswithfindr'   r9   rb   _JSON_OBJECT_REsearchgroupr;   dictrz   r:   r   lowerbool)r.   rH   nlr?   matchdone_valr   r   s           r+   _parse_judge_responser   B  s     <;;99;;D u !zz#YYt__88Q=D &*D	z$   &&t,, 	z%++a..11    dD!! QJ9S#3F3FJJDPPxxH(C   ~~%%''+GGH~~(##)r**0022F &%s6   .B &C'*'CC'C!C' C!!C'&C')timeoutr#   r   last_responser   r   r#   Optional[List[str]]Tuple[str, str, bool]c          	        |                                  sdS |                                 sdS 	 ddlm}m} n3# t          $ r&}t
                              d|           Y d}~dS d}~ww xY w	  |d          \  }}n3# t          $ r&}t
                              d	|           Y d}~dS d}~ww xY w||sd
S d |pg D             }	t          j        t          j
                                                                      d          }
|	r|d                    d t          |	d          D                       }t                              t#          | d          t#          |d          t#          |t$                    |
          }n>t&                              t#          | d          t#          |t$                    |
          }	 |j        j                            |dt.          dd|dgdt1                      | |            pd          }nL# t          $ r?}t
                              d|           ddt5          |          j         dfcY d}~S d}~ww xY w	 |j        d         j        j        pd}n# t          $ r d}Y nw xY wt?          |          \  }}}|rdnd}t
                              d|t#          |d                      |||fS )!u  Ask the auxiliary model whether the goal is satisfied.

    Returns ``(verdict, reason, parse_failed)`` where verdict is ``"done"``,
    ``"continue"``, or ``"skipped"`` (when the judge couldn't be reached).

    ``parse_failed`` is True only when the judge call succeeded but its output
    was unusable (empty or non-JSON). API/transport errors return False — they
    are transient and should fail-open silently. Callers use this flag to
    auto-pause after N consecutive parse failures (see
    ``DEFAULT_MAX_CONSECUTIVE_PARSE_FAILURES``).

    ``subgoals`` is an optional list of user-added criteria (from
    ``/subgoal``) that the judge must also factor into its DONE/CONTINUE
    decision. When non-empty the prompt switches to the with-subgoals
    template; otherwise behavior is identical to the original judge.

    This is deliberately fail-open: any error returns ``("continue", "...", False)``
    so a broken judge doesn't wedge progress — the turn budget and the
    consecutive-parse-failures auto-pause are the backstops.
    )skippedz
empty goalF)continuez$empty response (nothing to evaluate)Fr   )get_auxiliary_extra_bodyget_text_auxiliary_clientz.goal judge: auxiliary client import failed: %sN)r   zauxiliary client unavailableFr~   z0goal judge: get_text_auxiliary_client failed: %s)r   zno auxiliary client configuredFc                b    g | ],}||                                 |                                 -S r2   )r3   r4   s     r+   r7   zjudge_goal.<locals>.<listcomp>  s2    MMMAQM17799MaggiiMMMr-   )tzz%Y-%m-%d %H:%M:%S %ZrC   c              3  ,   K   | ]\  }}d | d| V  dS rE   r2   rF   s      r+   rI   zjudge_goal.<locals>.<genexpr>  sH       #
 #
!(Dd#
 #
 #
 #
 #
 #
r-   rJ   rK   i  )r   subgoals_blockresponsecurrent_time)r   r   r   system)rolecontentuser)modelmessagestemperaturer   r   
extra_bodyu@   goal judge: API call failed (%s) — falling through to continuer   zjudge error: Fr8   r   z goal judge: verdict=%s reason=%sx   ) r3   agent.auxiliary_clientr   r   rb   rc   rd   r   nowr   utc
astimezonestrftimerM   rN   (JUDGE_USER_PROMPT_WITH_SUBGOALS_TEMPLATEformatrz   _JUDGE_RESPONSE_SNIPPET_CHARSJUDGE_USER_PROMPT_TEMPLATEchatcompletionscreateJUDGE_SYSTEM_PROMPTr   infotyperQ   choicesmessager   r   )r   r   r   r#   r   r   rf   clientr   clean_subgoalsr   r   promptrespr.   r   r   parse_failedverdicts                      r+   
judge_goalr   s  s   6 ::<< .--   IHHA^^^^^^^^^ A A AEsKKK@@@@@@AA11,?? A A AGMMM@@@@@@A ~U~BB NM(.bMMMN<8<000;;==FFG]^^L 
 #
 #
,5nA,N,N,N#
 #
 #
 
 
 :@@4&&$^T::}.KLL%	 A 
 
 ,224&&}.KLL% 3 
 
G{&--!.ABBF33 -////119T . 

 

  G G GVX[\\\?499+=??FFFFFFFGl1o%-3    "7s!;!;D&,,ff*G
KK2GYvs=S=STTTFL((s^   7 
A'A""A'+A: :
B*B%%B*AH 
I4I	III2 2J Jc                      e Zd ZdZedd-dZed.d
            Zd/dZd/dZ	d0dZ
ddd1dZd2d3dZddd4dZd5dZd6dZd7d!Zd8d#Zd9d$Zd0d%Zdd&d:d*Zd;d,ZdS )<GoalManageruf  Per-session goal state + continuation decisions.

    The CLI and gateway each hold one ``GoalManager`` per live session.

    Methods:

    - ``set(goal)`` — start a new standing goal.
    - ``clear()`` — remove the active goal.
    - ``pause()`` / ``resume()`` — explicit user controls.
    - ``status()`` — printable one-liner.
    - ``evaluate_after_turn(last_response)`` — call the judge, update state,
      and return a decision dict the caller uses to drive the next turn.
    - ``next_continuation_prompt()`` — the canonical user-role message to
      feed back into ``run_conversation``.
    )default_max_turnsrW   r   r   r   c               r    || _         t          |pt                    | _        t	          |          | _        d S N)rW   r   r=   r   rn   _state)r*   rW   r   s      r+   __init__zGoalManager.__init__  s3    $!$%6%K:K!L!L+4Z+@+@r-   r$   rj   c                    | j         S r   )r   r)   s    r+   ro   zGoalManager.state  s
    {r-   r   c                4    | j         d uo| j         j        dk    S )Nr   r   r   r)   s    r+   	is_activezGoalManager.is_active  s    {$&I4;+=+IIr-   c                0    | j         d uo| j         j        dv S )N>   r   pausedr   r)   s    r+   has_goalzGoalManager.has_goal  s    {$&U4;+=AU+UUr-   c                   | j         }|	|j        dv rdS |j         d|j         d}|j        r4dt          |j                   dt          |j                  dk    rdnd	 nd	}|j        d
k    rd| | d|j         S |j        dk    r$|j        r
d|j         nd	}d| | | d|j         S |j        dk    rd| | d|j         S d|j         d| | d|j         S )N>   ru   z*No active goal. Set one with /goal <text>./z turnsz, z subgoalrJ   r6   r8   r   u   ⊙ Goal (active, ): r   u    — u   ⏸ Goal (paused, r   u   ✓ Goal done (zGoal ()r   r   r   r   r#   ry   r   r    )r*   r6   turnssubextras        r+   status_linezGoalManager.status_line  sB   K9L00??<55!+555UVU_gQ3qz??QQ3qz??a3G3GCCRQQQeg8x??s??qv???8x12H-AO---bEFFsFEFFafFFF8v<U<C<<AF<<<;;;E;3;;16;;;r-   N)r   r   r   Optional[int]r   c                  |pd                                 }|st          d          t          |dd|rt          |          n| j        t          j                    d          }|| _        t          | j        |           |S )Nr8   zgoal text is emptyr   r   r   )r   r   r   r   r   r   )	r3   
ValueErrorr   r   r   timer   rs   rW   )r*   r   r   ro   s       r+   setzGoalManager.set
  s    
!!## 	31222(1Mc)nnnt7My{{
 
 
 $/5)))r-   user-pausedr   c                    | j         sd S d| j         _        || j         _        t          | j        | j                    | j         S )Nr   )r   r   r    rs   rW   r*   r   s     r+   pausezGoalManager.pause  sA    { 	4%$*!$/4;///{r-   T)reset_budgetr   c                   | j         sd S d| j         _        d | j         _        |rd| j         _        t	          | j        | j                    | j         S )Nr   r   )r   r   r    r   rs   rW   )r*   r   s     r+   resumezGoalManager.resume"  sS    { 	4%$(! 	'%&DK"$/4;///{r-   rp   c                r    | j         d S d| j         _        t          | j        | j                    d | _         d S )Nru   )r   r   rs   rW   r)   s    r+   clearzGoalManager.clear,  s8    ;F&$/4;///r-   c                    | j         sd S d| j         _        d| j         _        || j         _        t	          | j        | j                    d S )Nr   )r   r   r   r   rs   rW   r   s     r+   	mark_donezGoalManager.mark_done3  sI    { 	F##) "($/4;/////r-   rH   c                   | j         |                                 st          d          |pd                                }|st	          d          | j         j                            |           t          | j        | j                    |S )zAppend a user-added criterion to the active goal. Requires
        ``has_goal()``; raises ``RuntimeError`` otherwise.

        Returns the cleaned text so the caller can show it back to the user.
        Nno active goalr8   zsubgoal text is empty)	r   r   RuntimeErrorr3   r   r#   appendrs   rW   )r*   rH   s     r+   add_subgoalzGoalManager.add_subgoal=  s     ;dmmoo/000
!!## 	64555##D)))$/4;///r-   index_1basedc                   | j         |                                 st          d          t          |          dz
  }|dk     s|t	          | j         j                  k    r*t          dt	          | j         j                   d          | j         j                            |          }t          | j	        | j                    |S )z<Remove a subgoal by 1-based index. Returns the removed text.Nr   rJ   r   zindex out of range (1..))
r   r   r   r   ry   r#   
IndexErrorpoprs   rW   )r*   r   idxremoveds       r+   remove_subgoalzGoalManager.remove_subgoalL  s    ;dmmoo/000,!#77cS!56666F#dk.B*C*CFFF   +&**3//$/4;///r-   c                    | j         |                                 st          d          t          | j         j                  }g | j         _        t          | j        | j                    |S )z.Wipe all subgoals. Returns the previous count.Nr   )r   r   r   ry   r#   rs   rW   )r*   prevs     r+   clear_subgoalszGoalManager.clear_subgoalsY  s[    ;dmmoo/0004;'((!$/4;///r-   c                b    | j         dS | j         j        sdS | j                                         S )z-Public helper for the /subgoal slash command.Nz(no active goal)u5   (no subgoals — use /subgoal <text> to add criteria))r   r#   rO   r)   s    r+   render_subgoalszGoalManager.render_subgoalsb  s8    ;%%{# 	KJJ{00222r-   )user_initiatedr   r  rZ   c               l   | j         }||j        dk    r|r|j        ndddddddS |xj        dz  c_        t          j                    |_        t          |j        ||j        pd	          \  }}}||_        ||_	        |r|xj
        dz  c_
        nd
|_
        |dk    r(d|_        t          | j        |           dddd|d| dS |j
        t          k    r>d|_        d|j
         d|_        t          | j        |           dddd|d|j
         ddS |j        |j        k    rNd|_        d|j         d|j         d|_        t          | j        |           dddd|d|j         d|j         ddS t          | j        |           dd|                                 d|d|j         d|j         d| dS )uv  Run the judge and update state. Return a decision dict.

        ``user_initiated`` distinguishes a real user prompt (True) from a
        continuation prompt we fed ourselves (False). Both increment
        ``turns_used`` because both consume model budget.

        Decision keys:
          - ``status``: current goal status after update
          - ``should_continue``: bool — caller should fire another turn
          - ``continuation_prompt``: str or None
          - ``verdict``: "done" | "continue" | "skipped" | "inactive"
          - ``reason``: str
          - ``message``: user-visible one-liner to print/send
        Nr   Finactiver   r8   )r   should_continuecontinuation_promptr   r   r   rJ   )r#   r   r   u   ✓ Goal achieved: r   z(judge model returned unparseable output z turns in a rowr   u%   ⏸ Goal paused — the judge model (z turns) isn't returning the required JSON verdict. Route the judge to a stricter model in ~/.hermes/config.yaml:
  auxiliary:
    goal_judge:
      provider: openrouter
      model: google/gemini-3-flash-preview
Then /goal resume to continue.zturn budget exhausted (r   r   u   ⏸ Goal paused — zD turns used. Use /goal resume to keep going, or /goal clear to stop.Tu   ↻ Continuing toward goal (r   )r   r   r   r   r   r   r   r#   r   r   r!   rs   rW   &DEFAULT_MAX_CONSECUTIVE_PARSE_FAILURESr    r   next_continuation_prompt)r*   r   r  ro   r   r   r   s          r+   evaluate_after_turnzGoalManager.evaluate_after_turnl  s   ( =ELH44*/9%,,T#('+%*   	A!Y[[(2J0F$)
 )
 )
% %"
  	1,,1,,,/0E,f!ELdou--- #('+! 999   +/UUU#ELl5;[lll  dou---"#('+% 5E<\ 5 5 5  $ u..#EL"aE<L"a"au"a"a"aEdou---"#('+% N5+; N Neo N N N
 
 
 	$/5)))##'#@#@#B#B!^u/?^^%/^^V\^^	
 	
 		
r-   r   c                   | j         r| j         j        dk    rd S | j         j        r=t                              | j         j        | j                                                   S t                              | j         j                  S )Nr   )r   r   )r   )r   r   r#   *CONTINUATION_PROMPT_WITH_SUBGOALS_TEMPLATEr   r   rO   CONTINUATION_PROMPT_TEMPLATEr)   s    r+   r  z$GoalManager.next_continuation_prompt  s    { 	dk0H<<4; 	=DD[%#{@@BB E    ,228H2IIIr-   )rW   r   r   r   )r$   rj   )r$   r   rP   )r   r   r   r   r$   r   )r   )r   r   r$   rj   )r   r   r$   rj   )r$   rp   )r   r   r$   rp   )rH   r   r$   r   )r   r   r$   r   r$   r   )r   r   r  r   r$   rZ   )r$   r   )rQ   rR   rS   rT   r=   r   propertyro   r   r   r   r   r   r   r   r   r   r   r   r  r	  r  r2   r-   r+   r   r     s          EV A A A A A A    XJ J J JV V V V< < < <" <@            .2         0 0 0 0         3 3 3 3  $	u
 u
 u
 u
 u
 u
nJ J J J J Jr-   r   uM  [Continuing toward this kanban task — judge says it is not done yet]
Reason: {reason}

Take the next concrete step toward completing the task. When the work is genuinely finished, call kanban_complete with a summary. If you are blocked and need human input, call kanban_block with a reason. Do not stop without calling one of them.z[The work looks complete, but the task is still open]
Reason: {reason}

If the task is genuinely done, call kanban_complete now with a short summary of what you did. If something still blocks completion, call kanban_block with the reason instead.r8   )r   first_responselogtask_id	goal_textr   r  c                   d4fd}t          |pt                    }|dk     rt          }|pd}	d}
d}	 	  |            }n,# t          $ r} |d
| d           d|
ddcY d}~S d}~ww xY w|dk    r |d|  d|
 d           d|
ddS |dk    r |d|  d|
 d           d|
ddS |dvr |d|  d|d           d|
d| dS t          ||	          \  }}} |d|
 d| d | d!t	          |d"                      |dk    rz|rL |d|  d#           	  |d$| d%           n&# t          $ r} |d&| d'           Y d}~nd}~ww xY wd(|
d)dS t
                              t	          |d*          +          }d	}n)t                              t	          |d*          +          }|
|k    re |d|  d,|
 d| d-           	  |d.|
 d| d/t	          |d0                      n&# t          $ r} |d&| d'           Y d}~nd}~ww xY wd(|
d1dS 	  ||          pd}	nA# t          $ r4} |d2| d           d|
d3t          |          j	         dcY d}~S d}~ww xY w|
dz  }
7)5u  Drive a kanban worker through a Ralph-style goal loop.

    The dispatcher spawns a goal-mode worker exactly like a normal worker
    (``hermes -p <profile> chat -q "work kanban task <id>"``). The worker's
    first turn has already run by the time this is called; ``first_response``
    is that turn's reply. From here we:

    1. Check whether the worker already terminated the task (called
       ``kanban_complete`` / ``kanban_block``). If so, stop — nothing to do.
    2. Otherwise judge the latest response against ``goal_text`` (the card's
       title + body). ``continue`` → feed a continuation prompt and run
       another turn IN THE SAME SESSION via ``run_turn``. ``done`` but the
       task is still open → one explicit "call kanban_complete" nudge.
    3. When the turn budget is exhausted and the worker still hasn't
       terminated the task, ``block_fn`` is invoked so the card lands in a
       sticky ``blocked`` state for human review (NOT a silent exit).

    This function performs NO SessionDB persistence — a worker process is
    ephemeral, so the turn budget lives in a local counter. It is fully
    decoupled from the CLI for testability: callers inject ``run_turn``
    (str -> str), ``task_status_fn`` (() -> str|None), and ``block_fn``
    (reason: str -> None).

    Returns a decision dict: ``{"outcome", "turns_used", "reason"}`` where
    outcome is one of ``"completed_by_worker"``, ``"blocked_budget"``,
    ``"blocked_by_worker"``, or ``"stopped"``.
    msgr   r$   rp   c                J    	  |            d S # t           $ r Y d S w xY wd S r   )rb   )r  r  s    r+   _logz"run_kanban_goal_loop.<locals>._log2  sG    ?C    ?s    
  rJ   r8   FTz'kanban goal loop: status check failed (z); stoppingstoppedzstatus check failed)outcomer   r   Nr   zkanban goal loop: task z completed by worker after z turn(s)completed_by_workerzworker completed the taskblockedz blocked by worker after blocked_by_workerzworker blocked the task)runningreadyz status=z
; stoppingzstatus=zkanban goal loop: turn r   z	 verdict=z reason=r   z0 judged done but worker won't finalize; blockingzfGoal-mode worker's output looked complete but it never called kanban_complete after a finalize nudge (z).z#kanban goal loop: block_fn failed (r   blocked_budgetzjudged done, never finalizedi  )r   z exhausted z turns; blockingz,Goal-mode worker exhausted its turn budget (z3) without completing the task. Last judge verdict: i,  zturn budget exhaustedz#kanban goal loop: run_turn failed (zrun_turn error: )r  r   r$   rp   )
r   r=   rb   r   rz   KANBAN_GOAL_FINALIZE_TEMPLATEr   !KANBAN_GOAL_CONTINUATION_TEMPLATEr   rQ   )r  r  run_turntask_status_fnblock_fnr   r  r  r  r   r   nudged_to_finalizer   rf   r   r   _parse_failedr   s          `          r+   run_kanban_goal_loopr&    s   N      I2!233I1}}%	"(bMJ<	e#^%%FF 	e 	e 	eDK3KKKLLL(
Ncdddddddd	e VDc7cczcccddd4JZuvvvYDa7aaZaaabbb2*Xqrrr---DP7PPFPPPQQQ(
N`X^N`N`aaa *4I})M)M&qzqqIqqqqYbciknYoYoqqrrrf! y hwhhhiiiGHUJPU U U    ! G G GDEsEEEFFFFFFFFG#3:Ywxxx2996SVAWAW9XXF!%6==YvWZE[E[=\\F ""Dg7ggzggIggghhhCD"D D%.D D+4VS+A+AD D   
  C C CA3AAABBBBBBBBC/zUlmmm	w$HV,,2MM 	w 	w 	wDGsGGGHHH(
NuaefiajajasNuNuvvvvvvvv	w 	a
y<sk   
A   
A)
A$A)$A)D( (
E2EE"G( (
H2HHH# #
I!-)II!I!)r   r   r  r  r   r   r   r  r=   rn   rs   rv   r   r&  )rW   r   r$   r   )r$   r\   )rW   r   r$   rj   )rW   r   ro   r   r$   rp   )rW   r   r$   rp   )rH   r   rw   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$   rZ   )4rT   
__future__r   r'   loggingrer   dataclassesr   r   r   r   r   typingr	   r
   r   r   r   	getLoggerrQ   rc   r=   DEFAULT_JUDGE_TIMEOUTr   r   r  r  r  r   r   r   r   rY   r[   rU   ri   rn   rs   rv   rz   compileDOTALLr   r   r   r   r   r   r  r&  __all__r2   r-   r+   <module>r1     s    : # " " " " "   				  0 0 0 0 0 0 0 0 0 0 ' ' ' ' ' ' ' ' 3 3 3 3 3 3 3 3 3 3 3 3 3 3		8	$	$       $  *+ &P  +I " < ), 3\ 3\ 3\ 3\ 3\ 3\ 3\ 3\v        	       <   *
> 
> 
> 
>! ! ! !, , , , "*Z33$ $ $ $0. . . .j +$(\) \) \) \) \) \)HTJ TJ TJ TJ TJ TJ TJ TJ@	( ",   's s s s s sl  r-   