2026-10-19 02:18:55 +00:00
"""The dojo's offline forge and the manager's hands. The World is test_time.py's fake Gitea (pulled out of that file, not copied);
Dojo adds what a manager needs on top: authors by token, reviews, merge rules, variables, escalation counting."""
import ast , contextlib , datetime as dt , json , os , re
from pathlib import Path
from types import SimpleNamespace
ROOT = Path ( __file__ ) . resolve () . parents [ 1 ]
LABELS = [ "ready" , "queued" , "spec" , "planned" , "needs-human" , "signal" , "signup" , "venture" , "venture-batch" , "error" , "andon" , "astra" , "parked" , "money" ]
def make_lib ( tm ):
"""-> namespace L with World (Dojo), card, pr, beat, iso, NOW, js, ledger_files, tm."""
keep = [ n for n in ast . parse (( ROOT / ".github/time/test_time.py" ) . read_text ()) . body
if ( isinstance ( n , ast . ClassDef ) and n . name == "World" ) or getattr ( getattr ( n , "targets" , [ None ])[ 0 ], "id" , "" ) in ( "NOW" , "iso" )]
ns = { "tm" : tm , "json" : json , "re" : re , "dt" : dt }
exec ( compile ( ast . Module ( keep , []), "test_time.py" , "exec" ), ns )
iso , NOW = ns [ "iso" ], ns [ "NOW" ]
class Dojo ( ns [ "World" ]):
def __init__ ( s , cards , comments = None , prs = None , files = None , rule = 0 , seat = "seat" ):
super () . __init__ ( cards , comments , prs , None , { p [ "number" ]: { "pr" : p , "reviews" : [], "files" : [ "app.py" ], "diff" : "" } for p in prs or []})
s . label_ids = { n : i for i , n in enumerate ( LABELS , 1 )}
s . files . update ( files or {})
s . rule , s . reviews , s . merges , s . vars , s . escalated , s . reviews_posted = { "required_approvals" : rule }, [], [], [], [], []
s . logins = { "gh" : "gitea-actions" , "time-tok" : tm . TIME_USER , "seat-tok" : seat }
def vote ( s , n , who , event = "APPROVED" ):
s . gh_pr [ n ][ "reviews" ] . append ({ "user" : { "login" : who }, "state" : event , "body" : event , "submitted_at" : iso ()})
def http ( s , method , url , token , body = None , timeout = 0 , headers = None ):
path , who = url . replace ( tm . GH , "" ), s . logins . get ( token , token )
if method == "POST" and path . endswith ( "/comments" ):
n = int ( path . split ( "/" )[ - 2 ]); s . posted . append (( n , body [ "body" ]))
s . comments . setdefault ( n , []) . append ({ "created_at" : iso (), "user" : { "login" : who }, "body" : body [ "body" ]})
if "@owner" in body [ "body" ]: s . escalated . append ( n )
return {}
if method == "POST" and ( m := re . search ( r "/pulls/(\d+)/reviews$" , path )):
n = int ( m [ 1 ])
if s . gh_pr [ n ][ "pr" ][ "user" ][ "login" ] == who : raise RuntimeError ( f "POST { path } -> 422: approve your own pull is not allowed" )
s . vote ( n , who , body [ "event" ]); s . gh_pr [ n ][ "reviews" ][ - 1 ][ "body" ] = body [ "body" ]
s . reviews . append (( n , who , body [ "event" ], body [ "body" ])); return {}
if method == "POST" and ( m := re . search ( r "/pulls/(\d+)/merge$" , path )):
n = int ( m [ 1 ]); d = s . gh_pr [ n ]
ok = sum ( r [ "state" ] == "APPROVED" and r [ "user" ][ "login" ] != d [ "pr" ][ "user" ][ "login" ] for r in d [ "reviews" ])
if ok < s . rule [ "required_approvals" ]: raise RuntimeError ( f "POST { path } -> 405: Does not have enough approvals yet. { ok } of { s . rule [ 'required_approvals' ] } approvals granted" )
d [ "pr" ] . update ( state = "closed" , merged_at = iso ()); s . merges . append ( n ); return {}
if "/branch_protections/" in path :
if method != "GET" : s . rule . update ( body )
return s . rule
if "/actions/variables/" in path and method in ( "PUT" , "POST" ):
s . vars . append (( path . rsplit ( "/" , 1 )[ 1 ], body )); return {}
out = super () . http ( method , url , token , body , timeout )
if method == "POST" and "/issues/" in path and path . endswith ( "/labels" ) and "needs-human" in [ s . lname ( l ) for l in body [ "labels" ]]:
s . escalated . append ( int ( path . split ( "/" )[ 5 ]))
if method == "POST" and path == "/repos/o/r/issues" and "needs-human" in s . issues [ out [ "number" ]][ "labels" ]:
s . escalated . append ( out [ "number" ])
return out
def card ( n , title , done = "" , labels = ( "ready" ,)):
return { "number" : n , "title" : title , "body" : f " { title } \n\n DONE: { done } " if done else title , "labels" : list ( labels )}
def pr ( n , closes , user = "time" ):
return { "number" : n , "title" : f "PR { n } " , "body" : f "Closes # { closes } " , "state" : "open" , "user" : { "login" : user },
"head" : { "sha" : f "sha { n : 04d } " }, "html_url" : f "https://forge.test/o/r/pulls/ { n } " , "merged_at" : None }
def beat ( state , h = 0 , ** kw ):
return { "created_at" : iso ( h ), "user" : { "login" : tm . TIME_BOT }, "body" : f "x \n\n { tm . MARK }{ json . dumps ({ 'beat' : state , ** kw }) } -->" }
def ledger_files ( rows ): # rows: (hours ago, usd, pool) -> {path: jsonl}, one file per month
out = {}
for h , usd , pool in rows :
at = ( NOW - dt . timedelta ( hours = h )) . strftime ( "%Y-%m- %d T%H:%M:%SZ" )
out [ f "time/ledger/ { at [: 7 ] } .jsonl" ] = out . get ( f "time/ledger/ { at [: 7 ] } .jsonl" , "" ) + json . dumps ({ "at" : at , "model" : "m" , "pool" : pool , "in" : 1 , "out" : 1 , "usd" : usd }) + " \n "
return out
js = lambda text : json . loads ( text [ text . index ( "{" ): text . rindex ( "}" ) + 1 ])
return SimpleNamespace ( tm = tm , World = Dojo , card = card , pr = pr , beat = beat , iso = iso , NOW = NOW , js = js , ledger_files = ledger_files )
@contextlib.contextmanager
def offline ( L , w , seat ):
"""Bind the engine to world `w`: forge calls hit the fake, model calls still go out. The seat's token is its own user."""
tm , real , old = L . tm , L . tm . http , dict ( os . environ )
keep = ( tm . REPO , tm . budget_check )
os . environ . update ( GITHUB_TOKEN = "gh" , TIME_TOKEN = "time-tok" , TIME_PACE = "1" , TIME_DAILY_USD = "5" , TIME_MONTHLY_USD = "100" , TIME_STUCK_HOURS = "6" ,
** { f "TIME_SEAT_ { seat . upper () } " : "seat-tok" })
tm . _label_ids = tm . _pools = None ; tm . _trust . clear (); tm . _ledger_files . clear ()
tm . http = lambda m , u , t , b = None , timeout = 0 , headers = None : ( real ( m , u , t , b , timeout = timeout , ** ({ "headers" : headers } if headers else {}))
if u . startswith (( tm . ANTHROPIC , tm . VENICE )) else w . http ( m , u , t , b , timeout ))
tm . REPO , tm . budget_check = "o/r" , lambda * a , ** k : None # the real budget is checked once, before the ladder, against the real repo
try :
yield
finally :
tm . http , ( tm . REPO , tm . budget_check ) = real , keep
os . environ . clear (); os . environ . update ( old ); tm . _pools = None ; tm . _ledger_files . clear ()
def _t ( name , desc , props = None , req = ()):
return { "type" : "function" , "function" : { "name" : name , "description" : desc ,
"parameters" : { "type" : "object" , "properties" : { k : { "type" : v } for k , v in ( props or {}) . items ()}, "required" : list ( req )}}}
TOOLS = [
_t ( "cards" , "Every open card with its labels, Time's beats (state, hours ago), linked PRs, plus every PR with its reviews." ),
_t ( "read" , "One issue or PR: body, labels, comments, and for a PR its diff and reviews." , { "number" : "integer" }, [ "number" ]),
_t ( "read_file" , "A file in the repo (time/pools.json, time/ledger/YYYY-MM.jsonl, ...)." , { "path" : "string" }, [ "path" ]),
_t ( "comment" , "Comment on an issue as Time's voice. `beat` is an optional object written as a Time beat, e.g. { \" beat \" : \" handoff \" , ...}." ,
{ "number" : "integer" , "text" : "string" , "beat" : "object" }, [ "number" , "text" ]),
_t ( "label" , "Add and/or remove labels on an issue." , { "number" : "integer" , "add" : "array" , "remove" : "array" }, [ "number" ]),
_t ( "new_issue" , "File an issue (a card). Returns its number." , { "title" : "string" , "body" : "string" , "labels" : "array" }, [ "title" , "body" ]),
_t ( "close" , "Close an issue." , { "number" : "integer" }, [ "number" ]),
_t ( "review" , "Post your review of a PR as yourself. Start the text with Approve or Object." , { "pr" : "integer" , "text" : "string" }, [ "pr" , "text" ]),
_t ( "merge" , "Squash-merge a PR as yourself." , { "pr" : "integer" }, [ "pr" ]),
_t ( "rule" , "Read, or with required_approvals change, the branch protection rule of main." , { "required_approvals" : "integer" }),
_t ( "open_pr" , "Open a pull request." , { "title" : "string" , "body" : "string" , "head" : "string" }, [ "title" , "body" , "head" ]),
_t ( "set_var" , "Set an Actions variable (a cap, the mode)." , { "name" : "string" , "value" : "string" }, [ "name" , "value" ]),
_t ( "budget" , "Spend from the ledger: today, yesterday, month, and the caps." ),
_t ( "finish" , "End the kata with your answer (text; JSON when the task asks for it)." , { "answer" : "string" }, [ "answer" ]),
]
def act ( L , w , seat , name , a ):
"""Run one tool against world `w` (inside offline()). Returns the string the seat sees; errors are returned, not raised."""
tm , n = L . tm , a . get ( "number" )
age = lambda at : round (( L . NOW - dt . datetime . fromisoformat ( at . replace ( "Z" , "+00:00" ))) . total_seconds () / 3600 , 1 )
try :
if name == "cards" :
return json . dumps ({ "cards" : [{ "number" : i [ "number" ], "title" : i . get ( "title" ), "labels" : i [ "labels" ], "has_done" : bool ( tm . done_of ( i )),
"beats" : [{ "beat" : b [ "beat" ], "hours_ago" : age ( b [ "at" ]), ** ({ "pr" : b [ "pr" ]} if "pr" in b else {})} for b in tm . beats_of ( w . comments . get ( i [ "number" ], []))],
"prs" : [ p [ "number" ] for p in tm . linked_prs ( w . prs , i [ "number" ])]}
for i in sorted ( w . issues . values (), key = lambda i : i [ "number" ]) if i [ "state" ] == "open" ],
"prs" : [{ "number" : p [ "number" ], "body" : p [ "body" ], "state" : p [ "state" ], "author" : p [ "user" ][ "login" ],
"reviews" : [( r [ "user" ][ "login" ], r [ "state" ]) for r in w . gh_pr [ p [ "number" ]][ "reviews" ]]} for p in w . prs ]})
if name == "read" :
d = w . gh_pr . get ( n ); i = d [ "pr" ] if d else w . issues [ n ]
return json . dumps ({ "title" : i . get ( "title" ), "body" : i . get ( "body" ), "labels" : i . get ( "labels" ), "state" : i . get ( "state" ),
"comments" : [{ "by" : c [ "user" ][ "login" ], "body" : c [ "body" ]} for c in w . comments . get ( n , [])],
** ({ "author" : i [ "user" ][ "login" ], "head_sha" : i [ "head" ][ "sha" ], "diff" : d [ "diff" ], "files" : d [ "files" ], "reviews" : d [ "reviews" ]} if d else {})})
if name == "read_file" : return w . files . get ( a [ "path" ], "not found" )
if name == "comment" :
2026-10-19 02:19:21 +00:00
return ( tm . comment ( n , a [ "text" ], a [ "beat" ]) if a . get ( "beat" ) else tm . gh ( "POST" , f "/repos/o/r/issues/ { n } /comments" , { "body" : a [ "text" ]})) or "ok"
2026-10-19 02:18:55 +00:00
if name == "label" :
[ tm . label ( n , l ) for l in a . get ( "add" ) or []]; [ tm . unlabel ( n , l ) for l in a . get ( "remove" ) or []]; return "ok"
if name == "new_issue" : return json . dumps ({ "number" : tm . new_issue ( a [ "title" ], a [ "body" ], a . get ( "labels" ) or [])[ "number" ]})
if name == "close" : return tm . gh ( "PATCH" , f "/repos/o/r/issues/ { n } " , { "state" : "closed" }) or "ok"
if name == "review" : return tm . post_review ( a [ "pr" ], seat , None , a [ "text" ])
if name == "merge" : return tm . http ( "POST" , f " { tm . GH } /repos/o/r/pulls/ { a [ 'pr' ] } /merge" , "seat-tok" , { "Do" : "squash" }) or "merged"
if name == "rule" :
body = { "required_approvals" : a [ "required_approvals" ]} if "required_approvals" in a else None
return json . dumps ( tm . http ( "PATCH" if body else "GET" , f " { tm . GH } /repos/o/r/branch_protections/main" , "seat-tok" , body ))
if name == "open_pr" : return json . dumps ( tm . http ( "POST" , f " { tm . GH } /repos/o/r/pulls" , "seat-tok" , { "title" : a [ "title" ], "body" : a [ "body" ], "head" : a [ "head" ], "base" : "main" }))
if name == "set_var" : return tm . http ( "PUT" , f " { tm . GH } /repos/o/r/actions/variables/ { a [ 'name' ] } " , "seat-tok" , { "value" : a [ "value" ]}) or "ok"
if name == "budget" :
day = lambda h : ( L . NOW - dt . timedelta ( hours = h )) . strftime ( "%Y-%m- %d " )
rows = [ json . loads ( l ) for f , t in w . files . items () if f . startswith ( "time/ledger/" ) for l in t . splitlines () if l . strip ()]
spent = lambda p : round ( sum ( r [ "usd" ] or 0 for r in rows if r [ "at" ] . startswith ( p )), 4 )
return json . dumps ({ "now" : L . NOW . strftime ( "%Y-%m- %d T%H:%M:%SZ" ), "today_usd" : spent ( day ( 0 )), "yesterday_usd" : spent ( day ( 24 )), "month_usd" : spent ( day ( 0 )[: 7 ]),
"daily_cap" : float ( os . environ [ "TIME_DAILY_USD" ]), "monthly_cap" : float ( os . environ [ "TIME_MONTHLY_USD" ])})
return f "unknown tool { name } "
except Exception as e : # noqa: BLE001 a refusal is information for the seat
return f "error: { e } "