@@ -9,6 +9,7 @@ import { outboundPublish, type OutboundApproval, type OutboundMedia, type Outbou
99import { sanitizeOutboundPostText } from './outbound-sanitize' ;
1010import { loadPriorLlmInput , loadProposedPostTextFromPriorNode } from './workflow-node-output-readers' ;
1111import { readTextFile } from './workflow-runner-io' ;
12+ import { evalIfCondition , lastIfValueFromRun } from './workflow-if' ;
1213import {
1314 asRecord , asString ,
1415 ensureDir , fileExists ,
@@ -75,6 +76,14 @@ export async function executeWorkflowNodes(opts: {
7576 const from = nodeStates [ fromId ] ?. status ;
7677 const on = String ( e . on ?? 'success' ) ;
7778 if ( ! from ) return false ;
79+
80+ if ( on === 'true' || on === 'false' ) {
81+ if ( from !== 'success' ) return false ;
82+ const v = lastIfValueFromRun ( curRun , fromId ) ;
83+ if ( v === null ) return false ;
84+ return on === 'true' ? v === true : v === false ;
85+ }
86+
7887 if ( on === 'always' ) return from === 'success' || from === 'error' ;
7988 if ( on === 'error' ) return from === 'error' ;
8089 return from === 'success' ;
@@ -161,6 +170,68 @@ export async function executeWorkflowNodes(opts: {
161170 continue ;
162171 }
163172
173+ if ( kind === 'if' ) {
174+ const runDir = path . dirname ( runLogPath ) ;
175+ const action = asRecord ( node . action ) ;
176+ const lhs = asString ( action [ 'lhs' ] ) . trim ( ) ;
177+ const op = asString ( action [ 'op' ] ) . trim ( ) ;
178+ const rhs = action [ 'rhs' ] ;
179+ if ( ! lhs ) throw new Error ( `Node ${ nodeLabel ( node ) } missing action.lhs` ) ;
180+ if ( ! op ) throw new Error ( `Node ${ nodeLabel ( node ) } missing action.op` ) ;
181+
182+ const evalRes = await evalIfCondition ( { runDir, condition : { lhs, op : op as 'truthy' , rhs } } ) ;
183+
184+ const defaultNodeOutputRel = path . join ( 'node-outputs' , `${ String ( i ) . padStart ( 3 , '0' ) } -${ node . id } .json` ) ;
185+ const nodeOutputRel = String ( node ?. output ?. path ?? '' ) . trim ( ) || defaultNodeOutputRel ;
186+ const nodeOutputAbs = path . resolve ( path . dirname ( runLogPath ) , nodeOutputRel ) ;
187+ await ensureDir ( path . dirname ( nodeOutputAbs ) ) ;
188+ await fs . writeFile ( nodeOutputAbs , JSON . stringify ( {
189+ runId, teamId, nodeId : node . id , kind : node . kind ,
190+ completedAt : new Date ( ) . toISOString ( ) , value : evalRes . value , detail : evalRes . detail ,
191+ } , null , 2 ) + '\n' , 'utf8' ) ;
192+
193+ const completedTs = new Date ( ) . toISOString ( ) ;
194+ await appendRunLog ( runLogPath , ( cur ) => ( {
195+ ...cur ,
196+ nextNodeIndex : i + 1 ,
197+ nodeStates : { ...( cur . nodeStates ?? { } ) , [ node . id ] : { status : 'success' , ts : completedTs } } ,
198+ events : [ ...cur . events , { ts : completedTs , type : 'node.completed' , nodeId : node . id , kind, value : evalRes . value , nodeOutputPath : path . relative ( teamDir , nodeOutputAbs ) } ] ,
199+ nodeResults : [ ...( cur . nodeResults ?? [ ] ) , { nodeId : node . id , kind, value : evalRes . value , nodeOutputPath : path . relative ( teamDir , nodeOutputAbs ) } ] ,
200+ } ) ) ;
201+ nodeStates [ String ( node . id ) ] = { status : 'success' , ts : completedTs } ;
202+ continue ;
203+ }
204+
205+ if ( kind === 'delay' ) {
206+ const action = asRecord ( node . action ) ;
207+ const secondsRaw = action [ 'seconds' ] ?? action [ 'delaySeconds' ] ?? action [ 'durationSeconds' ] ;
208+ const msRaw = action [ 'ms' ] ?? action [ 'delayMs' ] ?? action [ 'durationMs' ] ;
209+ const sec = typeof secondsRaw === 'number' ? secondsRaw : Number ( secondsRaw ) ;
210+ const ms = typeof msRaw === 'number' ? msRaw : Number ( msRaw ) ;
211+ const delayMs = Number . isFinite ( ms ) && ms > 0 ? ms : Number . isFinite ( sec ) && sec > 0 ? sec * 1000 : 0 ;
212+ if ( ! delayMs ) throw new Error ( `Node ${ nodeLabel ( node ) } missing delay duration (action.delaySeconds or action.delayMs)` ) ;
213+
214+ const maxDelayMs = 7 * 24 * 60 * 60 * 1000 ;
215+ const effectiveDelayMs = Math . min ( delayMs , maxDelayMs ) ;
216+ const resumeAt = new Date ( Date . now ( ) + effectiveDelayMs ) . toISOString ( ) ;
217+
218+ const completedTs = new Date ( ) . toISOString ( ) ;
219+ await appendRunLog ( runLogPath , ( cur ) => ( {
220+ ...cur ,
221+ status : 'paused' ,
222+ resumeAt,
223+ nextNodeIndex : i + 1 ,
224+ nodeStates : { ...( cur . nodeStates ?? { } ) , [ node . id ] : { status : 'success' , ts : completedTs } } ,
225+ events : [
226+ ...cur . events ,
227+ { ts : completedTs , type : 'node.completed' , nodeId : node . id , kind, delayMs : effectiveDelayMs , resumeAt } ,
228+ { ts : completedTs , type : 'run.paused' , nodeId : node . id , resumeAt } ,
229+ ] ,
230+ nodeResults : [ ...( cur . nodeResults ?? [ ] ) , { nodeId : node . id , kind, delayMs : effectiveDelayMs , resumeAt } ] ,
231+ } ) ) ;
232+ nodeStates [ String ( node . id ) ] = { status : 'success' , ts : completedTs } ;
233+ return { ticketPath : curTicketPath , lane : curLane , status : 'completed' } ;
234+ }
164235
165236 if ( kind === 'llm' ) {
166237 const agentId = String ( node ?. assignedTo ?. agentId ?? '' ) ;
0 commit comments