
    9j.             	         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dZ dZ!dZ"dZ#dZ$ddddddZ%i ddddddddddd dd!dd"dd#dd$d$d%d$d&d$d'd$d(d$d)d)d*d)d+d)d)d)d,d,d,d,d,d-Z&e G d. d/                      Z'dtd4Z(e G d5 d6                      Z)dud8Z*i Z+d9e,d:<   dvd<Z-dwd>Z.dxdAZ/dydBZ0dCdDdzdIZ1d{dLZ2d|dNZ3d}dOZ4 ej5        dPej6                  Z7d~dQZ8ddTZ9ddWZ:eddddXdd`Z;ddddZ<ededdgZ=ddiZ> G dj dk          Z?dlZ@dmZAedCddnddrZBg dsZCdS )un  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        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}

Completion contract:
{contract_block}

Continue working toward the outcome above. Take the next concrete step. Stay within the stated boundaries and do not violate the constraints. Before claiming the goal is done, satisfy the Verification criterion and show the concrete evidence (command output, file contents, test result). If you hit the stated stop condition or are otherwise blocked and need user input, 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, the agent's most recent response, and — when present — a list of background processes the agent has running. Decide one of three verdicts.

DONE — the goal is fully satisfied:
- 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).

WAIT — the goal is NOT done, but the next step is to wait for async work to finish rather than act again. Choose this ONLY when the agent's progress is genuinely gated on something running on its own:
- A background process listed below is still running AND the response shows the agent is waiting on its result (e.g. a CI poller, build, test run, deploy). If the process has a session id, return it in ``wait_on_session`` — that releases when the process exits OR its watch_patterns trigger fires (use this for a long-lived watcher that signals mid-run and may never exit). Otherwise return its pid in ``wait_on_pid`` (releases on exit only).
- The agent says it is rate-limited / backing off / must wait a fixed period — return seconds in ``wait_for_seconds``.
Picking WAIT parks the loop without burning a turn; it resumes automatically when the pid exits or the time elapses. Do NOT pick WAIT just because work remains — only when re-poking now would be pure busy-work because the agent can't progress until the async thing finishes.

CONTINUE — not done, and there is a concrete next step the agent can take right now. This is the default when in doubt.

Reply ONLY with a single JSON object on one line. Shapes:
{"verdict": "done", "reason": "<one sentence>"}
{"verdict": "continue", "reason": "<one sentence>"}
{"verdict": "wait", "wait_on_session": "<id>", "reason": "<one sentence>"}
{"verdict": "wait", "wait_on_pid": <int>, "reason": "<one sentence>"}
{"verdict": "wait", "wait_for_seconds": <int>, "reason": "<one sentence>"}
The legacy shape {"done": <true|false>, "reason": "..."} is still accepted (true=done, false=continue).znBackground processes the agent currently has running (it may be waiting on one of these):
{background_lines}

u   Goal:
{goal}

Agent's most recent response:
{response}

{background_block}Current time: {current_time}

Is the goal satisfied — done, continue, or wait?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}

{background_block}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 (or WAIT if blocked on a listed background process).

Is the goal AND every additional criterion satisfied?u=  Goal:
{goal}

Completion contract (the authoritative definition of done):
{contract_block}

Agent's most recent response:
{response}

{background_block}Current time: {current_time}

Decision rules:
- The goal is DONE only when the Verification criterion is satisfied AND the response shows concrete evidence of it (a command result, file contents excerpt, test/benchmark output) — not a claim like 'done' or 'all tests pass' without evidence.
- If any stated Constraint was violated, the goal is NOT done — CONTINUE.
- If the response shows the agent is waiting on a listed background process to satisfy the Verification criterion (e.g. CI is the verification and it's still running), return WAIT on that process instead of re-poking — re-poking now would be pure busy-work.
- If the response explains the work is blocked / unachievable / needs user input (e.g. the stated Stop condition was hit), treat it as DONE with the reason describing the block.
- Otherwise the goal is NOT done — CONTINUE.

Is the goal satisfied per its completion contract — done, continue, or wait?a  You turn a user's plain-language objective into a structured completion contract for an autonomous coding agent. The contract has five fields:
- outcome: the single end state that must be true when done
- verification: the specific test / command / artifact that PROVES the outcome (must be concrete and checkable)
- constraints: what must NOT change or regress
- boundaries: which files, dirs, tools, or systems are in scope
- stop_when: the condition under which the agent should stop and ask for human input instead of pushing on

Infer sensible, specific values from the objective and any project context implied by it. Prefer concrete verification (a named test command, a build, a benchmark) over vague phrases. Keep each field to one or two sentences. If a field genuinely cannot be inferred, use an empty string for it.

Reply ONLY with a single JSON object on one line:
{"outcome": "...", "verification": "...", "constraints": "...", "boundaries": "...", "stop_when": "..."})outcomeverificationconstraints
boundaries	stop_whenOutcomeVerificationConstraints
BoundarieszStop when blockedr   goaldonez	done whenr   verifyzverified byevidenceproofr   
constraintpreservezmust notzdo not changer   boundaryscoper   )allowedfilesz	stop whenr   blockedzstop if blockedzgive up whenc                      e Zd ZU dZ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dZ
ddZedd            ZddZdS )GoalContractu  Optional structured completion contract for a goal.

    Each field is free-form prose the user (or :func:`draft_contract`)
    supplies. Empty fields are omitted everywhere — a goal with no contract
    behaves exactly like the original free-form goal. The contract is woven
    into both the continuation prompt (so the agent targets the verification
    surface and respects constraints) and the judge prompt (so "done" is
    decided against evidence, not vibes).
     strr   r   r   r   r   returnboolc                F     t           fdt          D                        S )Nc              3  \   K   | ]&}t          |                                          V  'd S N)getattrstrip.0fselfs     6/home/rurouni/.hermes/hermes-agent/hermes_cli/goals.py	<genexpr>z(GoalContract.is_empty.<locals>.<genexpr>8  s9      JJAwtQ''--//JJJJJJ    )any_CONTRACT_FIELDSr4   s   `r5   is_emptyzGoalContract.is_empty7  s*    JJJJ9IJJJJJJJr7   Dict[str, str]c                *      fdt           D             S )Nc                2    i | ]}|t          |          S  )r/   r1   s     r5   
