@@ -24,6 +24,8 @@ import { extractCompletionJson } from "../utils/completion-parser.js";
2424import { TIMEOUTS , INTERVALS } from "../constants.js" ;
2525import { shouldSplit , autoSplitTasks } from "../task-splitter.js" ;
2626import { detectDependencies , sortByDependency } from "../task-dependency.js" ;
27+ import { OpsRunner } from "../ops-runner.js" ;
28+ import { extractJsonObject } from "../utils/extract-json.js" ;
2729
2830// ---------------------------------------------------------------------------
2931// Helpers
@@ -63,6 +65,14 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
6365 // Start stall detection for agent processes
6466 runner . startStallDetection ( ) ;
6567
68+ // Start ops task runner (background, parallel to main loop)
69+ const opsRunner = new OpsRunner ( {
70+ apiUrl : cliArgs . apiUrl ,
71+ apiKey : cliArgs . apiKey ,
72+ pollIntervalMs : 60_000 ,
73+ } ) ;
74+ opsRunner . start ( ) ;
75+
6676 while ( ! shutdownState . shuttingDown ) {
6777 try {
6878 sprintData = await api . fetchSprintData ( ) ;
@@ -257,6 +267,7 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
257267 // Use slot name as agent name to avoid collisions in parallel dispatch
258268 const agentName = slotName ;
259269 const agentRole = task . owner ?? "builder" ;
270+ const agentInfo = sprintData . agents . find ( ( a ) => a . name === agentRole ) ;
260271 const apiDocs = await api . fetchApiDocs ( agentRole ) ;
261272 const taskType = task . type as string | undefined ;
262273 const taskLabels = parseTaskLabels ( task ) ;
@@ -269,7 +280,7 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
269280
270281 const actionCtx : ActionContext = {
271282 api, task, agentName, template : agentTemplate , taskLog,
272- config : { apiUrl : cliArgs . apiUrl , apiKey : cliArgs . apiKey , workingDir : taskWorkingDir , baseBranch : cliArgs . baseBranch , sprintNumber : sprintData . sprint . number , language : ctx . language , engine : cliArgs . engine } ,
283+ config : { apiUrl : cliArgs . apiUrl , apiKey : cliArgs . apiKey , workingDir : taskWorkingDir , baseBranch : cliArgs . baseBranch , sprintNumber : sprintData . sprint . number , language : ctx . language , engine : cliArgs . engine , agentEngine : agentInfo ?. engine } ,
273284 onDataUpdate : ( entity , id , changes ) => {
274285 ctx . wsServer ?. broadcast ( {
275286 type : WS_MSG . DATA_UPDATE ,
@@ -358,7 +369,6 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
358369 if ( ctx . gitUserInfo ) ensureGitUser ( taskWorkingDir , ctx . gitUserInfo . name , ctx . gitUserInfo . email ) ;
359370
360371 // Resolve model from agent's DB engine setting or role default
361- const agentInfo = sprintData . agents . find ( ( a ) => a . name === agentRole ) ;
362372 const agentModel = resolveModelForRole ( agentRole , agentInfo ?. engine ) ;
363373
364374 const agentConfig = {
@@ -380,7 +390,7 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
380390 continue ;
381391 }
382392
383- ui . agentSpawned ( { agentName : agentConfig . name , taskId : task . id , taskTitle : task . title , docker : ! cliArgs . noDocker } ) ;
393+ ui . agentSpawned ( { agentName : agentConfig . name , taskId : task . id , taskTitle : task . title , docker : ! cliArgs . noDocker , model : agentModel } ) ;
384394
385395 const messagePoller = new MessagePoller ( { api, channel : agentRole , workingDir : taskWorkingDir } ) ;
386396 messagePoller . start ( ) ;
@@ -437,13 +447,15 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
437447 for ( const line of runningAgent . stdout ) {
438448 let raw : string | null = null ;
439449 if ( line . startsWith ( "RETRO_JSON:" ) ) {
440- raw = line . slice ( "RETRO_JSON:" . length ) ;
450+ raw = extractJsonObject ( line . slice ( "RETRO_JSON:" . length ) ) ;
441451 } else {
442452 try {
443453 const event = JSON . parse ( line ) ;
444454 const text = typeof event === "object" && event ?. type === "assistant" ? ( event . message ?. content ?. [ 0 ] ?. text ?? "" ) : "" ;
445- const m = text . match ?.( / R E T R O _ J S O N : ( \{ [ \s \S ] * \} ) / ) ;
446- if ( m ) raw = m [ 1 ] ;
455+ const retroIdx = text . indexOf ?.( "RETRO_JSON:" ) ;
456+ if ( retroIdx !== undefined && retroIdx !== - 1 ) {
457+ raw = extractJsonObject ( text . slice ( retroIdx + "RETRO_JSON:" . length ) ) ;
458+ }
447459 } catch { /* not JSON */ }
448460 }
449461 if ( raw ) {
@@ -459,13 +471,15 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
459471 for ( const line of runningAgent . stdout ) {
460472 let raw : string | null = null ;
461473 if ( line . startsWith ( "RETRO_JSON:" ) ) {
462- raw = line . slice ( "RETRO_JSON:" . length ) ;
474+ raw = extractJsonObject ( line . slice ( "RETRO_JSON:" . length ) ) ;
463475 } else {
464476 try {
465477 const event = JSON . parse ( line ) ;
466478 if ( event . type === "result" && typeof event . result === "string" ) {
467- const match = event . result . match ( / R E T R O _ J S O N : ( \{ [ \s \S ] * \} ) / ) ;
468- if ( match ) raw = match [ 1 ] ;
479+ const retroIdx = event . result . indexOf ( "RETRO_JSON:" ) ;
480+ if ( retroIdx !== - 1 ) {
481+ raw = extractJsonObject ( event . result . slice ( retroIdx + "RETRO_JSON:" . length ) ) ;
482+ }
469483 }
470484 } catch { /* not JSON */ }
471485 }
@@ -568,6 +582,7 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
568582 }
569583 }
570584
585+ opsRunner . stop ( ) ;
571586 await api . updateAgent ( { name : cliArgs . agentName , status : "idle" , activity : "Shut down" } ) ;
572587 ui . outro ( "Shutting down — goodbye" ) ;
573588}
0 commit comments