
    e_jJ                       d Z ddlmZ ddlZddlZddlZddlZddlZddlmZm	Z	m
Z
 ddlmZ  ee      j                         j                  j                  Z eedz  dz  dz        Zed	z  d
z  dz  Zed	z  d
z  dz  Zedz  Zedz  dz  j-                         ZdZdZdZdZdZedz  j;                         r!edz  j-                         j=                         ng D ]x  Zdev sejA                         jC                  d      r(ddl"Z"ejG                  d      \  Z$Z%Z&e"jN                  jQ                  e$jA                         e&jA                                z ejR                  jU                  d eedz               ddl+m+Z+ d2dZ,d3dZ-d4dZ.d5dZ/d6dZ0d7dZ1d8d Z2g d!Z3d9d:d"Z4d;d#Z5	 d9	 	 	 d<d$Z6d=d%Z7d>d&Z8d?d'Z9d@d(Z:d)Z;dAd*Z<dBd+Z=dCd,Z>dDd-Z?d3d.Z@d7d/ZAdEd0ZBeCd1k(  r eD eB             y)Fu  Resident design agent — the headless brains, automating DEPLOY.md §7.

The bot and server are transport; this daemon is the part that reads a
customer's free text, authors a RingSpec (via `claude -p` with prompts/design.md
as the manual), builds it through the deterministic `ringcli` compiler, and
flips the request so the bot delivers the studio link. Studio revision notes are
handled the same way through harness/revise.py.

Run (systemd, see ring-agent.service):
  .cad-venv/bin/python harness/design_agent.py            # poll forever
  .cad-venv/bin/python harness/design_agent.py --once     # one cycle, for tests

Scope is deliberately narrow (Ahmed's rule): author -> build -> deliver, and
revise on notes. It NEVER edits code, opens PRs, restarts services, or bypasses
a gate. A request becomes "done" only when the build returned ok:true AND the
STEP artifact exists on disk — never on faith, never lie to the customer.

The model's ONLY output is a spec; the compiler + gates are the backstop against
bad geometry. Untrusted customer text reaches `claude -p`, so the Telegram
allowlist (TELEGRAM_ALLOWED_CHATS) is the real bound on who can spend calls.
    )annotationsN)datetime	timedeltatimezone)Pathz	.cad-venvbinpython	artifactsreviewzrequests.jsonzcomments.jsondesignspromptsz	design.md-      i        z.env=#harness)triagec                    t        j                  t        j                        j	                  d      }t        d| d|  d       y )Nsecondstimespec[z] T)flush)r   nowr   utc	isoformatprint)msgtss     &/opt/ringagent/harness/design_agent.py_logr#   :   s8    	hll	#	-	-y	-	AB	AbTC5/&    c                     t         j                         r't        j                  t         j	                               S dg iS )Nrequests)REQUESTSexistsjsonloads	read_text r$   r"   _load_requestsr-   A   s/    zz(,,.//r$   c                    t         j                  j                  dd       t         j                  t	        j
                  | d             y )NTparentsexist_ok   indent)r'   parentmkdir
write_textr)   dumps)datas    r"   _save_requestsr:   G   s2    OO$6

