From 8bc36b6e4bdb6b1fa3f8c1414d12d4def98af2cc Mon Sep 17 00:00:00 2001 From: liberxue <5831248+Liberxue@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:17:38 +0800 Subject: [PATCH] fix(orchestrator): keep task executor through revision loop; harden judge wiring - DeliveryProgress::Delivered now carries the task's executor kind; Reviser::revise receives it, so ExecutorReviser re-dispatches with the original kind instead of hardcoding ExecutorKind::ClaudeCode (unknown kinds still fall back to Claude) - HttpJudge gets 30s/10s request/connect timeouts, matching LlmClient/JevJudge, so a hung SHEPHERD_JUDGE_URL cannot stall the verification gate forever - build_judge() logs a warning when it falls back to AcceptAllJudge so an unconfigured production gate is visible instead of silently passing everything --- .../src/application/delivery_feedback.rs | 56 +++++++++++++++---- crates/orchestrator/src/ports/mod.rs | 1 + crates/server/src/judge.rs | 11 +++- crates/server/src/orchestration.rs | 10 +++- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/crates/orchestrator/src/application/delivery_feedback.rs b/crates/orchestrator/src/application/delivery_feedback.rs index 73baec52..296e0179 100644 --- a/crates/orchestrator/src/application/delivery_feedback.rs +++ b/crates/orchestrator/src/application/delivery_feedback.rs @@ -8,7 +8,7 @@ use crate::ports::{ #[derive(Debug, Clone, PartialEq, Eq)] pub enum DeliveryProgress { Running, - Delivered { deliverable: DeliverableView }, + Delivered { deliverable: DeliverableView, executor: String }, Failed, } @@ -71,7 +71,7 @@ impl DeliveryFeedbackOrchestrator { self.task.task_criteria(decomposition_id, task_id).await.unwrap_or_default(); (Some(TaskTarget::Failed), Some(false), None, criteria) } - DeliveryProgress::Delivered { deliverable } => { + DeliveryProgress::Delivered { deliverable, executor } => { let _ = self.task.advance_task(decomposition_id, task_id, TaskTarget::Delivered).await; let criteria = self.task.task_criteria(decomposition_id, task_id).await?; @@ -80,7 +80,14 @@ impl DeliveryFeedbackOrchestrator { if let Some(reviser) = &self.reviser { while !v.passed && revisions < self.max_revisions { match reviser - .revise(decomposition_id, task_id, &criteria, ¤t, &v.reason) + .revise( + decomposition_id, + task_id, + &criteria, + ¤t, + &v.reason, + &executor, + ) .await { Ok(next) => { @@ -229,7 +236,10 @@ mod tests { .on_progress( "d1", "t1", - DeliveryProgress::Delivered { deliverable: dv("branch:x", "done") }, + DeliveryProgress::Delivered { + deliverable: dv("branch:x", "done"), + executor: "CLAUDE_CODE".into(), + }, ) .await .expect("ok"); @@ -259,9 +269,16 @@ mod tests { }); let orch = DeliveryFeedbackOrchestrator::new(task, verif.clone(), Arc::new(AcceptAllJudge)); - orch.on_progress("d1", "t1", DeliveryProgress::Delivered { deliverable: dv("b", "d") }) - .await - .expect("ok"); + orch.on_progress( + "d1", + "t1", + DeliveryProgress::Delivered { + deliverable: dv("b", "d"), + executor: "CLAUDE_CODE".into(), + }, + ) + .await + .expect("ok"); let linked = verif.linked.lock().unwrap_or_else(std::sync::PoisonError::into_inner); assert_eq!(linked.len(), 1); assert_eq!(linked[0], ("v1/t1".to_string(), vec!["login success".to_string()])); @@ -283,7 +300,10 @@ mod tests { .on_progress( "d1", "t1", - DeliveryProgress::Delivered { deliverable: dv("branch:x", "") }, + DeliveryProgress::Delivered { + deliverable: dv("branch:x", ""), + executor: "CLAUDE_CODE".into(), + }, ) .await .expect("ok"); @@ -310,7 +330,14 @@ mod tests { }); let orch = DeliveryFeedbackOrchestrator::new(task.clone(), verif, Arc::new(AcceptAllJudge)); let out = orch - .on_progress("d1", "t1", DeliveryProgress::Delivered { deliverable: dv("", "") }) + .on_progress( + "d1", + "t1", + DeliveryProgress::Delivered { + deliverable: dv("", ""), + executor: "CLAUDE_CODE".into(), + }, + ) .await .expect("ok"); assert!(out.verdict.unwrap().passed); @@ -338,6 +365,7 @@ mod tests { _c: &[String], _prev: &DeliverableView, _feedback: &str, + _executor: &str, ) -> Result { let mut n = self.calls.lock().unwrap_or_else(std::sync::PoisonError::into_inner); *n += 1; @@ -365,7 +393,10 @@ mod tests { .on_progress( "d1", "t1", - DeliveryProgress::Delivered { deliverable: dv("branch:x", "") }, + DeliveryProgress::Delivered { + deliverable: dv("branch:x", ""), + executor: "CLAUDE_CODE".into(), + }, ) .await .expect("ok"); @@ -407,7 +438,10 @@ mod tests { .on_progress( "d1", "t1", - DeliveryProgress::Delivered { deliverable: dv("branch:x", "") }, + DeliveryProgress::Delivered { + deliverable: dv("branch:x", ""), + executor: "CLAUDE_CODE".into(), + }, ) .await .expect("ok"); diff --git a/crates/orchestrator/src/ports/mod.rs b/crates/orchestrator/src/ports/mod.rs index 83e51c61..8519a348 100644 --- a/crates/orchestrator/src/ports/mod.rs +++ b/crates/orchestrator/src/ports/mod.rs @@ -63,6 +63,7 @@ pub trait Reviser: Send + Sync { criteria: &[String], previous: &DeliverableView, feedback: &str, + executor: &str, ) -> Result; } diff --git a/crates/server/src/judge.rs b/crates/server/src/judge.rs index ec8174da..f618c0ee 100644 --- a/crates/server/src/judge.rs +++ b/crates/server/src/judge.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::Duration; use async_trait::async_trait; use serde::Deserialize; @@ -54,10 +55,18 @@ pub fn build_judge() -> Arc { Ok(url) if !url.trim().is_empty() => { let client = reqwest::Client::builder() .no_proxy() + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(10)) .build() .unwrap_or_else(|_| reqwest::Client::new()); Arc::new(HttpJudge { client, url }) } - _ => Arc::new(AcceptAllJudge), + _ => { + tracing::warn!( + "no verification judge configured (SHEPHERD_JEV_API_KEY / SHEPHERD_LLM_URL / \ + SHEPHERD_JUDGE_URL unset): the delivery gate will accept every deliverable" + ); + Arc::new(AcceptAllJudge) + } } } diff --git a/crates/server/src/orchestration.rs b/crates/server/src/orchestration.rs index 88286931..b6f442ac 100644 --- a/crates/server/src/orchestration.rs +++ b/crates/server/src/orchestration.rs @@ -126,7 +126,10 @@ impl Reviser for ExecutorReviser { criteria: &[String], previous: &DeliverableView, feedback: &str, + executor: &str, ) -> Result { + // Keep the task on its original executor kind; unknown kinds fall back to Claude. + let kind = ExecutorKind::parse(executor).unwrap_or(ExecutorKind::ClaudeCode); let spec = WorkSpec { // Revision finishes synchronously, no async callback, so attempt_id stays empty. attempt_id: String::new(), @@ -135,7 +138,7 @@ impl Reviser for ExecutorReviser { title: format!("修订任务 {task_id}"), description: previous.summary.clone(), acceptance_criteria: criteria.to_vec(), - executor: ExecutorKind::ClaudeCode, + executor: kind, context: None, instructions: Some(format!( "上一轮交付未通过验证门,请据反馈修正后重做。\n反馈: {feedback}" @@ -206,7 +209,10 @@ impl DeliveryObserver for OrchestratorObserver { reference: String::new(), summary: String::new(), }); - DeliveryProgress::Delivered { deliverable } + DeliveryProgress::Delivered { + deliverable, + executor: attempt.executor.as_str().to_string(), + } } AttemptStatus::Failed => DeliveryProgress::Failed, // Dispatched-but-not-started / deliberately stopped: don't drive the verification gate.