@@ -196,6 +196,7 @@ async def create_agent_spec(
196196 timeout_seconds = spec_data .timeout_seconds ,
197197 parent_spec_id = spec_data .parent_spec_id ,
198198 source_feature_id = spec_data .source_feature_id ,
199+ spec_path = spec_data .spec_path ,
199200 priority = spec_data .priority ,
200201 tags = spec_data .tags ,
201202 created_at = created_at ,
@@ -277,6 +278,7 @@ async def create_agent_spec(
277278 timeout_seconds = spec_dict ["timeout_seconds" ],
278279 parent_spec_id = spec_dict ["parent_spec_id" ],
279280 source_feature_id = spec_dict ["source_feature_id" ],
281+ spec_path = spec_dict .get ("spec_path" ),
280282 created_at = spec_dict ["created_at" ],
281283 priority = spec_dict ["priority" ],
282284 tags = spec_dict ["tags" ],
@@ -593,6 +595,7 @@ async def get_agent_spec(
593595 timeout_seconds = spec_dict ["timeout_seconds" ],
594596 parent_spec_id = spec_dict ["parent_spec_id" ],
595597 source_feature_id = spec_dict ["source_feature_id" ],
598+ spec_path = spec_dict .get ("spec_path" ),
596599 created_at = spec_dict ["created_at" ],
597600 priority = spec_dict ["priority" ],
598601 tags = spec_dict ["tags" ],
@@ -615,10 +618,16 @@ async def _execute_spec_background(
615618 project_name : str ,
616619) -> None :
617620 """
618- Background task to execute an AgentSpec.
621+ Background task to execute an AgentSpec via HarnessKernel .
619622
620- This is a placeholder implementation that will be replaced with the
621- actual HarnessKernel execution when it's implemented.
623+ Feature #136: Wire execute endpoint to actually call HarnessKernel.execute().
624+
625+ This function:
626+ 1. Transitions the pre-created AgentRun to 'running' status
627+ 2. Broadcasts WebSocket notification (Feature #61)
628+ 3. Invokes HarnessKernel.execute(spec) for real kernel execution
629+ 4. Syncs results from the kernel's run back to the pre-created run
630+ 5. Handles errors gracefully, updating run status to 'failed' on failure
622631
623632 Args:
624633 project_dir: Path to the project directory
@@ -641,6 +650,13 @@ async def _execute_spec_background(
641650
642651 # Get the spec for display_name and icon
643652 spec = db .query (AgentSpecModel ).filter (AgentSpecModel .id == spec_id ).first ()
653+ if not spec :
654+ _logger .error (f"AgentSpec { spec_id } not found for run { run_id } " )
655+ run .status = "failed"
656+ run .completed_at = _utc_now ()
657+ run .error = f"AgentSpec '{ spec_id } ' not found"
658+ db .commit ()
659+ return
644660 display_name = spec .display_name if spec else f"Run { run_id [:8 ]} "
645661 icon = spec .icon if spec else None
646662
@@ -664,24 +680,81 @@ async def _execute_spec_background(
664680 started_at = run .started_at ,
665681 )
666682
667- # TODO: This is where HarnessKernel.execute(spec) will be called
668- # For now, we just log that execution would happen here
669- _logger .info (f"[PLACEHOLDER] Would execute HarnessKernel for spec { spec_id } " )
683+ # Phase 2: Execute via HarnessKernel (Feature #136)
684+ # Use a separate DB session for the kernel execution to ensure
685+ # proper transaction isolation and commit behavior
686+ from api .harness_kernel import HarnessKernel
687+
688+ with get_db_session (project_dir ) as kernel_db :
689+ # Load the spec in the kernel's session with acceptance_spec eagerly loaded
690+ kernel_spec = kernel_db .query (AgentSpecModel ).options (
691+ joinedload (AgentSpecModel .acceptance_spec )
692+ ).filter (AgentSpecModel .id == spec_id ).first ()
693+
694+ if not kernel_spec :
695+ raise RuntimeError (f"AgentSpec '{ spec_id } ' not found in kernel session" )
696+
697+ _logger .info (f"Invoking HarnessKernel.execute() for spec { spec_id } " )
698+
699+ # Create kernel and execute the spec
700+ # The kernel creates its own AgentRun internally and manages
701+ # the full execution lifecycle (turns, events, acceptance, verdict)
702+ kernel = HarnessKernel (db = kernel_db )
703+ kernel_run = kernel .execute (
704+ kernel_spec ,
705+ turn_executor = None , # No Claude SDK executor yet; completes immediately
706+ context = {
707+ "project_dir" : str (project_dir ),
708+ },
709+ )
670710
671- # Simulate a small delay for demonstration purposes
672- # In production, this is where the actual kernel execution happens
673- await asyncio .sleep (0.1 )
711+ _logger .info (
712+ f"HarnessKernel execution completed: kernel_run={ kernel_run .id } , "
713+ f"status={ kernel_run .status } , verdict={ kernel_run .final_verdict } , "
714+ f"turns={ kernel_run .turns_used } "
715+ )
674716
675- # Note: In the real implementation, the kernel will:
676- # 1. Build system prompt from spec
677- # 2. Execute via Claude SDK
678- # 3. Record events for each turn
679- # 4. Run acceptance validators
680- # 5. Update final status and verdict
717+ # Capture kernel run results before session closes
718+ kernel_status = kernel_run .status
719+ kernel_completed_at = kernel_run .completed_at
720+ kernel_turns_used = kernel_run .turns_used
721+ kernel_tokens_in = kernel_run .tokens_in
722+ kernel_tokens_out = kernel_run .tokens_out
723+ kernel_final_verdict = kernel_run .final_verdict
724+ kernel_acceptance_results = kernel_run .acceptance_results
725+ kernel_error = kernel_run .error
726+ kernel_retry_count = kernel_run .retry_count
727+
728+ # Phase 3: Sync kernel results back to the pre-created run
729+ # The endpoint returned run_id to the client, so we update that
730+ # record with the actual execution results from HarnessKernel
731+ with get_db_session (project_dir ) as sync_db :
732+ original_run = sync_db .query (AgentRunModel ).filter (
733+ AgentRunModel .id == run_id
734+ ).first ()
735+
736+ if original_run :
737+ original_run .status = kernel_status
738+ original_run .completed_at = kernel_completed_at or _utc_now ()
739+ original_run .turns_used = kernel_turns_used
740+ original_run .tokens_in = kernel_tokens_in
741+ original_run .tokens_out = kernel_tokens_out
742+ original_run .final_verdict = kernel_final_verdict
743+ original_run .acceptance_results = kernel_acceptance_results
744+ original_run .error = kernel_error
745+ original_run .retry_count = kernel_retry_count
746+ sync_db .commit ()
747+
748+ _logger .info (
749+ f"Synced kernel results to original run { run_id } : "
750+ f"status={ kernel_status } , verdict={ kernel_final_verdict } "
751+ )
752+ else :
753+ _logger .warning (f"Original run { run_id } not found during result sync" )
681754
682755 except Exception as e :
683756 _logger .exception (f"Error executing spec { spec_id } : { e } " )
684- # Mark run as failed
757+ # Mark run as failed with error details
685758 try :
686759 with get_db_session (project_dir ) as db :
687760 run = db .query (AgentRunModel ).filter (AgentRunModel .id == run_id ).first ()
@@ -757,7 +830,7 @@ async def execute_agent_spec(
757830 The execution will:
758831 1. Validate AgentSpec (Feature #78, Steps 1-4)
759832 2. Transition the run to 'running' status
760- 3. Execute the spec via HarnessKernel (when implemented )
833+ 3. Execute the spec via HarnessKernel (Feature #136 )
761834 4. Record events for each tool call and turn
762835 5. Run acceptance validators
763836 6. Update final status (completed/failed/timeout)
@@ -1074,6 +1147,7 @@ async def update_agent_spec(
10741147 timeout_seconds = spec_dict ["timeout_seconds" ],
10751148 parent_spec_id = spec_dict ["parent_spec_id" ],
10761149 source_feature_id = spec_dict ["source_feature_id" ],
1150+ spec_path = spec_dict .get ("spec_path" ),
10771151 created_at = spec_dict ["created_at" ],
10781152 priority = spec_dict ["priority" ],
10791153 tags = spec_dict ["tags" ],
0 commit comments