423r$   c                    t               }d}|d   D ]  }|d   | k(  s|j                  |       |}  |t        |       |S )z@Patch one request in place; returns the updated entry (or None).Nr&   id)r-   updater:   )ridfieldsr9   hitrs        r"   _set_requestrB   L   sS    D
C* T7c>HHVC tJr$   c                    t               }|d   D ]h  }|d   | k(  s|j                  d      dk7  r yd|d<   t        j                  t        j
                        j                  d      |d	<   t        |        y
 y)a  Atomically move a request new -> building so a restart can't double-run
    it. 'building' is invisible to the bot (delivers only done/failed) and to
    triage's new_requests, so a claimed item won't be re-listed. Returns False
    if someone else already moved it off 'new'.r&   r<   statusnewFbuildingr   r   claimedT)r-   getr   r   r   r   r   r:   )r>   r9   rA   s      r"   _claimrI   Y   s{    
 D* T7c>uuX%'$AhK#<<5???SAiL4  r$   c                    t               } d}t        j                  t        j                        t        t              z
  }| d   D ]  }|j                  d      dk(  s	 t        j                  |j                  dd            }|||k  sFd	|d<   |j                  dd       d
}t        d|j                  d       d|d    d        |rt        |        yy# t        $ r d}Y cw xY w)zEOn startup / each cycle, return crashed 'building' requests to 'new'.F)minutesr&   rD   rF   rG    NrE   Tzreset stale build claim on namez (r<   ))r-   r   r   r   r   r   CLAIM_STALE_MINrH   fromisoformat
ValueErrorpopr#   r:   )r9   changedcutoffrA   rG   s        r"   _reset_stale_claimsrU   j   s    DG\\(,,')O*LLF* 
P55?j("00y"1EF 'F"2#(i&2155=/AdG9ANO
P t   s   %CC('C(c                   | j                  d      }|dk  ryd}t        |t        |             D ]A  }| |   dk(  r|dz  }| |   dk(  s|dz  }|dk(  s%	 t        j                  | ||dz          c S  y# t        j
                  $ r Y  yw xY w)z<Pull the first balanced {...} object out of claude's stdout.{r   N   })findrangelenr)   r*   JSONDecodeError)textstartdepthis       r"   _extract_jsonrb      s    IIcNEqyE5#d)$ 	 7c>QJE!W^QJEz ::d5Q&788	   ++   s   A44B
B)BashReadGlobGrep	WebSearchWebFetchEditWriteTask	TodoWriteNotebookEditc                   |rdd| dt         ddddg	}n"dd| dt         ddd	d
j                  t              g	}t        j                  |t        t              ddt        t        j                        }|j                  dk7  rDt        d|j                   d|j                  xs |j                  j                         dd        |j                  j                         S )u  One headless claude -p call. design.md is the SYSTEM prompt (feeding it
    as a user turn makes the model treat it as docs to react to — flaky/empty
    output); the customer directive is the short user turn. Env (IS_SANDBOX +
    OpenRouter auth) comes from the systemd unit / inherited environment.

    With `image`, the model needs the Read tool to view the file, so we allow
    Read only and give it a few turns (Read, then answer) instead of the
    tool-free single turn used for pure text.claudez-pz--system-promptz--max-turns4z--allowed-toolsrd   1z--disallowed-tools,T)cwdcapture_outputr^   timeoutstdinr   zclaude -p failed (z): N)DESIGN_MANUALjoin	_NO_TOOLS
subprocessrunstrROOTCLAUDE_TIMEOUTDEVNULL
returncodeRuntimeErrorstderrstdoutstrip)user_promptimagecmdrA   s       r"   _clauder      s     {,=}c#4f> {,=}c#7)9LNsD	$T-Z5G5G	IA||q/~S xx3188::<TUCDF G 	G88>>r$   c                    d| z   dz   S )Nu0  You are running HEADLESS: there is no interactive turn, so NEVER ask a clarifying question — assume the documented defaults instead (round cut, US sizing, a sensible metal). Every spec MUST have either "archetype" (preset) OR a "shank" (v3) — never omit both.
If — and ONLY if — the request is not a ring description at all (gibberish, empty, spam, or clearly off-topic), do NOT invent a ring: output exactly {"reject": "<short reason>"} instead. A vague but real ring request (e.g. just "a gold ring") is NOT a reject — fill defaults and design it.
zr
Output ONLY the JSON object (a RingSpec, or the reject object): no prose, no explanation, no markdown code fence.r,   )instructions    r"   _author_promptr      s#    - 	P
P 
Qr$   c           	     (   d}i }t        dt        dz         D ]  }t        |       }|r|d|z   z  }t        ||      }t	        |      }|t        d| d       d}Dd	|v rGd
|vrCt        dt        |d	         dd         ddt        |j                  d	            dd gdi fc S ||d
<   |}t        |      }	|	j                  d      r|	|fc S dj                  |	j                  ddg            }
t        d| d|
dd         |
} |r	|fS d|fS )a*  Author a spec and build it, self-correcting on validation/build errors.
    Returns (build_result_or_None, last_spec). build_result is the ringcli
    result dict; None means we never got parseable JSON out of the model.
    With `image`, the first model call reads the photo as a design reference.rL   rX   zT