<dictcomp>z(GoalContract.to_dict.<locals>.<dictcomp>;  s%    >>>74##>>>r7   )r9   r:   s   `r5   to_dictzGoalContract.to_dict:  s    >>>>-=>>>>r7   dataOptional[Dict[str, Any]]'GoalContract'c                t    t          t                    s
 |             S  | di fdt          D             S )Nc                ~    i | ]9}|t                              |          pd                                           :S r(   )r)   getr0   )r2   r3   rB   s     r5   r@   z*GoalContract.from_dict.<locals>.<dictcomp>A  s>    RRRAaTXXa[[.B//5577RRRr7   r?   )
isinstancedictr9   )clsrB   s    `r5   	from_dictzGoalContract.from_dict=  sL    $%% 	355LsSSRRRRAQRRRSSSr7   c                    g }t           D ]L}t          | |                                          }|r&|                    dt          |          d|            Md                    |          S )u   Render non-empty contract fields as a labelled block. Empty
        contract → empty string (callers skip the section entirely).- : 
)r9   r/   r0   append_CONTRACT_LABELSjoin)r4   linesr3   vals       r5   render_blockzGoalContract.render_blockC  sz     ! 	@ 	@A$""((**C @>"21"5>>>>???yyr7   Nr*   r+   )r*   r<   )rB   rC   r*   rD   r*   r)   )__name__
__module____qualname____doc__r   __annotations__r   r   r   r   r;   rA   classmethodrL   rV   r?   r7   r5   r'   r'   %  s           GLKJIK K K K? ? ? ? T T T [T
           r7   r'   textr)   r*   Tuple[str, GoalContract]c                   | sdt                      fS g }d t          D             }|                                 D ]}|                                }|sd}d|v r|                    d          \  }}}t
                              |                                                                          }	|	C|                                r/||	                             |                                           d}|s|                    |           d	                    |                                          }
t          d	i d |
                                D             }|
|fS )
uL  Split user-typed goal text into a headline + structured contract.

    Supports inline ``field: value`` lines so power users can type a full
    contract in one shot, e.g.::

        Migrate auth to JWT
        verify: the auth test suite passes
        constraints: keep the public /login response shape unchanged
        boundaries: only touch services/auth and its tests
        stop when: a schema change needs product sign-off

    The first non-field line(s) become the goal headline; recognized
    ``field:`` lines populate the contract. Lines for the same field are
    joined. Unrecognized prefixes stay part of the headline, so a plain
    free-form goal with an incidental colon (``Fix bug: the parser``)
    is NOT mangled — only lines whose prefix matches a known alias are
    pulled out. Returns ``(headline, contract)``.
    r(   c                    i | ]}|g S r?   r?   )r2   r3   s     r5   r@   z"parse_contract.<locals>.<dictcomp>e  s    #D#D#DaAr#D#D#Dr7   F:NT c                d    i | ]-\  }}|d                      |                                          .S )rd   )rS   r0   )r2   r3   vs      r5   r@   z"parse_contract.<locals>.<dictcomp>w  s4    
=
=
=da1chhqkk!!
=
=
=r7   r?   )r'   r9   
splitlinesr0   	partition_CONTRACT_ALIASESrH   lowerrQ   rS   items)r_   headline_partsfieldsraw_linelinematchedprefix_valuekeyheadlinecontracts               r5   parse_contractrw   N  sc   &  "<>>!! "N#D#D3C#D#D#DFOO%% ( (~~ 	$;;#~~c22FAu#''(<(<(>(>??C5;;==s""5;;==111 	(!!$'''xx''--//H  
=
=fllnn
=
=
= H Xr7   c                  V   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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'd&ZdS )*	GoalStatez+Serializable goal state stored per session.r)   r   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Optional[int]waiting_on_pidwaiting_on_sessionwaiting_untilwaiting_reasonwaiting_sincer'   rv   r*   c                L    t          |           }t          j        |d          S )NF)ensure_ascii)r   jsondumps)r4   rB   s     r5   to_jsonzGoalState.to_json  s"    d||z$U3333r7   raw'GoalState'c                   t          j        |          }|                    d          pg }g }t          |t                    rd |D             } | di d|                    dd          d|                    dd          dt          |                    dd          pd          d	t          |                    d	t                    pt                    d
t          |                    d
d          pd          dt          |                    dd          pd          d|                    d          d|                    d          d|                    d          dt          |                    dd          pd          d|d|                    d          rt          |d                   nd d|                    d          rt          |d                   nd dt          |                    dd          pd          d|                    d          dt          |                    dd          pd          dt          
                    |                    d                    S )Nr   c                    g | ]D}t          |                                          #t          |                                          ES r?   )r)   r0   r2   ss     r5   
<listcomp>z'GoalState.from_json.<locals>.<listcomp>  s9    OOO1AOAOOOr7   r   r(   r{   rz   r}   r   r~   r   r   r   r   r   r   r   r   r   r   r   r   rv   r?   )r   loadsrH   rI   listr|   DEFAULT_MAX_TURNSr   r)   r'   rL   )rK   r   rB   raw_subgoalsr   s        r5   	from_jsonzGoalState.from_json  s   z#xx
++1r lD)) 	POOOOOHs 
 
 
&"%%%
88Hh///
 488L!449:::
 $((;0ABBWFWXXX	

 TXXlC88?C@@@
 txx<<CDDD
 .111
 ///
 ((?333
 (+4884PRS+T+T+YXY'Z'Z'Z
 X
 <@88DT;U;U_C%5 6777[_
 DH88L`CaCa kD)=$> ? ? ?gk
  # > > E#FFF
  88$4555
   # > > E#FFF!
" "++DHHZ,@,@AAA#
 	
