Orchestrator.run does not handle stage handler exceptions, leaving state inconsistent
Severity: High
File: src/cain_agent/orchestrator.py:147-153
The run method iterates through all stages and calls run_stage for each, but run_stage only saves state after a handler returns successfully:
def run(self) -> dict[str, Any]:
for stage in STAGES:
self.run_stage(stage)
return self.load_state()
If the "recon" handler raises an uncaught exception, run_stage exits before writing state.json — the completed stages list remains empty. The exception propagates up to run and then to the caller (cmd_run in cli.py), which catches it and calls orchestrator.load_state(). But the state shows no stages completed, so the summary prints nothing useful and partial artifacts from the failed stage are orphaned in the workspace with no record.
Why it matters
A handler crash during recon or test leaves the workspace in an ambiguous state: stage artifacts may exist on disk but state.json shows no progress. There is no mechanism to resume or diagnose which stage failed, and the partial results are invisible to the operator.
Orchestrator.rundoes not handle stage handler exceptions, leaving state inconsistentSeverity: High
File:
src/cain_agent/orchestrator.py:147-153The
runmethod iterates through all stages and callsrun_stagefor each, butrun_stageonly saves state after a handler returns successfully:If the "recon" handler raises an uncaught exception,
run_stageexits before writingstate.json— the completed stages list remains empty. The exception propagates up torunand then to the caller (cmd_runincli.py), which catches it and callsorchestrator.load_state(). But the state shows no stages completed, so the summary prints nothing useful and partial artifacts from the failed stage are orphaned in the workspace with no record.Why it matters
A handler crash during recon or test leaves the workspace in an ambiguous state: stage artifacts may exist on disk but
state.jsonshows no progress. There is no mechanism to resume or diagnose which stage failed, and the partial results are invisible to the operator.