Your previous spec FAILED. Fix exactly these and re-emit the full corrected JSON:
r   Nz
  attempt z: no JSON in model outputz0Your last output contained no valid JSON object.rejectrM   z  rejected: x   FT   )okr   errorsr   ; r   zunknown build failurez build failed: )
r[   MAX_SPEC_ATTEMPTSr   r   rb   r#   r}   rH   _buildry   )r   rM   r   
errors_ctx	last_specattemptpromptoutspecresulterrss              r"   author_specr      sk    JI-12 , <>HI JFfE*S!<:gY&?@AKJtd 2<DN 3DS 9:;<4"488H#56t<=?@BC CV	::d4<yyH/F.GHIz'/$t*>?
-.  FI55TI55r$   c           
        t        j                  t        ddddt        j                  |       gt        t              ddt              }|j                  j                         }d|v r(	 t        j                  ||j                  d      d        S d|xs |j                  j                         d	d  gd
S # t        j                  $ r Y :w xY w)N-mzringcli.clibuild--spec-jsonTrs   rt   r^   ru   rW   Frw   )r   r   )r{   r|   PYBINr)   r8   r}   r~   BUILD_TIMEOUTr   r   r*   indexr]   r   )r   rA   r   s      r"   r   r      s    t]G]

4(*/24y -	9A ((..
C
cz	::c#))C./233 S_AHH$;$;$=de$D#EFF ## 		s   (&B4 4C
	C
c                   t         |  dz  }t        dz  dz  | z  }t        ||  dz        ddgt        ||  dz        ddgfD ]<  }t        j                  t
        d	d
t        |      g|z   t        t              dd       > t        j                  t
        d	dt        |      t        ||  dz        gt        t              dd       y)zEStudio previews + parts GLB, best-effort (same commands as serve.py)..pyr
   cliz.pngz25:20z
--supportsz	-head.pngz--headr   zringcli.renderTi,  )rs   rt   ru   zringcli.glbz
.parts.glbN)DESIGNSr~   r}   r{   r|   r   )rM   genoutdirargss       r"   _render_previewsr      s    
tfCL
 CK%'$.Ff$t},-wEf$y112GXFH Ht%5s3x@4Gt9T3	HH NNE4CD6!44574ysDr$   c                    t         dz  dz  | z  |  dz  }|j                         xr |j                         j                  dkD  S )z<The done-guard: a real, non-trivial STEP must exist on disk.r
   r   z.stepiP  )r~   r(   statst_size)rM   steps     r"   _step_okr      sC    +%,$u~=D;;=9TYY[00699r$   c                    t         |  dz  }|j                         rH|j                         j                         D ]'  }|j	                  d      s	 t        |dd  di i      c S  i S # t        $ r i cY c S w xY w)Nr   zSPEC =    __builtins__)r   r(   r+   
splitlines
startswitheval	Exception)designr   lines      r"   _current_specr     s    
vhcN
"C
zz|MMO..0 	Dy)QR>2*>??	 I ! Is   A''A76A7u   I couldn't turn that into a ring I can build. Try describing the cut (round, oval, emerald…), metal, ring size, and a carat or style — one ring per message.c           	        t        |      }|sy	 t        d| dt        j                  |       d|  d      }|j                         j                         j                  d	      S # t        $ r}t        d|d       Y d}~yd}~ww xY w)
zIs this follow-up text a REVISION of `design`, or a request for a NEW
    ring? Returns True for revise. Defaults to new on any ambiguity/error.Fz&The customer already has ring design "z" with this spec:
z

Their new message is: "z"

Is this a REVISION of that existing ring, or a request for a brand-NEW, different ring? A tweak to metal/stone/band/size is a revision; describing a different ring is new. Answer with ONLY one word: revise OR new.z  classifier error (u   ) — defaulting to newNrevise)	r   r   r)   r8   r   r#   r   lowerr   )r^   r   currentr   es        r"   _classify_revisionr     s     F#G4VH<Ozz'"# $&&*V ,##$ 99;))(33  #A5(?@As   (A% %	B.BBc                   | d   | d   | j                  d      xs d}}}| j                  d      }| j                  d      }t        |      sy |rI|sG|rEt        d| d| d	       t        ||      rt        d
|        t	        |||       y t        d       |rdnd}t        d| d| d| d|d d        	 |rd| d|rd| dndz   d| dz   }nd| d| }t        |||      \  }}	|rM|j                  d      r<t        |      r1t        |       |rddini }
t        |fddi|
 t        d | d!       y d"j                  |xs i j                  d#d$g            }t        |d%t        &       t        d'| d(|d d)         y # t        $ r-}t        |d%t        &       t        d*| d+|       Y d }~y d }~ww xY w),Nr<   rM   r^   rL   r   reply_tozrequest z: classifying vs existing ''z  -> revision of z  -> new ringphotoznew z	 request z 'z': P   z*The customer sent the reference image at: uh   
Read that image — it is the ring they want as a REFERENCE. Compose ONE RingSpec matching what you seez!, then apply this modification: ""z. A photo cannot show ring size, so use size 6.5. Assume documented defaults for anything not visible. Use exactly this name: "".zKTranslate this customer request into ONE RingSpec. Use exactly this name: "z".

Customer request: r   r   size_assumedTrD   donez  done: z# built + previews; bot will deliverr   r   zno buildable specfailed)rD   errorz
  failed: u    — r   z  error on : )rH   rI   r#   r   _route_as_revisionr   r   r   rB   ry   FRIENDLY_FAILr   )reqr>   rM   r^   r   r   kindr   r   r   extrar   r   s                r"   handle_new_requestr   .  s   $iVcggfo.CtCGGGEwwz"H#; $xu7zCDdH-$XJ/0sHd3}7D4vYse2dV3tCRym	<=*<UG D= > CG6tfA>BP#fB(	( 66:V <004v7K #;EBfjj&8D>T".3^T*E5V5u58D6!DEF99fl//;N:OPQDX]C:dV5ds56 *S?{4&1%())*s    :BF
 =AF
 
	G #F;;G c                4   t        j                         j                  dd ||ddt        j                  t
        j                        j                  d      d}t        j                         r't        j                  t        j                               ndg i}|d   j                  |       t        j                  j                  d	d	
       t        j!                  t        j"                  |d             t%        ||d   |d       t'        | dd	|       y)u1  Convert a Telegram follow-up into a studio note against `design` and run
    the existing revise path, then retire the request so it never builds as a
    new ring. The customer already has the (same) studio link; revise.py keeps
    the same name so that link just updates — no new bot delivery needed.N   revisionopenr   r   )r<   r   r^   r   rD   createdcommentsTr/   r2   r3   r<   )r   r<   r^   r   )rD   notifiedrouted_revision)uuiduuid4hexr   r   r   r   r   COMMENTSr(   r)   r*   r+   appendr5   r6   r7   r8   handle_noterB   )r>   r   r^   noter9   s        r"   r   r   `  s    
 **,""2A&&*||HLL1;;Y;OQD 08/@4::h((*+zSUFVDD!OO$6

4236dTBC VdFKr$   c                   | d   | d   | d   }}}|j                  d      r|d d n|}t        d| d| d|d d	        t        |      }|st        d
|        y 	 d| dt        j                  |       d| d| d	}t        t        |            }t        |      }|t        d|        y ||d<   t        j                  t        d|dt        j                  |      d|dd|d d  g	t        t              ddt              }	|	j                  dk(  rt        d| d|        y t        d| d|	j                  xs |	j                   j#                         dd          y # t$        $ r}
t        d| d|
       Y d }