r7   r+   c                H    | j         d uo| j                                          S r.   )rv   r;   r:   s    r5   has_contractzGoalState.has_contract  s%    }D(I1G1G1I1I-IIr7   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.r(   rP   c              3  ,   K   | ]\  }}d | d| V  dS rN   z. Nr?   r2   ir_   s      r5   r6   z2GoalState.render_subgoals_block.<locals>.<genexpr>  s7      [[ga)a))4))[[[[[[r7      start)r   rS   	enumerater:   s    r5   render_subgoals_blockzGoalState.render_subgoals_block  sD     } 	2yy[[4=XY9Z9Z9Z[[[[[[r7   rX   )r   r)   r*   r   rW   )rY   rZ   r[   r\   r]   r{   r}   r   r~   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r'   rv   r   r^   r   r   r   r?   r7   r5   ry   ry     s        55IIIFJ&I&&&&JL"&L&&&&!%K%%%%#'M''''&'''''  %555H5555( %)N(((((,,,,,M$(N((((M #U<@@@H@@@@4 4 4 4
 
 
 
 [
8J J J J
\ \ \ \ \ \r7   ry   
session_idc                    d|  S )Nzgoal:r?   )r   s    r5   	_meta_keyr     s    :r7   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   rH   )r   r   homeexccacheddbs         r5   _get_session_dbr     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)	r   get_metar   r   r   r   ry   r   warning)r   r   r   r   s       r5   	load_goalr     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)r   set_metar   r   r   r   r   )r   r   r   r   s       r5   	save_goalr   #  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)r   r{   r   )r   r   s     r5   
clear_goalr   0  s6    j!!E}ELj%     r7   r(   reasonold_session_idnew_session_idr   r+   c               t   | r|r| |k    rdS 	 t          |           }|t          |dd          dk    rdS t          |          dS t          ||           t          |            t                              d| ||pd           dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)	u  Carry a persistent /goal from a parent session to its continuation.

    Context compression rotates ``session_id`` to a fresh child session,
    but ``load_goal`` does a flat ``goal:<session_id>`` lookup with no
    parent-lineage walk — so an active goal silently dies at the
    compaction boundary (#33618). Copy the goal onto the new session and
    archive the old row as ``cleared`` so exactly one active goal row
    exists per logical conversation (avoids the "two active goals"
    hazard of a pure copy).

    Returns True when a goal was migrated, False when there was nothing
    to migrate or the DB was unavailable. Best-effort and never raises —
    a failure here must not block compression.
    FNr{   r   z(GoalManager: migrated goal %s -> %s (%s)rotationTz&GoalManager: goal migration failed: %s)r   r/   r   r   r   r   r   )r   r   r   r   r   s        r5   migrate_goal_to_sessionr   9  s       >^3S3Su.))=GE8T::iGG5 ^$$05.%(((>"""6NF,@j	
 	
 	
 t   =sCCCuuuuus"   &B B >B 
B7B22B7limitr|   c                N    | sdS t          |           |k    r| S | d |         dz   S )Nr(   u   … [truncated])len)r_   r   s     r5   	_truncater   d  s9     r
4yyE<+++r7   pidc                   | r| dk    rdS 	 ddl m} t           |t          |                               S # t          $ r Y nw xY w	 ddl}t          |                    t          |                               S # t          $ r Y dS w xY w)u<  Return True if a process with ``pid`` is currently alive.

    Delegates to ``gateway.status._pid_exists`` — the canonical,
    cross-platform, footgun-safe liveness check (psutil with a ctypes /
    POSIX fallback). Critically this avoids ``os.kill(pid, 0)``, which on
    Windows is NOT a no-op: it routes to ``CTRL_C_EVENT`` and hard-kills the
    target's console process group (bpo-14484). Any error resolves to False
    (treat unknown as dead) so a stale barrier never wedges the loop — the
    worst case is the goal resumes one turn early, which is safe.
    r   F)_pid_existsN)gateway.statusr   r+   r|   r   psutil
pid_exists)r   r   r   s      r5   
_pid_aliver   l  s      #((u......KKC))***   F%%c#hh//000   uus!   *7 
AA2A; ;
B	B	c                ~    | sdS 	 ddl m} t          |                    |                     S # t          $ r Y dS w xY w)u]  Whether a goal parked on a process_registry session should stay parked.

    Delegates to ``process_registry.is_session_waiting`` — True while the
    session is running and (if it has watch_patterns) its trigger hasn't fired.
    Fail-safe: any import/registry error yields False (don't wait) so a stale
    barrier can never wedge the loop.
    Fr   process_registry)tools.process_registryr   r+   is_session_waitingr   )r   r   s     r5   _session_waitingr     sg      u;;;;;;$77
CCDDD   uus   '. 
<<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   rH   DEFAULT_JUDGE_MAX_TOKENSr|   r   )r   cfgrs   s      r5   _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[str, str, bool, Optional[Dict[str, Any]]]c                   | 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dfS t                              d          pd                                           pd}                    d          }t          |t                    r'|                                                                 }nh                    d          }t          |t                    r)|                                                                 dv }nt          |          }|rdnd	}|dvrd	}|dk    r||ddfS d%fd}	                    d          p)                    d          p                    d          }
t          |
t                    r.|
                                 rd|dd|
                                 ifS  |	ddd           }|d|dd|ifS  |	d!d"d#          }|d|dd"|ifS d	| d$ddfS )&a  Parse the judge's reply. Fail-open on unusable output.

    Returns ``(verdict, reason, parse_failed, wait_directive)`` where:
      - ``verdict`` is ``"done"``, ``"continue"``, or ``"wait"``.
      - ``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 it to auto-pause after N consecutive
        parse failures so a weak judge model doesn't silently burn the budget.
      - ``wait_directive`` is set only for ``verdict == "wait"``: a dict with
        ``{"pid": int}`` or ``{"seconds": int}`` (whichever the judge supplied).
        ``None`` otherwise. If a wait verdict carries neither a usable pid nor
        seconds, it is downgraded to ``continue`` (can't park on nothing).

    Accepts both the new ``{"verdict": ...}`` shape and the legacy
    ``{"done": <bool>}`` shape.
    )continuezjudge returned empty responseTN````rP   r   Nr   r   zjudge reply was not JSON:    Tr   r(   zno reason providedverdictr   >   1yesr   true>   r   waitr   r   Fkeysr)   r*   r   c                     | D ]K}                     |          }|	 t          |          }|dk    r|c S 5# t          t          f$ r Y Hw xY wd S )Nr   )rH   r|   	TypeError
ValueError)r   krf   ivrB   s       r5   
_first_intz)_parse_judge_response.<locals>._first_int  sz     		 		AAyVV66III z*   ts   9AAwait_on_sessionr   wait_sessionwait_on_pidr   wait_pidwait_for_secondssecondswait_secondsu,    (wait verdict had no target — continuing))r   r)   r*   r   )r0   
startswithfindr   r   r   _JSON_OBJECT_REsearchgrouprI   rJ   r   r)   rH   rj   r+   )r   r_   nlmatchr   verdict_rawr   done_valr   r   sessr   r  rB   s                @r5   _parse_judge_responser    sE   "  GFF99;;D u !zz#YYt__88Q=D &*D	z$   &&t,, 	z%++a..11    dD!! \O	#s8K8KOOQUW[[[(##)r**0022J6JF ((9%%K+s## 1##%%++--88F##h$$ 	">>##))++/KKDD>>D 0&&j222&t++      88%&&\$((<*@*@\DHH^D\D\D$ C Cvu|TZZ\\&BBB
*]E:
6
6C
vuucl22j+YGGGvuy'&:::&NNNPUW[[[s6   /B &C(+'CC(C"C(!C""C('C(background_processesOptional[List[Dict[str, Any]]]c                   | sdS g }| D ]}t          |t                    s|                    d          dk    r3|                    d          }|sKt          t	          |                    d          pd                              dd                                          d          }|                    d	          }t          t	          |                    d
          pd                              dd                                          d          }|                    d          }d| }|r|d| z  }|d| z  }|	|d| dz  }|                    d          }	|	r$|                    d          rdnd}
|d|	 |
 z  }n|                    d          r|dz  }|r|d| z  }|                    |           |sdS t          	                    d
                    |                    S )a  Render the live background-process list for the judge prompt.

    Each entry is a ``process_registry.list_sessions()`` dict. Only RUNNING
    processes are worth showing (an exited one is nothing to wait on). Returns
    an empty string when there's nothing running, so the judge prompt is
    byte-identical to the no-background case (no behavior change for the
    common path).
    r(   r{   exitedr   commandrP   rd   x   uptime_secondsoutput_previewr   z- pid z / session rO   Nz
 (running zs)watch_patterns	watch_hitz [already matched]z | watch_patterns=notify_on_completez | notify_on_completez | recent output: )background_lines)rI   rJ   rH   r   r)   replacer0   rQ   JUDGE_BACKGROUND_BLOCK_TEMPLATEformatrS   )r  rT   pr   cmduptimetailsidro   wpshits              r5   _render_background_blockr)    s>      rE!  !T"" 	55??h&&eeEll 	AEE),,233;;D#FFLLNNPSTT'((QUU#344:;;CCD#NNTTVVX[\\eeL!!~~ 	('#'''D
S

+++++D ee$%% 	,*+%%*<*<D&&"C33c333DDUU'(( 	,++D 	0////DT r*11499UCSCS1TTTr7   )timeoutr   r  rv   last_responser*  r   r   Optional[List[str]]rv   Optional[GoalContract]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          |          }t          j	        t          j                                                                      d          }||                                s|                                }|r5d                    d t#          |d          D                       }| d| }t$                              t)          | d          t)          |d          t)          |t*                    ||          }n|r}d                    d t#          |d          D                       }t,                              t)          | d          t)          |d          t)          |t*                    ||          }n?t.                              t)          | d          t)          |t*                    ||          }	 |	j        j                            |
dt6          dd|dgdt9                      | |            pd          }nM# t          $ r@}t
                              d|           ddt=          |          j         d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tG          |          \  }}}}t
                              d!|t)          |d"          |rd#| nd            ||||fS )$u  Ask the auxiliary model whether the goal is satisfied.

    Returns ``(verdict, reason, parse_failed, wait_directive)`` where verdict
    is ``"done"``, ``"continue"``, ``"wait"``, or ``"skipped"`` (when the
    judge couldn't be reached). ``wait_directive`` is set only for ``"wait"``
    (``{"pid": int}`` or ``{"seconds": int}``); ``None`` otherwise.

    ``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``) factored into the verdict. ``background_processes`` is the
    live ``process_registry.list_sessions()`` snapshot; when the agent is
    waiting on one (a CI poller, build, etc.) the judge can return a ``wait``
    verdict naming its pid, parking the loop instead of re-poking.
    ``contract`` is an optional structured completion contract; when present
    the judge decides DONE strictly against its Verification criterion and
    refuses completion when a Constraint was violated. All three are additive
    — a contract, subgoals, and a background-process list can coexist in one
    judge prompt; when none are set, behavior is identical to the original
    free-form judge.

    This is deliberately fail-open: any error returns ``("continue", ..., False, None)``
    so a broken judge doesn't wedge progress — the turn budget and the
    consecutive-parse-failures auto-pause are the backstops.
    )skippedz
empty goalFN)r   z$empty response (nothing to evaluate)FNr   get_auxiliary_extra_bodyget_text_auxiliary_clientz.goal judge: auxiliary client import failed: %sN)r   zauxiliary client unavailableFNr   z0goal judge: get_text_auxiliary_client failed: %s)r   zno auxiliary client configuredFNc                b    g | ],}||                                 |                                 -S r?   )r0   r   s     r5   r   zjudge_goal.<locals>.<listcomp>  s2    MMMAQM17799MaggiiMMMr7   )tzz%Y-%m-%d %H:%M:%S %ZrP   c              3  ,   K   | ]\  }}d | d| V  dS z- Extra criterion rO   Nr?   r   s      r5   r6   zjudge_goal.<locals>.<genexpr>  sJ        At 1Q00$00     r7   r   r   i  i	  )r   contract_blockresponsebackground_blockcurrent_timec              3  ,   K   | ]\  }}d | d| V  dS r   r?   r   s      r5   r6   zjudge_goal.<locals>.<genexpr>  sH       #
 #
!(Dd#
 #
 #
 #
 #
 #
r7   )r   subgoals_blockr8  r9  r:  )r   r8  r9  r:  systemrolecontentusermodelmessagestemperaturer   r*  
extra_bodyu@   goal judge: API call failed (%s) — falling through to continuer   zjudge error: Fr(   z"goal judge: verdict=%s reason=%s%sr  z wait=)$r0   agent.auxiliary_clientr1  r2  r   r   r   r)  r   nowr   utc
astimezonestrftimer;   rV   rS   r   (JUDGE_USER_PROMPT_WITH_CONTRACT_TEMPLATEr!  r   _JUDGE_RESPONSE_SNIPPET_CHARS(JUDGE_USER_PROMPT_WITH_SUBGOALS_TEMPLATEJUDGE_USER_PROMPT_TEMPLATEchatcompletionscreateJUDGE_SYSTEM_PROMPTr   infotyperY   choicesmessager@  r  )r   r+  r*  r   r  rv   r1  r2  r   clientrC  clean_subgoalsr9  r:  r7  extrapromptr<  respr   r   r   parse_failedwait_directives                           r5   
judge_goalr_  D  s   L ::<< 433   ONNG^^^^^^^^^ G G GEsKKKFFFFFFGG11,?? G G GGMMMFFFFFFG ~U~HH NM(.bMMMN/0DEE<8<000;;==FFG]^^LH$5$5$7$7!..00 	:II  (qAAA    E !/99%99N9@@4&&$^T::}.KLL-% A 
 
 
 
 #
 #
,5nA,N,N,N#
 #
 #
 
 
 :@@4&&$^T::}.KLL-% A 
 
 ,224&&}.KLL-%	 3 
 
M{&--!.ABBF33 -////119T . 

 

  M M MVX[\\\?499+=??LLLLLLLMl1o%-3    5J#4N4N1GV\>
KK,63''%3;!!!!  
 FL.88s^   7 
A'A""A'+A: :
B*B%%B*AK 
L5LLLL4 4MMtask_idr   List[Dict[str, Any]]c                    	 ddl m} |                    |           pg }n4# t          $ r'}t                              d|           g cY d}~S d}~ww xY wd |D             S )u  Return the live background-process snapshot for the goal judge.

    Thin, fail-safe wrapper over ``process_registry.list_sessions(task_id)``.
    Returns only RUNNING processes (an exited one is nothing to wait on) and
    never raises — any import/registry failure yields ``[]`` so the goal loop
    degrades to its pre-wait-barrier behavior (judge just won't see processes).
    The drivers (CLI + gateway) call this and pass the result into
    ``GoalManager.evaluate_after_turn(background_processes=...)``.
    r   r   )r`  z&gather_background_processes failed: %sNc                n    g | ]2}t          |t                    |                    d           dk    0|3S )r{   r  )rI   rJ   rH   r   s     r5   r   z/gather_background_processes.<locals>.<listcomp>  s;    WWW!:a#6#6W155??h;V;VA;V;V;Vr7   )r   r   list_sessionsr   r   r   )r`  r   sessionsr   s       r5   gather_background_processesrf    s    ;;;;;;#11'1BBHb   =sCCC						 XWxWWWWs   ! 
AAAA)r*  	objectivec          
        | pd                                 } | 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 	 |j        j        	                    |dt          d	d
dt          | d           d	gdt                      | |            pd          }n3# t          $ r&}t
                              d|           Y d}~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#          |          }	t%          |	t&                    s+t
                              dt          |d                     dS t(                              |	          }
|
                                rdn|
S )u  Expand a plain-language objective into a structured completion contract.

    Uses the ``goal_judge`` auxiliary task (main-model-first, cache-safe — it
    is a side LLM call, not a conversation turn). Returns a populated
    :class:`GoalContract` on success, or ``None`` when the auxiliary client is
    unavailable or the model's reply can't be parsed. Callers fall back to a
    bare free-form goal in that case, so a missing/weak aux model never blocks
    setting a goal.
    r(   Nr   r0  z.goal draft: auxiliary client import failed: %sr   z0goal draft: get_text_auxiliary_client failed: %sr=  r>  rA  zObjective:
r   rB  z goal draft: API call failed (%s)z"goal draft: reply was not JSON: %rr   )r0   rG  r1  r2  r   r   r   rP  rQ  rR  DRAFT_CONTRACT_SYSTEM_PROMPTr   r   rT  rV  rW  r@  _extract_json_objectrI   rJ   r'   rL   r;   )rg  r*  r1  r2  r   rX  rC  r\  r   rB   rv   s              r5   draft_contractrk    se    b''))I t^^^^^^^^^   EsKKKttttt11,??   GMMMttttt ~U~t{&--!.JKK,W9YPT;U;U,W,WXX -////119T . 

 

    6<<<tttttl1o%-3     $$DdD!! 99S#;N;NOOOt%%d++H$$&&444H4sW   % 
AAAA( (
B2BB"AC< <
D,D''D,0E
 
EErC   c                   | sdS |                                  }|                    d          r=|                     d          }|                    d          }|dk    r||dz   d         }	 t          j        |          }nj# t
          $ r] t                              |          }|sY dS 	 t          j        |                    d                    }n# t
          $ r Y Y dS w xY wY nw xY wt          |t                    r|ndS )zBest-effort: pull the first JSON object out of a model reply.

    Shares the fence-stripping + first-object fallback logic used by the
    judge parser, but returns the dict (or None) rather than a verdict.
    Nr   r   rP   r   r   r   )r0   r  r	  r   r   r   r
  r  r  rI   rJ   )r   r_   r  rB   r  s        r5   rj  rj    s#     t99;;Du !zz#YYt__88Q=D	z$   &&t,, 	44	:ekk!nn--DD 	 	 	444	 D dD))344t3s6   ,B &C(+'CC(
C"C(!C""C('C(c                     e Zd ZdZedd=dZed>d
            Zd?dZd?dZ	d?dZ
d@dZddddAdZdBdZdCdDdZdddEd!ZdFd#ZdGd$ZdHd&ZdId(ZdJd)Zd@d*ZdKdLd-ZdKdMd.ZdKdNd0Zd?d1Zd?d2Zddd3dOd9ZdPd;Zd@d<ZdS )Q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_turnsr   r)   ro  r|   c               r    || _         t          |pt                    | _        t	          |          | _        d S r.   )r   r|   r   ro  r   _state)r4   r   ro  s      r5   __init__zGoalManager.__init__F  s3    $!$%6%K:K!L!L+4Z+@+@r7   r*   r   c                    | j         S r.   )rq  r:   s    r5   r   zGoalManager.stateM  s
    {r7   r+   c                4    | j         d uo| j         j        dk    S )Nrz   rq  r{   r:   s    r5   	is_activezGoalManager.is_activeQ  s    {$&I4;+=+IIr7   c                0    | j         d uo| j         j        dv S )N>   rz   pausedru  r:   s    r5   has_goalzGoalManager.has_goalT  s    {$&U4;+=AU+UUr7   c                F    | j         d uo| j                                         S r.   )rq  r   r:   s    r5   r   zGoalManager.has_contractW  s"    {$&E4;+C+C+E+EEr7   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	}|                                 rd
nd	}| | | }|j        dk    r|j        r5t          |j                  r!|j	        p	d|j         }d| d| d|j
         S |j        r5t          |j                  r!|j	        p	d|j         }d| d| d|j
         S |j        rct          j                    |j        k     rGt          |j        t          j                    z
            }|j	        p| d}d| d| d| d|j
         S 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>   r   z*No active goal. Set one with /goal <text>./z turnsz, z subgoalr   r   r(   z
, contractrz   session u   ⏳ Goal (parked on ): pid u   ⏳ Goal (parked u   s — u   ⊙ Goal (active, rx  u    — u   ⏸ Goal (paused, r   u   ✓ Goal done (zGoal ()rq  r{   r}   r~   r   r   r   r   r   r   r   r   r   r   timer|   r   )	r4   r   turnssubconmetawr	remainingrZ  s	            r5   status_linezGoalManager.status_lineZ  s   K9L00??<55!+555UVU_gQ3qz??QQ3qz??a3G3GCCRQQQeg"//119llr##c##8x# F(89M(N(N F%J)JA4H)J)JEbEEDEEQVEEE FJq/?$@$@ F%B)B0@)B)BEbEEDEEQVEEE T49;;#@#@$)++ =>>	%8IS9SSBSS$SS16SSS9999998x12H-AO---bE@@e@@@@@8v6T66af666555D55QV555r7   N)r~   rv   r   r~   r   rv   r-  ry   c          
     &   |pd                                 }|st          d          t          |dd|rt          |          n| j        t          j                    d||nt                                }|| _        t          | j	        |           |S )Nr(   zgoal text is emptyrz   r   r   )r   r{   r}   r~   r   r   rv   )
r0   r   ry   r|   ro  r  r'   rq  r   r   )r4   r   r~   rv   r   s        r5   setzGoalManager.setw  s    
!!## 	31222(1Mc)nnnt7My{{!)!5XX<>>
 
 
 $/5)))r7   r'   c                    | j         dS |pt                      | j         _        t          | j        | j                    | j         S )zAttach or replace the completion contract on the active goal.

        Returns the updated state, or None when there is no goal to attach to.
        N)rq  r'   rv   r   r   )r4   rv   s     r5   set_contractzGoalManager.set_contract  s@    
 ;4'9<>>$/4;///{r7   user-pausedr   c                    | j         sd S d| j         _        || j         _        d | j         _        d | j         _        d| j         _        d | j         _        d| j         _        t          | j	        | j                    | j         S )Nrx  r   )
rq  r{   r   r   r   r   r   r   r   r   r4   r   s     r5   pausezGoalManager.pause  ss    { 	4%$*!%)")-&$'!%)"$'!$/4;///{r7   T)reset_budgetr  c                  | j         sd S d| j         _        d | j         _        d | j         _        d | j         _        d| j         _        d | j         _        d| j         _        |rd| j         _        t          | j
        | j                    | j         S )Nrz   r   r   )rq  r{   r   r   r   r   r   r   r}   r   r   )r4   r  s     r5   resumezGoalManager.resume  s    { 	4%$(!%)")-&$'!%)"$'! 	'%&DK"$/4;///{r7   r   c                r    | j         d S d| j         _        t          | j        | j                    d | _         d S )Nr   )rq  r{   r   r   r:   s    r5   clearzGoalManager.clear  s8    ;F&$/4;///r7   c                    | j         sd S d| j         _        d| j         _        || j         _        t	          | j        | j                    d S )Nr   )rq  r{   r   r   r   r   r  s     r5   	mark_donezGoalManager.mark_done  sI    { 	F##) "($/4;/////r7   r_   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 goalr(   zsubgoal text is empty)	rq  ry  RuntimeErrorr0   r   r   rQ   r   r   )r4   r_   s     r5   add_subgoalzGoalManager.add_subgoal  s     ;dmmoo/000
!!## 	64555##D)))$/4;///r7   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  r   r   zindex out of range (1..))
rq  ry  r  r|   r   r   
IndexErrorpopr   r   )r4   r  idxremoveds       r5   remove_subgoalzGoalManager.remove_subgoal  s    ;dmmoo/000,!#77cS!56666F#dk.B*C*CFFF   +&**3//$/4;///r7   c                    | j         |                                 st          d          t          | j         j                  }g | j         _        t          | j        | j                    |S )z.Wipe all subgoals. Returns the previous count.Nr  )rq  ry  r  r   r   r   r   )r4   prevs     r5   clear_subgoalszGoalManager.clear_subgoals  s[    ;dmmoo/0004;'((!$/4;///r7   c                b    | j         dS | j         j        sdS | j                                         S )z-Public helper for the /subgoal slash command.N(no active goal)u5   (no subgoals — use /subgoal <text> to add criteria))rq  r   r   r:   s    r5   render_subgoalszGoalManager.render_subgoals  s8    ;%%{# 	KJJ{00222r7   r(   r   c                   | j         | j         j        dk    rt          d          t          |          }|dk    rt	          d          || j         _        d| j         _        d| j         _        |pd                                pd| j         _	        t          j
                    | j         _        t          | j        | j                    | j         S )u  Park the goal loop on a background process PID.

        While the PID is alive, ``evaluate_after_turn`` returns
        ``should_continue=False`` without burning a turn or calling the
        judge — the loop quiesces instead of re-poking the agent into busy
        work. The barrier auto-clears when the process exits. Requires an
        active goal. For a process with a watch_patterns/notify_on_complete
        trigger, prefer ``wait_on_session`` so a mid-run trigger (not just
        exit) releases the barrier.
        Nrz   no active goal to parkr   zpid must be a positive integerr   r(   )rq  r{   r  r|   r   r   r   r   r0   r   r  r   r   r   )r4   r   r   s      r5   wait_onzGoalManager.wait_on  s     ;$+"4"@"@7888#hh!88=>>>%(")-&$'!&,l%9%9%;%;%Ct"$(IKK!$/4;///{r7   c                   | j         | j         j        dk    rt          d          t          |pd                                          }|st          d          || j         _        d| j         _        d| j         _        |pd                                pd| j         _	        t          j
                    | j         _        t          | j        | j                    | j         S )u  Park the goal loop on a process_registry session's OWN trigger.

        Unlike ``wait_on`` (which releases only on PID exit), this releases
        when the session's trigger fires: it exits, OR — if it was started
        with ``watch_patterns`` — its pattern matches. This is the right
        barrier for a long-lived watcher/server/poller that signals mid-run
        and may never exit. Requires an active goal.
        Nrz   r  r(   z%session_id must be a non-empty stringr   )rq  r{   r  r)   r0   r   r   r   r   r   r  r   r   r   )r4   r   r   s      r5   r  zGoalManager.wait_on_session	  s     ;$+"4"@"@7888)r**0022
 	FDEEE)3&%)"$'!&,l%9%9%;%;%Ct"$(IKK!$/4;///{r7   r  c                   | j         | j         j        dk    rt          d          t          |          }|dk    rt	          d          d| j         _        d| j         _        t          j                    |z   | j         _        |pd	                                pd| j         _
        t          j                    | j         _        t          | j        | j                    | j         S )u;  Park the goal loop until ``seconds`` from now have elapsed.

        Time-based counterpart to ``wait_on`` — for backoff / cooldown waits
        where there's no process to track (e.g. the agent is rate-limited).
        The barrier auto-clears once the deadline passes. Requires an active
        goal.
        Nrz   r  r   z"seconds must be a positive integerr(   )rq  r{   r  r|   r   r   r   r  r   r0   r   r   r   r   )r4   r  r   s      r5   r  zGoalManager.wait_for_seconds  s     ;$+"4"@"@7888g,,a<<ABBB%)")-&$(IKK'$9!&,l%9%9%;%;%Ct"$(IKK!$/4;///{r7   c                   | j         dS | j         j        | j         j        | j         j        sdS d| j         _        d| j         _        d| j         _        d| j         _        d| j         _        t          | j        | j                    dS )z^Clear any active wait barrier (pid / session / time). Returns True
        if one was cleared.NFr   T)rq  r   r   r   r   r   r   r   r:   s    r5   stop_waitingzGoalManager.stop_waiting4  s     ;5K&..6K- 7 5%)")-&$'!%)"$'!$/4;///tr7   c                ^   | j         }|dS |j        ,t          |j                  rdS |                                  dS |j        ,t          |j                  rdS |                                  dS |j        r4t          j                    |j        k     rdS |                                  dS dS )a  True iff a barrier is set AND not yet satisfied.

        Session barrier: active until the process exits or its watch-pattern
        trigger fires. Pid barrier: active while the process is alive. Time
        barrier: active until the deadline passes. Side effect: a satisfied
        barrier is cleared here (lazy auto-clear) so the next evaluation
        resumes normal judging.
        NFT)rq  r   r   r  r   r   r   r  )r4   r   s     r5   
is_waitingzGoalManager.is_waitingG  s     K95+ 455 t5'!*++ t5? 	y{{Q_,,t5ur7   )user_initiatedr  r+  r  r  r  r   c                  | j         }||j        dk    r|r|j        ndddddddS |                                 rw|j        d|j         }nM|j        d	|j         }n;t          d
t          |j        t          j                    z
                      }| d}|j	        p|}dddd|d| d| dS |xj
        dz  c_
        t          j                    |_        t          |j        ||j        pd||                                r|j        nd          \  }}}	}
||_        ||_        |	r|xj        dz  c_        nd
|_        |dk    r|
r|
                    d          r6|                     t-          |
d                   |           d|
d          }n|
                    d          r6|                     t          |
d                   |           d	|
d          }n5|                     t          |
d                   |           |
d          d}dddd|d| d| dS |dk    r(d|_        t3          | j        |           dddd|d| dS |j        t6          k    r>d|_        d|j         d|_        t3          | j        |           dddd|d|j         ddS |j
        |j        k    rNd|_        d |j
         d!|j         d"|_        t3          | j        |           dddd|d#|j
         d!|j         d$dS t3          | j        |           dd%|                                 d|d&|j
         d!|j         d'| dS )(u  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.

        ``background_processes`` is the live ``process_registry.list_sessions()``
        snapshot for this session. It's handed to the judge so it can decide
        to WAIT on an in-flight process (CI poller, build, ...) instead of
        re-poking the agent — the automatic counterpart to ``/goal wait``.

        Decision keys:
          - ``status``: current goal status after update
          - ``should_continue``: bool — caller should fire another turn
          - ``continuation_prompt``: str or None
          - ``verdict``: "done" | "continue" | "wait" | "skipped" | "inactive"
          - ``reason``: str
          - ``message``: user-visible one-liner to print/send
        Nrz   Finactiver  r(   )r{   should_continuecontinuation_promptr   r   rW  r}  r  r   zs remainingwaitingu   ⏳ Goal parked — waiting on rO   r   )r   r  rv   r   r   r   r   r  r   u'   ⏳ Goal parked (judge) — waiting on r   u   ✓ Goal achieved: rx  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~  )rq  r{   r  r   r   maxr|   r   r  r   r}   r   r_  r   r   r   rv   r   r   r   rH   r  r)   r  r  r   r   &DEFAULT_MAX_CONSECUTIVE_PARSE_FAILURESr   r~   next_continuation_prompt)r4   r+  r  r  r   tgtr  r   r   r]  r^  s              r5   evaluate_after_turnzGoalManager.evaluate_after_turnf  s   4 =ELH44*/9%,,T#('+%*   ?? 	'3;!9;;%13U1333u':TY[['H#I#IJJ	"///)0SF"#('+$ LSLLFLL   	A!Y[[8BJ^+t!5','9'9';';EU^^9
 9
 9
5~ %"
  	1,,1,,,/0E, f!!,// 6$$S)E%F%Fv$VVV?!=??##E** 6S!677GGG4^E244%%c.*C&D&DV%TTT'	2555"#('+! TSTTFTT   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\^^	
 	
 		
r7   r   c                `   | j         r| j         j        dk    rd S | j                                         r| j         j                                        }| j         j        r?d                    d t          | j         j        d          D                       }| d| }t          	                    | j         j
        |          S | j         j        r=t          	                    | j         j
        | j                                                   S t          	                    | j         j
                  S )	Nrz   rP   c              3  ,   K   | ]\  }}d | d| V  dS r6  r?   r   s      r5   r6   z7GoalManager.next_continuation_prompt.<locals>.<genexpr>  sJ       " "4 544d44" " " " " "r7   r   r   )r   r7  )r   r<  )r   )rq  r{   r   rv   rV   r   rS   r   *CONTINUATION_PROMPT_WITH_CONTRACT_TEMPLATEr!  r   *CONTINUATION_PROMPT_WITH_SUBGOALS_TEMPLATEr   CONTINUATION_PROMPT_TEMPLATE)r4   r7  rZ  s      r5   r  z$GoalManager.next_continuation_prompt  s>   { 	dk0H<<4 ;##%% 	![1>>@@N{# >		 " "#,T[-A#K#K#K" " "   %3!=!=e!=!==DD[%- E    ; 	=DD[%#{@@BB E    ,228H2IIIr7   c                    | j         dS | j                                         sdS | j         j                                        S )z>Public helper for the /goal show + /goal draft slash commands.Nr  u^   (no completion contract — set one with /goal draft <objective> or inline field: value lines))rq  r   rv   rV   r:   s    r5   render_contractzGoalManager.render_contract.  sD    ;%%{'')) 	tss{#00222r7   )r   r)   ro  r|   )r*   r   rW   rX   )r   r)   r~   r   rv   r-  r*   ry   )rv   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|   rG   )r   r|   r   r)   r*   ry   )r   r)   r   r)   r*   ry   )r  r|   r   r)   r*   ry   )r+  r)   r  r+   r  r  r*   r   )r*   r   )rY   rZ   r[   r\   r   rr  propertyr   rv  ry  r   r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r?   r7   r5   rn  rn  5  s          EV A A A A A A    XJ J J JV V V VF F F F6 6 6 6: <@dh      "	 	 	 	     .2          0 0 0 0         3 3 3 3    0    ,    *   &   F  $?Cm
 m
 m
 m
 m
 m
^J J J J23 3 3 3 3 3r7   rn  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.)r~   first_responselog	goal_textr~   r  c                   d6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k    rd} |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/           	  |d0|
 d!| d1t	          |d2                      n&# t          $ r} |d(| d)           Y d}~nd}~ww xY wd*|
d3dS 	  ||          pd}	nA# t          $ r4} |d4| d           d|
d5t          |          j	         dcY d}~S d}~ww xY w|
dz  }
@)7u  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*   r   c                J    	  |            d S # t           $ r Y d S w xY wd S r.   )r   )r  r  s    r5   _logz"run_kanban_goal_loop.<locals>._log{  sG    ?C    ?s    
  r   r(   FTz'kanban goal loop: status check failed (z); stoppingstoppedzstatus check failed)r   r}   r   Nr   zkanban goal loop: task z completed by worker after z turn(s)completed_by_workerzworker completed the taskr%   z blocked by worker after blocked_by_workerzworker blocked the task)runningreadyz status=z
; stoppingzstatus=r   r   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*   r   )
r|   r   r   r_  r   KANBAN_GOAL_FINALIZE_TEMPLATEr!  !KANBAN_GOAL_CONTINUATION_TEMPLATErU  rY   )r`  r  run_turntask_status_fnblock_fnr~   r  r  r  r+  r}   nudged_to_finalizer{   r   r   r   _parse_failed_waitr[  s          `           r5   run_kanban_goal_loopr  T  s   N      I2!233I1}}%	"(bMJA	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 1;9m0T0T-f G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
CAsk   
A   
A)
A$A)$A)!D1 1
E;EE"G1 1
H;HHH, ,
I*6)I%I*%I*)ry   r'   rn  rw   rk  r  r  r  rO  rN  rL  ri  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   ry   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)   r*   r   )r  r  r*   r)   )r   r)   r+  r)   r*  r   r   r,  r  r  rv   r-  r*   r   r.   )r`  r   r*   ra  )rg  r)   r*  r   r*   r-  )r   r)   r*   rC   )
r`  r)   r  r)   r~   r|   r  r)   r*   r   )Dr\   
__future__r   r   loggingrer  dataclassesr   r   r   r   r   typingr	   r
   r   r   r   	getLoggerrY   r   r   DEFAULT_JUDGE_TIMEOUTr   rM  r  r  r  r  rS  r   rO  rN  rL  ri  r9   rR   ri   r'   rw   ry   r   r   r]   r   r   r   r   r   r   r   r   compileDOTALLr
  r   r  r)  r_  rf  rk  rj  rn  r  r  r  __all__r?   r7   r5   <module>r     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 	+ +" +#, X0  9 < ).U ):/ > Y  " $  y
I I 	
 N n >  ^ = -   ] ,  !" \#$ "1   8 %  %  %  %  %  %  %  % P. . . .l \\ \\ \\ \\ \\ \\ \\ \\H        	       <   *
> 
> 
> 
>! ! ! ! XZ # # # # # #V, , , ,   8   $ "*Z33$ $ $ $0^\ ^\ ^\ ^\B+U +U +U +Ud +$(;?'+@9 @9 @9 @9 @9 @9FX X X X X( 8M 75 75 75 75 75 75t4 4 4 4@3 3 3 3 3 3 3 3V( ",   'x x x x x xv  r7   