@@ -959,26 +959,170 @@ export function runNext(slug: string): OrchestratorAction {
959959 }
960960
961961 // Consistency check: verify all stages before active_stage are completed.
962- // If not, reset to the first incomplete stage. This catches stale active_stage
963- // values set by old binaries or direct file edits.
962+ // If not, either synthesize completion records (safe repair) or reset to
963+ // the first incomplete stage. Safe repair triggers when the active stage
964+ // has real work (units) — this indicates a migrated intent where earlier
965+ // stages were never elaborated. Resetting backwards would force
966+ // re-elaboration of empty stages while real work sits in a later stage.
964967 const activeIdx = studioStages . indexOf ( currentStage )
965968 if ( activeIdx > 0 ) {
969+ // Collect all incomplete prior stages in one pass
970+ const incompletePrior : string [ ] = [ ]
966971 for ( let i = 0 ; i < activeIdx ; i ++ ) {
967972 const prevState = readJson (
968973 join ( iDir , "stages" , studioStages [ i ] , "state.json" ) ,
969974 )
970975 const prevStatus = ( prevState . status as string ) || "pending"
971976 if ( prevStatus !== "completed" ) {
972- // Found an incomplete stage before active_stage — reset
973- currentStage = studioStages [ i ]
974- // Fix the intent's active_stage to match reality
977+ incompletePrior . push ( studioStages [ i ] )
978+ }
979+ }
980+
981+ if ( incompletePrior . length > 0 ) {
982+ // Check if the active stage has real work — units on disk
983+ const activeUnitsDir = join ( iDir , "stages" , currentStage , "units" )
984+ const activeUnitFiles = existsSync ( activeUnitsDir )
985+ ? readdirSync ( activeUnitsDir ) . filter ( ( f ) => f . endsWith ( ".md" ) )
986+ : [ ]
987+
988+ if ( activeUnitFiles . length > 0 ) {
989+ // ── Safe intent repair ──────────────────────────────────────
990+ // The active stage has real work but earlier stages are incomplete.
991+ // This is a migration artifact (e.g., AIDLC → H·AI·K·U migration
992+ // that only populated the development stage). Synthesize completion
993+ // records for incomplete prior stages so the FSM can proceed without
994+ // forcing re-elaboration of empty stages.
995+ //
996+ // Safety constraints:
997+ // 1. Only synthesizes for stages with NO units (truly empty)
998+ // — stages with units but incomplete status are left for manual review
999+ // 2. Uses the same completion record format as haiku_repair
1000+ // 3. The agent cannot trigger this — it's FSM-internal
1001+ // 4. No hook bypass — this runs inside haiku_run_next
1002+
1003+ const synthesized : string [ ] = [ ]
1004+ const needsManualReview : string [ ] = [ ]
1005+ const now = timestamp ( )
1006+ const intentStarted =
1007+ ( intent . started_at as string ) || ( intent . created_at as string ) || now
1008+
1009+ for ( const stageName of incompletePrior ) {
1010+ const priorUnitsDir = join ( iDir , "stages" , stageName , "units" )
1011+ const priorUnitFiles = existsSync ( priorUnitsDir )
1012+ ? readdirSync ( priorUnitsDir ) . filter ( ( f ) => f . endsWith ( ".md" ) )
1013+ : [ ]
1014+
1015+ if ( priorUnitFiles . length > 0 ) {
1016+ // Stage has units but isn't completed — this needs manual attention
1017+ needsManualReview . push ( stageName )
1018+ } else {
1019+ // Truly empty prior stage — safe to synthesize completion
1020+ const stageDir = join ( iDir , "stages" , stageName )
1021+ mkdirSync ( stageDir , { recursive : true } )
1022+ const statePath = join ( stageDir , "state.json" )
1023+ writeJson ( statePath , {
1024+ stage : stageName ,
1025+ status : "completed" ,
1026+ phase : "gate" ,
1027+ started_at : intentStarted ,
1028+ completed_at : intentStarted ,
1029+ gate_entered_at : null ,
1030+ gate_outcome : "advanced" ,
1031+ } )
1032+ synthesized . push ( stageName )
1033+ }
1034+ }
1035+
1036+ // Check if the active stage's units need input backfill.
1037+ // If the stage is in execute phase but units lack inputs, regress
1038+ // to elaborate so the normal backpressure can enforce input declarations.
1039+ const activeStageState = readJson (
1040+ join ( iDir , "stages" , currentStage , "state.json" ) ,
1041+ )
1042+ const activePhase = ( activeStageState . phase as string ) || ""
1043+ let phaseRegressed = false
1044+ const missingInputs : string [ ] = [ ]
1045+ if ( activePhase === "execute" ) {
1046+ for ( const f of activeUnitFiles ) {
1047+ const fm = readFrontmatter ( join ( activeUnitsDir , f ) )
1048+ const unitStatus = ( fm . status as string ) || ""
1049+ if ( [ "completed" , "skipped" , "failed" ] . includes ( unitStatus ) )
1050+ continue
1051+ const inputs =
1052+ ( fm . inputs as string [ ] ) || ( fm . refs as string [ ] ) || [ ]
1053+ if ( inputs . length === 0 ) missingInputs . push ( f )
1054+ }
1055+ if ( missingInputs . length > 0 ) {
1056+ // Regress phase to elaborate so validateUnitInputs catches this
1057+ activeStageState . phase = "elaborate"
1058+ writeJson (
1059+ join ( iDir , "stages" , currentStage , "state.json" ) ,
1060+ activeStageState ,
1061+ )
1062+ phaseRegressed = true
1063+ }
1064+ }
1065+
1066+ if ( synthesized . length > 0 || phaseRegressed ) {
1067+ gitCommitState (
1068+ `haiku: safe-repair ${ slug } — synthesize ${ synthesized . join ( ", " ) } ${ phaseRegressed ? "; regress phase to elaborate" : "" } ` ,
1069+ )
1070+ }
1071+
1072+ emitTelemetry ( "haiku.fsm.safe_repair" , {
1073+ intent : slug ,
1074+ active_stage : currentStage ,
1075+ synthesized_stages : synthesized . join ( "," ) ,
1076+ needs_manual_review : needsManualReview . join ( "," ) ,
1077+ phase_regressed : String ( phaseRegressed ) ,
1078+ } )
1079+
1080+ // If all incomplete stages were synthesized, proceed normally
1081+ // by falling through to the rest of runNext. If any need manual
1082+ // review, return an action so the agent can report the situation.
1083+ if ( needsManualReview . length > 0 ) {
1084+ return {
1085+ action : "safe_intent_repair" ,
1086+ intent : slug ,
1087+ studio,
1088+ stage : currentStage ,
1089+ synthesized_stages : synthesized ,
1090+ needs_manual_review : needsManualReview ,
1091+ phase_regressed : phaseRegressed ,
1092+ units_missing_inputs : missingInputs ,
1093+ message : `Intent '${ slug } ' was in an inconsistent state — work exists in '${ currentStage } ' but earlier stages were incomplete.\n\n${ synthesized . length > 0 ? `Synthesized completion records for empty stages: [${ synthesized . join ( ", " ) } ]\n` : "" } Stages needing manual review (have units but aren't completed): [${ needsManualReview . join ( ", " ) } ]\n${ phaseRegressed ? `\nAdditionally, phase was regressed from 'execute' to 'elaborate' because some units are missing \`inputs:\` declarations.\n` : "" } Resolve these stages manually, then call haiku_run_next again.` ,
1094+ }
1095+ }
1096+
1097+ // All prior stages synthesized — if phase was regressed, let the
1098+ // agent know so it can address missing inputs before execution.
1099+ // Otherwise fall through to normal processing.
1100+ if ( phaseRegressed ) {
1101+ return {
1102+ action : "safe_intent_repair" ,
1103+ intent : slug ,
1104+ studio,
1105+ stage : currentStage ,
1106+ synthesized_stages : synthesized ,
1107+ needs_manual_review : [ ] ,
1108+ phase_regressed : true ,
1109+ units_missing_inputs : missingInputs ,
1110+ message : `Intent '${ slug } ' repaired — synthesized completion for [${ synthesized . join ( ", " ) } ]. Phase regressed from 'execute' to 'elaborate' because some units are missing \`inputs:\` declarations. Add inputs to the flagged units, then call haiku_run_next to proceed.` ,
1111+ }
1112+ }
1113+
1114+ // Clean repair with no phase regression — fall through to normal
1115+ // runNext processing. The agent doesn't need to take special action.
1116+ } else {
1117+ // No units in the active stage — normal consistency reset.
1118+ // The intent may have been corrupted or active_stage set incorrectly.
1119+ currentStage = incompletePrior [ 0 ]
9751120 setFrontmatterField ( intentFile , "active_stage" , currentStage )
9761121 emitTelemetry ( "haiku.fsm.consistency_fix" , {
9771122 intent : slug ,
9781123 stale_stage : activeStage ,
9791124 corrected_stage : currentStage ,
9801125 } )
981- break
9821126 }
9831127 }
9841128 }
@@ -3067,6 +3211,24 @@ function buildRunInstructions(
30673211 break
30683212 }
30693213
3214+ case "safe_intent_repair" : {
3215+ const synthesizedStages = ( action . synthesized_stages as string [ ] ) || [ ]
3216+ const phaseWasRegressed = ( action . phase_regressed as boolean ) || false
3217+ sections . push ( `## Safe Intent Repair\n\n${ action . message } ` )
3218+ if ( synthesizedStages . length > 0 ) {
3219+ sections . push ( `**Synthesized stages:** ${ synthesizedStages . join ( ", " ) } ` )
3220+ }
3221+ if ( phaseWasRegressed ) {
3222+ sections . push (
3223+ "**Phase regressed:** The active stage was regressed from `execute` to `elaborate` because some units are missing `inputs:` declarations. Address the missing inputs before proceeding." ,
3224+ )
3225+ }
3226+ sections . push (
3227+ `### Instructions\n\nResolve any stages needing manual review, then call \`haiku_run_next { intent: "${ slug } " }\` again.` ,
3228+ )
3229+ break
3230+ }
3231+
30703232 default : {
30713233 sections . push (
30723234 `## Unknown Action: ${ action . action } \n\n${ JSON . stringify ( action , null , 2 ) } ` ,
@@ -3682,6 +3844,82 @@ export async function handleOrchestratorTool(
36823844 }
36833845 }
36843846
3847+ // ── Repair agent intercept ─────────────────────────────────────────
3848+ // If runNext detected a broken migrated intent, try the embedded repair
3849+ // agent before returning to the outer agent. Falls through to the normal
3850+ // withInstructions return if the agent isn't available or repair fails.
3851+ if ( result . action === "safe_intent_repair" ) {
3852+ try {
3853+ const { runRepairAgent } = await import ( "./repair-agent.js" )
3854+ const root = findHaikuRoot ( )
3855+ const iDir = join ( root , "intents" , slug )
3856+
3857+ // Resolve studio directory via the cached studio reader
3858+ const studioInfo = resolveStudio ( intentStudio )
3859+ const studioDir = studioInfo ?. path
3860+ if ( ! studioDir ) {
3861+ // Can't find studio — fall through to normal handling
3862+ syncSessionMetadata ( slug , args . state_file as string | undefined )
3863+ return text ( withInstructions ( result ) )
3864+ }
3865+
3866+ const activeStage = ( result . stage as string ) || ""
3867+ const diagnosis = {
3868+ slug,
3869+ intentDir : iDir ,
3870+ studio : intentStudio ,
3871+ studioDir,
3872+ activeStage,
3873+ synthesizedStages : ( result . synthesized_stages as string [ ] ) || [ ] ,
3874+ needsManualReview : ( result . needs_manual_review as string [ ] ) || [ ] ,
3875+ phaseRegressed : ( result . phase_regressed as boolean ) || false ,
3876+ unitsMissingInputs : ( result . units_missing_inputs as string [ ] ) || [ ] ,
3877+ }
3878+
3879+ const repairResult = await runRepairAgent ( diagnosis )
3880+
3881+ if ( repairResult . success && ! repairResult . fallbackUsed ) {
3882+ // Repair agent succeeded — run FSM again to get the real next action
3883+ const postRepairResult = runNext ( slug )
3884+
3885+ // Guard: if repair didn't actually fix things, don't loop
3886+ if ( postRepairResult . action === "safe_intent_repair" ) {
3887+ // Fall through to return the original result as-is
3888+ } else {
3889+ emitTelemetry ( "haiku.orchestrator.action" , {
3890+ intent : slug ,
3891+ action : postRepairResult . action ,
3892+ } )
3893+ if ( stFile )
3894+ logSessionEvent ( stFile , {
3895+ event : "run_next" ,
3896+ intent : slug ,
3897+ action : postRepairResult . action ,
3898+ stage : postRepairResult . stage ,
3899+ unit : postRepairResult . unit ,
3900+ hat : postRepairResult . hat ,
3901+ wave : postRepairResult . wave ,
3902+ } )
3903+
3904+ syncSessionMetadata ( slug , args . state_file as string | undefined )
3905+
3906+ const repairNote = `**Intent repaired automatically:** ${ repairResult . summary } \n\n---\n\n`
3907+ return {
3908+ content : [
3909+ {
3910+ type : "text" as const ,
3911+ text : repairNote + withInstructions ( postRepairResult ) ,
3912+ } ,
3913+ ] ,
3914+ }
3915+ }
3916+ }
3917+ // Repair failed or used fallback — fall through to return safe_intent_repair as-is
3918+ } catch {
3919+ // Repair agent not available — fall through to normal handling
3920+ }
3921+ }
3922+
36853923 syncSessionMetadata ( slug , args . state_file as string | undefined )
36863924 return text ( withInstructions ( result ) )
36873925 }
0 commit comments