~
y d }
~
ww xY w) Nr   r<   r^   z-prodznote z on r   r   z  skip: no current spec for z)Here is the CURRENT RingSpec for design "z":
z)

The customer left this revision note: "zN"
Apply the change, keeping everything else the same. Use exactly this name: "r   z(  skip: model produced no JSON for note rM   zharness/revise.pyr   z	--resolvez--notez	applied: r   Tr   r   z
  revised z, resolved note z  revise.py failed for i8z  error revising )endswithr#   r   r)   r8   r   r   rb   r{   r|   r   r}   r~   r   r   r   r   r   r   )r   r   note_idr^   baser   r   r   r   rA   r   s              r"   r   r   r  s    NDJVTGF //'26#2;D5	dV2d3Bi]	34D!G+D62307vTzz'"# $66:V <&	$ 	 n[12S!<;G9EFVNN'}djj>N'8yds-EGD	$T=R <<1:dV#3G9=>*4&XX)002459:< = 0 b.//0s&   AE  .A6E  %:E   	F)E??Fc                 &   t         j                         si S 	 t        j                  t         j	                               } | j                  dg       D ci c]  }|d   |j                  dd       c}S # t        j
                  $ r i cY S w xY wc c}w )u   id -> kind for open notes. System notes (kind:'system') are gate-failure
    breadcrumbs meant for a human — the daemon must NOT try to 'revise' them.r   r<   r   r   )r   r(   r)   r*   r+   r]   rH   )r9   cs     r"   _note_kindsr     s     ??	zz(,,./ 9=R8PQ1AdGQUU6:..QQ  	Qs   'A3 B3B
Bc                     t                t               } | j                  dg       D ]  }t        |        t	               }| j                  dg       D ]%  }|j                  |d         dk(  rt        |       ' y )Nnew_requests
open_notesr<   system)rU   r   rH   r   r   r   )tr   kindsr   s       r"   cycler     sr    Auu^R(  3 MElB' 99T$Z H,Dr$   c                    dt         j                  v } t        dt         d| rdnd d       | rt	                y	 	 t	                t        j                  t               %# t
        $ r}t        d|       Y d }~6d }~ww xY w)	Nz--oncezdesign agent up (poll su	    — ONCErL   rN   r   zcycle error: )sysargvr#   POLL_SECONDSr   r   timesleep)oncer   s     r"   mainr     s|    sxxD!,q"0MQ	OP
	(G 	

<    	(=&''	(s   
A   	B)A<<B__main__)r    r}   returnNone)r   dict)r9   r  r   r   )r>   r}   r   dict | None)r>   r}   r   bool)r   r   )r^   r}   r   r  )N)r   r}   r   
str | Noner   r}   )r   r}   r   r}   )r   r}   rM   r}   r   r  r   ztuple[dict | None, dict])r   r  r   r  )rM   r}   r   r   )rM   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   int)E__doc__
__future__r   r)   r{   r   r   r   r   r   r   pathlibr   __file__resolver5   r~   r}   r   r'   r   r   r+   rx   r   r   r   r   rO   r(   r   _liner   r   os	partition_k__venviron
setdefaultpathinsertr   r#   r-   r:   rB   rI   rU   rb   rz   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   __name__
SystemExitr,   r$   r"   <module>r     s  , #   
   2 2 H~&&--D;&12+(?:+(?:

	!K/::<  ;?-9O9O9QdVm&&(335WY 6E
e|EKKM44S9OOC(	Ar


bhhj"((*5	6 3ti'( ) '4

",.C	.Q %)6!6-E6H
G
D:	<
4*/*dL$0D	R	! z
TV
 r$   