@@ -19,6 +19,8 @@ import { execSync } from "node:child_process";
1919import { handlePropose } from "./propose.js" ;
2020import type { ShutdownState } from "./shutdown.js" ;
2121import { parseTaskLabels } from "../utils/parse-labels.js" ;
22+ import { extractCompletionJson } from "../utils/completion-parser.js" ;
23+ import { TIMEOUTS , INTERVALS } from "../constants.js" ;
2224
2325// ---------------------------------------------------------------------------
2426// Helpers
@@ -62,7 +64,7 @@ Output ONLY a JSON array, no markdown:
6264 return new Promise ( ( resolve ) => {
6365 const child = spawn ( "claude" , [ "--print" , "--model" , resolveModel ( "claude-haiku" ) , "--max-turns" , "1" , prompt ] , {
6466 stdio : [ "ignore" , "pipe" , "pipe" ] ,
65- timeout : 30_000 ,
67+ timeout : TIMEOUTS . SPLIT_TASK ,
6668 } ) ;
6769 let out = "" ;
6870 child . stdout ?. on ( "data" , ( chunk : Buffer ) => { out += chunk . toString ( ) ; } ) ;
@@ -96,16 +98,14 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
9698 const { api, wsServer, tobanHome, repos, gitToken, gitUserInfo, credentialHelperPath } = ctx ;
9799 let { sprintData } = ctx ;
98100
99- const POLL_INTERVAL_MS = 30_000 ;
101+ const POLL_INTERVAL_MS = INTERVALS . POLL ;
100102
101103 // Parallel agent slots
102104 const { SlotScheduler } = await import ( "../slot-scheduler.js" ) ;
103- const { MergeLock } = await import ( "../merge-lock.js" ) ;
104105 const scheduler = new SlotScheduler ( [
105106 { role : "builder" , maxConcurrency : 2 } ,
106107 { role : "cloud-engineer" , maxConcurrency : 1 } ,
107108 ] ) ;
108- const mergeLock = new MergeLock ( ) ;
109109
110110 while ( ! shutdownState . shuttingDown ) {
111111 try {
@@ -456,87 +456,15 @@ export async function runLoop(cliArgs: CliArgs, runner: AgentRunner, shutdownSta
456456 } ) ;
457457 } ;
458458 // Extract COMPLETION_JSON from agent stdout → enrich post_action update_task
459- // Extract COMPLETION_JSON or stream result from agent stdout
460- // Fallback: if no COMPLETION_JSON found, check stream-json result event
461459 if ( ! actionCtx . completionJson ) {
462- for ( const l of runningAgent . stdout ) {
463- try {
464- const ev = JSON . parse ( l ) ;
465- if ( ev . type === "result" && ev . subtype === "success" && typeof ev . result === "string" ) {
466- // Try to extract COMPLETION_JSON from result text
467- const cjMatch = ev . result . match ( / C O M P L E T I O N _ J S O N : ( \{ [ \s \S ] * \} ) / ) ;
468- let resultText : string ;
469- if ( cjMatch ) {
470- try {
471- const cj = JSON . parse ( cjMatch [ 1 ] ) ;
472- actionCtx . completionJson = { review_comment : cj . review_comment , commits : cj . commits } ;
473- resultText = cj . review_comment || ev . result . slice ( 0 , 2000 ) ;
474- } catch {
475- resultText = ev . result . slice ( 0 , 2000 ) ;
476- actionCtx . completionJson = { review_comment : resultText , commits : "" } ;
477- }
478- } else {
479- resultText = ev . result . slice ( 0 , 2000 ) ;
480- actionCtx . completionJson = { review_comment : resultText , commits : "" } ;
481- }
482- for ( const action of agentTemplate . post_actions ) {
483- if ( action . type === "update_task" && action . when === "success" && action . params ?. status === "review" ) {
484- action . params = { ...action . params , review_comment : resultText , commits : "" } ;
485- break ;
486- }
487- }
488- ui . info ( `[completion] Extracted completion from stream result (no COMPLETION_JSON)` ) ;
489- taskLog . event ( "completion_parse" , { source : "stream_result" , review_comment : resultText . slice ( 0 , 200 ) } ) ;
490- break ;
491- }
492- } catch { /* skip */ }
493- }
494- }
495- for ( const line of runningAgent . stdout ) {
496- const completionLine = line . startsWith ( "COMPLETION_JSON:" ) ? line : null ;
497- if ( completionLine ) {
498- try {
499- const json = JSON . parse ( completionLine . slice ( "COMPLETION_JSON:" . length ) ) ;
500- // Inject review_comment and commits into the update_task post_action params
501- for ( const action of agentTemplate . post_actions ) {
502- if ( action . type === "update_task" && action . when === "success" && action . params ?. status === "review" ) {
503- action . params = { ...action . params , review_comment : json . review_comment , commits : json . commits } ;
504- break ;
505- }
506- }
507- actionCtx . completionJson = { review_comment : json . review_comment , commits : json . commits } ;
508- ui . info ( `[completion] Parsed COMPLETION_JSON: ${ json . review_comment ?. slice ( 0 , 80 ) } ...` ) ;
509- taskLog . event ( "completion_parse" , { source : "completion_json" , review_comment : json . review_comment ?. slice ( 0 , 200 ) , commits : json . commits } ) ;
510- // Broadcast review comment immediately for real-time dashboard update
511- if ( json . review_comment ) {
512- actionCtx . onReviewUpdate ?.( task . id , "agent_submitted" , json . review_comment ) ;
513- }
514- break ;
515- } catch { /* skip */ }
460+ const parsed = extractCompletionJson ( runningAgent . stdout , agentTemplate . post_actions , {
461+ onReviewUpdate : ( tid , phase , comment ) => actionCtx . onReviewUpdate ?.( tid , phase , comment ) ,
462+ taskId : task . id ,
463+ taskLog,
464+ } ) ;
465+ if ( parsed ) {
466+ actionCtx . completionJson = parsed ;
516467 }
517- // Also try stream-json events
518- try {
519- const event = JSON . parse ( line ) ;
520- if ( event . type === "result" && typeof event . result === "string" ) {
521- const match = event . result . match ( / C O M P L E T I O N _ J S O N : ( \{ [ \s \S ] * \} ) / ) ;
522- if ( match ) {
523- const json = JSON . parse ( match [ 1 ] ) ;
524- for ( const action of agentTemplate . post_actions ) {
525- if ( action . type === "update_task" && action . when === "success" && action . params ?. status === "review" ) {
526- action . params = { ...action . params , review_comment : json . review_comment , commits : json . commits } ;
527- break ;
528- }
529- }
530- actionCtx . completionJson = { review_comment : json . review_comment , commits : json . commits } ;
531- ui . info ( `[completion] Parsed COMPLETION_JSON from stream: ${ json . review_comment ?. slice ( 0 , 80 ) } ...` ) ;
532- // Broadcast review comment immediately for real-time dashboard update
533- if ( json . review_comment ) {
534- actionCtx . onReviewUpdate ?.( task . id , "agent_submitted" , json . review_comment ) ;
535- }
536- break ;
537- }
538- }
539- } catch { /* not JSON */ }
540468 }
541469
542470 actionCtx . onRetro = async ( ) => {
0 commit comments