diff --git a/Cargo.lock b/Cargo.lock index 2504aea..e2e2d24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,7 +141,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -152,7 +152,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1761,7 +1761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2113,9 +2113,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -2380,7 +2380,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3103,7 +3103,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3822,7 +3822,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.6.4", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -3860,7 +3860,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -4421,14 +4421,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", "once_cell", @@ -4463,9 +4463,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -4909,7 +4909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5445,7 +5445,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6252,7 +6252,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/crates/agent-runtime/src/backend.rs b/crates/agent-runtime/src/backend.rs index 83c313c..86042ef 100644 --- a/crates/agent-runtime/src/backend.rs +++ b/crates/agent-runtime/src/backend.rs @@ -250,6 +250,11 @@ impl GenericCliBackend { timeout, ) } + pub fn deepcode(timeout: Duration) -> Self { + // Headless one-shot; --json keeps the output machine-readable. The prompt + // is appended as the final argv by GenericCliBackend::execute. + Self::from_env("deepcode", "DEEPCODE_CMD", "deepcode exec --json", timeout) + } } #[async_trait] diff --git a/crates/agent-runtime/src/main.rs b/crates/agent-runtime/src/main.rs index b330bf3..cce3d20 100644 --- a/crates/agent-runtime/src/main.rs +++ b/crates/agent-runtime/src/main.rs @@ -117,6 +117,7 @@ fn backend_for(executor: &str, task_timeout: Duration) -> Arc Arc::new(GenericCliBackend::codex(task_timeout)), "OPENCODE" => Arc::new(GenericCliBackend::opencode(task_timeout)), "CODEBUDDY" => Arc::new(GenericCliBackend::codebuddy(task_timeout)), + "DEEPCODE" => Arc::new(GenericCliBackend::deepcode(task_timeout)), _ => Arc::new(ClaudeBackend::new(task_timeout)), } } diff --git a/crates/delivery/src/adapters/local.rs b/crates/delivery/src/adapters/local.rs index 34b46e9..c31af2e 100644 --- a/crates/delivery/src/adapters/local.rs +++ b/crates/delivery/src/adapters/local.rs @@ -36,9 +36,12 @@ impl LocalCommandAgentExecutor { match kind { ExecutorKind::ClaudeCode => &self.claude_code, ExecutorKind::Codex => &self.codex, - // The local path only configures two argvs; OpenCode/CodeBuddy deliberately - // fall back to the claude argv (real routing lives in crates/agent-runtime). - ExecutorKind::OpenCode | ExecutorKind::CodeBuddy => &self.claude_code, + // The local path only configures two argvs; OpenCode/CodeBuddy/DeepCode + // deliberately fall back to the claude argv (real routing lives in + // crates/agent-runtime). + ExecutorKind::OpenCode | ExecutorKind::CodeBuddy | ExecutorKind::DeepCode => { + &self.claude_code + } } } } diff --git a/crates/delivery/src/adapters/queue.rs b/crates/delivery/src/adapters/queue.rs index 63caff4..99370ea 100644 --- a/crates/delivery/src/adapters/queue.rs +++ b/crates/delivery/src/adapters/queue.rs @@ -22,7 +22,7 @@ use crate::ports::{ RuntimeInfo, WorkQueue, WorkSpec, }; -const KNOWN_CAPS: [ExecutorKind; 4] = ExecutorKind::ALL; +const KNOWN_CAPS: [ExecutorKind; 5] = ExecutorKind::ALL; #[derive(Default)] pub struct InMemoryWorkQueue { diff --git a/crates/delivery/src/adapters/redis_queue.rs b/crates/delivery/src/adapters/redis_queue.rs index 86db1d8..42ae4d6 100644 --- a/crates/delivery/src/adapters/redis_queue.rs +++ b/crates/delivery/src/adapters/redis_queue.rs @@ -25,7 +25,7 @@ const ACKMAP: &str = "fleet:ackmap"; const RT_INDEX: &str = "fleet:rt:index"; const SEP: char = '\u{1f}'; -fn known_caps() -> [ExecutorKind; 4] { +fn known_caps() -> [ExecutorKind; 5] { ExecutorKind::ALL } diff --git a/crates/delivery/src/domain/delivery.rs b/crates/delivery/src/domain/delivery.rs index c6b6771..d003ce3 100644 --- a/crates/delivery/src/domain/delivery.rs +++ b/crates/delivery/src/domain/delivery.rs @@ -12,13 +12,14 @@ pub enum ExecutorKind { Codex, OpenCode, CodeBuddy, + DeepCode, } impl ExecutorKind { /// Single authoritative list of executor kinds; queue adapters and tests consume /// it, so a new variant only touches this file. - pub const ALL: [ExecutorKind; 4] = - [Self::ClaudeCode, Self::Codex, Self::OpenCode, Self::CodeBuddy]; + pub const ALL: [ExecutorKind; 5] = + [Self::ClaudeCode, Self::Codex, Self::OpenCode, Self::CodeBuddy, Self::DeepCode]; pub fn as_str(&self) -> &'static str { match self { @@ -26,6 +27,7 @@ impl ExecutorKind { Self::Codex => "CODEX", Self::OpenCode => "OPENCODE", Self::CodeBuddy => "CODEBUDDY", + Self::DeepCode => "DEEPCODE", } } @@ -35,6 +37,7 @@ impl ExecutorKind { "CODEX" => Some(Self::Codex), "OPENCODE" => Some(Self::OpenCode), "CODEBUDDY" => Some(Self::CodeBuddy), + "DEEPCODE" => Some(Self::DeepCode), _ => None, } } diff --git a/crates/server/src/jev.rs b/crates/server/src/jev.rs new file mode 100644 index 0000000..a0afb9b --- /dev/null +++ b/crates/server/src/jev.rs @@ -0,0 +1,245 @@ +//! Jev (TypeSafe System One) verification judge: a fast typed-judgment model. +//! +//! Jev answers `noul` (yes/no probability) questions about a blob of state — +//! no prose, no code generation. That makes it a drop-in, cheap alternative to +//! the LLM judge for the delivery verification gate: one `noul` per acceptance +//! criterion, all in a single HTTP call; the gate passes only when every +//! probability reaches the configured threshold (default 0.8). +//! +//! Env vars: `SHEPHERD_JEV_API_KEY` (falls back to `TYPESAFE_API_KEY`), +//! `SHEPHERD_JEV_URL` (default `https://api.typesafe.ai/v1/systemone`), +//! `SHEPHERD_JEV_MODEL` (default `jev-latest`), +//! `SHEPHERD_JEV_THRESHOLD` (default `0.8`). + +use std::collections::BTreeMap; +use std::time::Duration; + +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::json; + +use orchestrator::ports::{DeliverableView, Judge, Verdict}; + +const DEFAULT_URL: &str = "https://api.typesafe.ai/v1/systemone"; +const DEFAULT_MODEL: &str = "jev-latest"; +const DEFAULT_THRESHOLD: f64 = 0.8; + +#[derive(Deserialize)] +struct SystemOneResponse { + #[serde(default)] + answers: BTreeMap, +} + +#[derive(Deserialize)] +struct NoulAnswer { + noul: f64, +} + +pub struct JevJudge { + client: reqwest::Client, + url: String, + key: String, + model: String, + threshold: f64, +} + +impl JevJudge { + pub fn from_env() -> Option { + let key = std::env::var("SHEPHERD_JEV_API_KEY") + .ok() + .filter(|s| !s.trim().is_empty()) + .or_else(|| std::env::var("TYPESAFE_API_KEY").ok().filter(|s| !s.trim().is_empty()))?; + let url = env_or("SHEPHERD_JEV_URL", DEFAULT_URL); + let model = env_or("SHEPHERD_JEV_MODEL", DEFAULT_MODEL); + let threshold = std::env::var("SHEPHERD_JEV_THRESHOLD") + .ok() + .and_then(|v| v.parse().ok()) + .filter(|t| (0.0..=1.0).contains(t)) + .unwrap_or(DEFAULT_THRESHOLD); + 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()); + Some(Self { client, url, key, model, threshold }) + } + + #[cfg(test)] + fn new(url: impl Into, key: impl Into, threshold: f64) -> Self { + Self { + client: reqwest::Client::new(), + url: url.into(), + key: key.into(), + model: DEFAULT_MODEL.into(), + threshold, + } + } + + fn state(criteria: &[String], deliverable: &DeliverableView) -> String { + let criteria = criteria.iter().map(|c| format!("- {c}")).collect::>().join("\n"); + format!( + "Acceptance criteria:\n{criteria}\n\nDeliverable:\nkind: {}\nreference: {}\nsummary: {}", + deliverable.kind, deliverable.reference, deliverable.summary + ) + } +} + +#[async_trait] +impl Judge for JevJudge { + async fn judge(&self, criteria: &[String], deliverable: &DeliverableView) -> Verdict { + if criteria.is_empty() { + return Verdict { passed: true, reason: "jev: no criteria to judge".into() }; + } + let questions: BTreeMap = criteria + .iter() + .enumerate() + .map(|(i, c)| { + ( + format!("c{i}"), + json!({ + "type": "noul", + "instructions": format!("Does the deliverable satisfy this acceptance criterion: {c}"), + }), + ) + }) + .collect(); + let body = json!({ "state": Self::state(criteria, deliverable), "model": self.model, "questions": questions }); + + let resp = match self.client.post(&self.url).bearer_auth(&self.key).json(&body).send().await + { + Ok(r) if r.status().is_success() => r, + Ok(r) => return Verdict { passed: false, reason: format!("jev HTTP {}", r.status()) }, + Err(e) => return Verdict { passed: false, reason: format!("jev unreachable: {e}") }, + }; + let parsed: SystemOneResponse = match resp.json().await { + Ok(p) => p, + Err(e) => { + return Verdict { + passed: false, + reason: format!("failed to parse jev response: {e}"), + } + } + }; + + // Every criterion must clear the threshold; report the weakest one. + let mut weakest = (1.0_f64, String::new()); + for (i, c) in criteria.iter().enumerate() { + let key = format!("c{i}"); + let p = match parsed.answers.get(&key) { + Some(a) => a.noul, + None => { + return Verdict { + passed: false, + reason: format!("jev response missing answer {key}"), + } + } + }; + if p < weakest.0 { + weakest = (p, c.clone()); + } + } + if weakest.0 >= self.threshold { + Verdict { + passed: true, + reason: format!( + "jev: min noul {:.2} >= threshold {:.2}", + weakest.0, self.threshold + ), + } + } else { + Verdict { + passed: false, + reason: format!( + "jev: noul {:.2} < threshold {:.2} for criterion: {}", + weakest.0, self.threshold, weakest.1 + ), + } + } + } +} + +fn env_or(key: &str, default: &str) -> String { + std::env::var(key).ok().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| default.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{routing::post, Json, Router}; + + async fn spawn(app: Router, path: &str) -> String { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let addr = listener.local_addr().expect("addr"); + tokio::spawn(async move { axum::serve(listener, app).await.expect("serve") }); + format!("http://{addr}{path}") + } + + fn dv() -> DeliverableView { + DeliverableView { + kind: "DIFF".into(), + reference: "branch:feat".into(), + summary: "done".into(), + } + } + + #[tokio::test] + async fn jev_judge_passes_when_every_criterion_clears_threshold() { + let app = Router::new().route( + "/v1/systemone", + post(|| async { + Json(json!({ + "model": "jev-1.13.0", + "answers": { "c0": { "type": "noul", "noul": 0.95 }, "c1": { "type": "noul", "noul": 0.9 } }, + "usage": { "input_tokens": 1, "output_tokens": 1 } + })) + }), + ); + let url = spawn(app, "/v1/systemone").await; + let j = JevJudge::new(url, "k", 0.8); + let v = j.judge(&["login success".into(), "token issued".into()], &dv()).await; + assert!(v.passed, "got: {v:?}"); + assert!(v.reason.contains("0.90"), "reason names the weakest: {}", v.reason); + } + + #[tokio::test] + async fn jev_judge_fails_on_criterion_below_threshold() { + let app = Router::new().route( + "/v1/systemone", + post(|| async { + Json(json!({ + "model": "jev-1.13.0", + "answers": { "c0": { "type": "noul", "noul": 0.99 }, "c1": { "type": "noul", "noul": 0.5 } }, + "usage": { "input_tokens": 1, "output_tokens": 1 } + })) + }), + ); + let url = spawn(app, "/v1/systemone").await; + let j = JevJudge::new(url, "k", 0.8); + let v = j.judge(&["login success".into(), "token issued".into()], &dv()).await; + assert!(!v.passed); + assert!( + v.reason.contains("token issued"), + "reason names the failing criterion: {}", + v.reason + ); + } + + #[tokio::test] + async fn jev_judge_passes_with_no_criteria() { + let j = JevJudge::new("http://127.0.0.1:9", "k", 0.8); + let v = j.judge(&[], &dv()).await; + assert!(v.passed); + assert_eq!(v.reason, "jev: no criteria to judge"); + } + + #[tokio::test] + async fn jev_judge_fail_closed_on_bad_json() { + let app = Router::new().route("/v1/systemone", post(|| async { "not json at all" })); + let url = spawn(app, "/v1/systemone").await; + let j = JevJudge::new(url, "k", 0.8); + let v = j.judge(&["login success".into()], &dv()).await; + assert!(!v.passed); + assert!(v.reason.contains("parse"), "got: {}", v.reason); + } +} diff --git a/crates/server/src/judge.rs b/crates/server/src/judge.rs index dec3253..ec8174d 100644 --- a/crates/server/src/judge.rs +++ b/crates/server/src/judge.rs @@ -41,6 +41,12 @@ impl Judge for HttpJudge { } pub fn build_judge() -> Arc { + // Jev first: it is the cheap typed-judgment alternative to the LLM judge. + // Setting SHEPHERD_JEV_API_KEY (or TYPESAFE_API_KEY) routes the verification + // gate to Jev even when SHEPHERD_LLM_URL is also set for planner/PRD use. + if let Some(j) = crate::jev::JevJudge::from_env() { + return Arc::new(j); + } if let Some(j) = crate::llm::judge() { return j; } diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 9575e27..0ab2bc0 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -11,6 +11,7 @@ mod debug_send; mod decomposition_run; mod design_bridge; mod import_scheduler; +mod jev; mod judge; mod llm; mod mcp_bus; diff --git a/crates/server/src/mcp_tools.rs b/crates/server/src/mcp_tools.rs index 17902ee..251efa2 100644 --- a/crates/server/src/mcp_tools.rs +++ b/crates/server/src/mcp_tools.rs @@ -485,7 +485,7 @@ pub fn router( .requires("TASK", "ADD")) .tool(Tool::new( "shepherd_dispatch_delivery", - "把任务派发给 AI 执行者(executor: CLAUDE_CODE | CODEX | OPENCODE | CODEBUDDY);自动驱动任务、过验证门并回灌验证。\ + "把任务派发给 AI 执行者(executor: CLAUDE_CODE | CODEX | OPENCODE | CODEBUDDY | DEEPCODE);自动驱动任务、过验证门并回灌验证。\ 可直接传 skillIds(+projectId)自动组合行为规范注入,无需先调 compose。", obj( json!({ @@ -494,7 +494,7 @@ pub fn router( "title": { "type": "string" }, "description": { "type": "string" }, "acceptanceCriteria": { "type": "array", "items": { "type": "string" } }, - "executor": { "type": "string", "enum": ["CLAUDE_CODE", "CODEX", "OPENCODE", "CODEBUDDY"] }, + "executor": { "type": "string", "enum": ["CLAUDE_CODE", "CODEX", "OPENCODE", "CODEBUDDY", "DEEPCODE"] }, "projectId": { "type": "string", "description": "skillIds 非空时必填(用于组合)" }, "skillIds": { "type": "array", "items": { "type": "string" }, "description": "可选:自动组合这些 skill 为行为规范" }, "instructions": { "type": "string", "description": "可选:显式行为规范(与 skillIds 组合结果合并)" } diff --git a/crates/shepherd-cli/src/client.rs b/crates/shepherd-cli/src/client.rs index 14e0968..26f5d44 100644 --- a/crates/shepherd-cli/src/client.rs +++ b/crates/shepherd-cli/src/client.rs @@ -345,8 +345,9 @@ pub fn normalize_agent(t: &str) -> R { "opencode" => Ok("OPENCODE".into()), // The brand is one word, but the claude-code spelling invites code-buddy; accept both. "codebuddy" | "code_buddy" => Ok("CODEBUDDY".into()), + "deepcode" | "deep_code" => Ok("DEEPCODE".into()), other => Err(format!( - "unknown agent type: {other} (supported: claude-code | codex | opencode | codebuddy)" + "unknown agent type: {other} (supported: claude-code | codex | opencode | codebuddy | deepcode)" ) .into()), } diff --git a/crates/shepherd-cli/src/commands/agent.rs b/crates/shepherd-cli/src/commands/agent.rs index 2c11cdf..fb12cad 100644 --- a/crates/shepherd-cli/src/commands/agent.rs +++ b/crates/shepherd-cli/src/commands/agent.rs @@ -3,7 +3,7 @@ use clap::Subcommand; #[derive(Subcommand)] pub enum AgentCmd { - /// Connect an AI executor (claude-code | codex | opencode | codebuddy) and save it as the dispatch default. + /// Connect an AI executor (claude-code | codex | opencode | codebuddy | deepcode) and save it as the dispatch default. Connect { #[arg(long = "type")] kind: String, diff --git a/crates/shepherd-cli/src/main.rs b/crates/shepherd-cli/src/main.rs index 816265a..a2b4dba 100644 --- a/crates/shepherd-cli/src/main.rs +++ b/crates/shepherd-cli/src/main.rs @@ -323,6 +323,8 @@ mod tests { assert_eq!(normalize_agent("opencode").unwrap(), "OPENCODE"); assert_eq!(normalize_agent("CodeBuddy").unwrap(), "CODEBUDDY"); assert_eq!(normalize_agent("code-buddy").unwrap(), "CODEBUDDY"); + assert_eq!(normalize_agent("deepcode").unwrap(), "DEEPCODE"); + assert_eq!(normalize_agent("DeepCode").unwrap(), "DEEPCODE"); assert!(normalize_agent("gpt").is_err()); } diff --git a/docs/EXECUTORS.md b/docs/EXECUTORS.md index bbaa562..efaae00 100644 --- a/docs/EXECUTORS.md +++ b/docs/EXECUTORS.md @@ -1,6 +1,6 @@ # Shepherd — Running AI Executors -How to run each supported AI executor (Claude Code / Codex / OpenCode / CodeBuddy) +How to run each supported AI executor (Claude Code / Codex / OpenCode / CodeBuddy / DeepCode) behind `agent-runtime`. For fleet architecture and server-side setup see [USAGE.md §7](./USAGE.md); for image builds and deployment see [DEPLOYMENT.md](./DEPLOYMENT.md). @@ -10,6 +10,7 @@ behind `agent-runtime`. For fleet architecture and server-side setup see - [Codex](#codex) - [OpenCode](#opencode) - [CodeBuddy](#codebuddy) +- [DeepCode](#deepcode) - [One runtime, multiple executors](#one-runtime-multiple-executors) - [Mock executor (no CLI)](#mock-executor-no-cli) - [Windows](#windows) @@ -24,7 +25,7 @@ behind `agent-runtime`. For fleet architecture and server-side setup see ## How dispatch reaches a CLI Every delivery attempt carries an `executor` kind (`CLAUDE_CODE` / `CODEX` / -`OPENCODE` / `CODEBUDDY`). The server enqueues the task per kind; an +`OPENCODE` / `CODEBUDDY` / `DEEPCODE`). The server enqueues the task per kind; an `agent-runtime` long-polls with its `SHEPHERD_CAPS` and only claims kinds it declared. On claim, the runtime spawns the matching CLI in a dedicated git worktree, snapshots the resulting changes as a commit, and reports back. @@ -148,6 +149,20 @@ SHEPHERD_CAPS=CODEBUDDY AGENT_WORKDIR=/repo … ./agent-runtime Override with `CODEBUDDY_CMD`, e.g. widen permissions for tasks that need to run shell commands: `CODEBUDDY_CMD="codebuddy -p --permission-mode bypassPermissions"`. +## DeepCode + +Generic backend. The default invocation is `deepcode exec --json ""` — +`--json` keeps the output machine-readable for the result summary. + +```bash +deepcode --version + +SHEPHERD_CAPS=DEEPCODE AGENT_WORKDIR=/repo … ./agent-runtime +``` + +Override with `DEEPCODE_CMD`, e.g. widen permissions for tasks that need to +edit files headlessly: `DEEPCODE_CMD="deepcode exec --access full-access --json"`. + ## One runtime, multiple executors A single runtime can claim several kinds — list them all and make sure every @@ -169,7 +184,7 @@ canned output without spawning a CLI — useful to smoke-test the dispatch loop before installing real CLIs. ```bash -AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY … ./agent-runtime +AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY,DEEPCODE … ./agent-runtime ``` ## Windows @@ -183,7 +198,7 @@ The runtime runs natively on Windows (build with the MSVC toolchain: `codex.exe` take precedence); explicit paths via `CLAUDE_BIN` / `*_CMD` are used as-is. `CLAUDE_BIN=D:\nvm4w\nodejs\claude.cmd` works: the Claude backend feeds the prompt over stdin, so the cmd.exe hop is harmless. -- **Generic backends (codebuddy/codex/opencode) cannot go through a `.cmd` +- **Generic backends (codebuddy/codex/opencode/deepcode) cannot go through a `.cmd` shim**: they pass the prompt as an argv, prompts contain newlines, and Rust refuses newline arguments to batch files (cmd.exe cannot carry them safely). Bypass cmd.exe by invoking node directly — read the shim (`type @@ -254,7 +269,7 @@ shepherd dispatch --decomp d1 --task t1 --executor CODEBUDDY # or bind a default once: shepherd agent connect --kind codebuddy ``` -The MCP tool `shepherd_dispatch_delivery` accepts the same four kinds. +The MCP tool `shepherd_dispatch_delivery` accepts the same five kinds. ### Pinning a task to one runtime @@ -323,5 +338,5 @@ animation look the same as local ones. |---|---| | Task stays queued (`ready` grows) | No online runtime declares that kind — compare `SHEPHERD_CAPS` with the task's `executor`, then `GET /agent/runtime` for liveness | | Attempt delivers with "no code change" | CLI ran but refused edits (permission mode) or the prompt didn't ask for file changes — read the deliverable summary, it contains the CLI's own output | -| Spawn error in events | CLI not on `PATH` inside the runtime's environment — set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` | +| Spawn error in events | CLI not on `PATH` inside the runtime's environment — set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD` | | Attempt fails at timeout | Raise `AGENT_TASK_TIMEOUT_SECS`; generic backends get killed (process group) at the deadline | diff --git a/docs/EXECUTORS.zh-CN.md b/docs/EXECUTORS.zh-CN.md index 728862f..5712220 100644 --- a/docs/EXECUTORS.zh-CN.md +++ b/docs/EXECUTORS.zh-CN.md @@ -1,7 +1,7 @@ # Shepherd — AI 执行者运行指南 介绍如何在 `agent-runtime` 下运行各家 AI 执行者(Claude Code / Codex / OpenCode / -CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-CN.md), +CodeBuddy / DeepCode)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-CN.md), 镜像构建与部署见 [DEPLOYMENT.zh-CN.md](./DEPLOYMENT.zh-CN.md)。 - [派发如何到达 CLI](#派发如何到达-cli) @@ -10,6 +10,7 @@ CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-C - [Codex](#codex) - [OpenCode](#opencode) - [CodeBuddy](#codebuddy) +- [DeepCode](#deepcode) - [一个 runtime 承接多种执行者](#一个-runtime-承接多种执行者) - [mock 执行者(无需 CLI)](#mock-执行者无需-cli) - [Windows](#windows) @@ -24,7 +25,7 @@ CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-C ## 派发如何到达 CLI 每个交付尝试都带 `executor` 类型(`CLAUDE_CODE` / `CODEX` / `OPENCODE` / -`CODEBUDDY`)。服务端按类型入队;`agent-runtime` 以 `SHEPHERD_CAPS` 长轮询, +`CODEBUDDY` / `DEEPCODE`)。服务端按类型入队;`agent-runtime` 以 `SHEPHERD_CAPS` 长轮询, 只认领自己声明过的类型。认领后 runtime 在独立 git worktree 中拉起对应 CLI, 把产生的改动快照成 commit 并回报。 @@ -140,6 +141,20 @@ SHEPHERD_CAPS=CODEBUDDY AGENT_WORKDIR=/repo … ./agent-runtime 用 `CODEBUDDY_CMD` 覆盖,例如任务需要跑 shell 时放宽权限: `CODEBUDDY_CMD="codebuddy -p --permission-mode bypassPermissions"`。 +## DeepCode + +通用后端。默认调用 `deepcode exec --json ""` —— `--json` 让输出保持 +机器可读,便于回填结果摘要。 + +```bash +deepcode --version + +SHEPHERD_CAPS=DEEPCODE AGENT_WORKDIR=/repo … ./agent-runtime +``` + +用 `DEEPCODE_CMD` 覆盖,例如 headless 下需要放开文件编辑权限时: +`DEEPCODE_CMD="deepcode exec --access full-access --json"`。 + ## 一个 runtime 承接多种执行者 单个 runtime 可以声明多种类型——全部列出,并保证对应 CLI 都装好且已登录: @@ -158,7 +173,7 @@ runtime(能力任意组合)。服务端设了 `SHEPHERD_FLEET_REDIS` 时,跨机 CLI——适合在装 CLI 之前先冒烟验证派发链路。 ```bash -AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY … ./agent-runtime +AGENT_MOCK=1 SHEPHERD_CAPS=CLAUDE_CODE,CODEX,OPENCODE,CODEBUDDY,DEEPCODE … ./agent-runtime ``` ## Windows @@ -171,7 +186,7 @@ runtime 可在 Windows 原生运行(MSVC 工具链:`cargo build --release -p age (`codex.exe` 这类原生二进制优先);`CLAUDE_BIN` / `*_CMD` 里写的显式路径原样使用。 `CLAUDE_BIN=D:\nvm4w\nodejs\claude.cmd` 没问题:Claude 后端的 prompt 走 stdin, 经 cmd.exe 一跳无害。 -- **通用后端(codebuddy/codex/opencode)不能走 `.cmd` 垫片**:它们把 prompt 作为 +- **通用后端(codebuddy/codex/opencode/deepcode)不能走 `.cmd` 垫片**:它们把 prompt 作为 命令行参数传,prompt 必含换行,而 Rust 拒绝给批处理文件传含换行的参数 (cmd.exe 无法安全携带)。绕开 cmd.exe,直接用 node 调包入口——先 `type codebuddy.cmd` 看垫片真正调用什么,然后照抄,如 @@ -233,7 +248,7 @@ shepherd dispatch --decomp d1 --task t1 --executor CODEBUDDY # 或先绑定默认:shepherd agent connect --kind codebuddy ``` -MCP 工具 `shepherd_dispatch_delivery` 同样接受这四种类型。 +MCP 工具 `shepherd_dispatch_delivery` 同样接受这五种类型。 ### 定向到具体某台 runtime @@ -293,5 +308,5 @@ cargo r -p pool-runner |---|---| | 任务一直排队(`ready` 增长) | 没有在线 runtime 声明该类型——核对 `SHEPHERD_CAPS` 与任务 `executor`,再看 `GET /agent/runtime` 的在线状态 | | 交付显示"无代码变动" | CLI 跑了但拒绝改文件(权限模式),或提示词本身没要求改文件——看交付摘要,里面是 CLI 的原始输出 | -| 事件里报 spawn 错误 | CLI 不在 runtime 进程的 `PATH` 上——设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` | +| 事件里报 spawn 错误 | CLI 不在 runtime 进程的 `PATH` 上——设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD` | | 任务超时失败 | 调大 `AGENT_TASK_TIMEOUT_SECS`;通用后端到点会被整进程组杀掉 | diff --git a/docs/USAGE.md b/docs/USAGE.md index 610b285..86f9f7a 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -20,7 +20,7 @@ you file a requirement │ │ dispatch ▼ ▼ FLEET DISPATCH ──► agent-runtime executor - (pull / long-poll) (claude / codex / opencode / codebuddy) + (pull / long-poll) (claude / codex / opencode / codebuddy / deepcode) │ runs in a git worktree ▼ deliverable (diff / PR) @@ -59,7 +59,7 @@ Company AI tools (Claude Code, Codex, …) run on internal dev machines or CI | Quick start (Docker) | Docker + Docker Compose v2 | | From-source dev | Rust (stable, edition 2021; CI uses `rust:1.86`), Node.js 18+, a PostgreSQL 16 instance | | Multi-host fleet | Redis 7 | -| Real AI executors | `git` plus the agent CLIs on `PATH` (`claude` / `codex` / `opencode` / `codebuddy`) | +| Real AI executors | `git` plus the agent CLIs on `PATH` (`claude` / `codex` / `opencode` / `codebuddy` / `deepcode`) | PostgreSQL is required; the server **auto-applies migrations on startup**. Redis is required **only** for the multi-host fleet. @@ -187,6 +187,23 @@ All server start-up switches are consolidated into a typed config (`crates/serve Advanced/lazy-read switches also exist for the pluggable AI touchpoints — `SHEPHERD_AGENT_URL` / `SHEPHERD_AGENT_CMD` / `SHEPHERD_AGENT_ASYNC` (executor routing), `SHEPHERD_LLM_URL`, `SHEPHERD_PLANNER_URL`, `SHEPHERD_JUDGE_URL`, `SHEPHERD_MAX_REVISIONS`. Defaults need none of them. +#### 5.1.1 Verification judge: Jev + +The delivery verification gate (`Judge`) can run on [Jev](https://learnjev.com), TypeSafe's +fast typed-judgment model (yes/no probability answers, no prose) instead of an LLM. +Set the key and the gate asks one `noul` question per acceptance criterion in a single +call; it passes only when every probability reaches the threshold. + +| Variable | Default | Meaning | +|---|---|---| +| `SHEPHERD_JEV_API_KEY` | *(falls back to `TYPESAFE_API_KEY`)* | TypeSafe API key — presence enables the Jev judge (takes priority over the LLM judge) | +| `SHEPHERD_JEV_URL` | `https://api.typesafe.ai/v1/systemone` | Jev System One endpoint | +| `SHEPHERD_JEV_MODEL` | `jev-latest` | Jev model alias | +| `SHEPHERD_JEV_THRESHOLD` | `0.8` | Minimum `noul` probability per criterion to pass | + +Failures fail closed: any HTTP/parse error returns a not-passed verdict, so a broken +Jev setup blocks delivery instead of silently passing it. + ### 5.2 Agent-runtime (`agent-runtime`) | Variable | Default | Meaning | @@ -203,6 +220,7 @@ Advanced/lazy-read switches also exist for the pluggable AI touchpoints — `SHE | `CODEX_CMD` | `codex exec` | Codex CLI invocation | | `OPENCODE_CMD` | `opencode run` | OpenCode CLI invocation | | `CODEBUDDY_CMD` | `codebuddy -p --permission-mode acceptEdits` | CodeBuddy CLI invocation | +| `DEEPCODE_CMD` | `deepcode exec --json` | DeepCode CLI invocation | --- @@ -275,6 +293,7 @@ The runtime picks a backend per task by its `executor` kind, unless `AGENT_MOCK= | `CODEX` | generic CLI | `codex exec` (`CODEX_CMD`) | | `OPENCODE` | generic CLI | `opencode run` (`OPENCODE_CMD`) | | `CODEBUDDY` | generic CLI | `codebuddy -p --permission-mode acceptEdits` (`CODEBUDDY_CMD`) | +| `DEEPCODE` | generic CLI | `deepcode exec --json` (`DEEPCODE_CMD`) | | any (with `AGENT_MOCK=1`) | mock — returns canned output | none | Real backends need `git` and the CLI on `PATH` (or pointed at via the override env). Per-executor run recipes (login, permission modes, dispatch examples) are in [EXECUTORS.md](./EXECUTORS.md). Adding a new backend means implementing one `CliAgentBackend` (`async fn execute(prompt, cwd, sink)`) and registering an enum variant — see `crates/agent-runtime/src/backend.rs`. @@ -344,7 +363,7 @@ Both honour `SHEPHERD_BASE` (default `http://127.0.0.1:9180`) and `SHEPHERD_USER | Web console shows blank / API 404 in dev | Vite proxy target mismatch. The dev proxy points at `:9180`; bind the server there or set `SHEPHERD_API` to your server URL. | | Tasks never get claimed | Server not in fleet mode (`SHEPHERD_AGENT_FLEET=1`), no runtime online, or capability mismatch — check `SHEPHERD_CAPS` vs the task's executor kind, and `GET /agent/work/stats`. | | Multi-host runtimes can't share work | `SHEPHERD_FLEET_REDIS` not set (or not the same Redis) on all server replicas → each falls back to its own in-process queue. | -| Real agent does nothing / errors spawning | CLI not on `PATH`; set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD`, or run with `AGENT_MOCK=1` to confirm the loop. | +| Real agent does nothing / errors spawning | CLI not on `PATH`; set `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD`, or run with `AGENT_MOCK=1` to confirm the loop. | | API batch-run stuck `RUNNING` with no results | `SHEPHERD_RUNNER=noop` is set (demo placeholder). Unset it to use the native runner. | | New migration not applied | Restart the server — migrations run on boot; a new migration file needs a rebuild. | | OIDC endpoint 404 | The provider is only registered when **both** id and secret env vars are set. | diff --git a/docs/USAGE.zh-CN.md b/docs/USAGE.zh-CN.md index a8b242e..d3adead 100644 --- a/docs/USAGE.zh-CN.md +++ b/docs/USAGE.zh-CN.md @@ -20,7 +20,7 @@ Shepherd 是一个 AI 研发**监督**平台:从 idea 到交付,更安全、更 │ │ 派发 ▼ ▼ 机群派发 ──► agent-runtime 执行者 - (拉取 / 长轮询) (claude / codex / opencode / codebuddy) + (拉取 / 长轮询) (claude / codex / opencode / codebuddy / deepcode) │ 在独立 git worktree 中运行 ▼ 交付物(diff / PR) @@ -59,7 +59,7 @@ Shepherd 是一个 AI 研发**监督**平台:从 idea 到交付,更安全、更 | 快速开始(Docker) | Docker + Docker Compose v2 | | 源码开发 | Rust(stable,edition 2021;CI 用 `rust:1.86`)、Node.js 18+、一个 PostgreSQL 16 实例 | | 多机机群 | Redis 7 | -| 真实 AI 执行者 | `git` + agent CLI 在 `PATH` 上(`claude` / `codex` / `opencode` / `codebuddy`) | +| 真实 AI 执行者 | `git` + agent CLI 在 `PATH` 上(`claude` / `codex` / `opencode` / `codebuddy` / `deepcode`) | PostgreSQL 必需;服务端**启动时自动跑迁移**。Redis 仅在多机机群时必需。 @@ -186,6 +186,22 @@ cargo clippy --workspace -- -D warnings 可插拔 AI 触点还有若干进阶/懒读开关:`SHEPHERD_AGENT_URL` / `SHEPHERD_AGENT_CMD` / `SHEPHERD_AGENT_ASYNC`(执行者路由)、`SHEPHERD_LLM_URL`、`SHEPHERD_PLANNER_URL`、`SHEPHERD_JUDGE_URL`、`SHEPHERD_MAX_REVISIONS`。默认全都无需设置。 +#### 5.1.1 验证门 Judge:Jev + +交付验证门(`Judge`)可以跑在 [Jev](https://learnjev.com)(TypeSafe 的快速类型化判断 +模型,只回答是/否概率,不生成文字)上,替代 LLM judge。配好 key 后,验证门会为每条 +验收标准发一个 `noul` 问题(单次请求),所有概率都达到阈值才判过。 + +| 变量 | 默认 | 含义 | +|---|---|---| +| `SHEPHERD_JEV_API_KEY` | *(缺省回退 `TYPESAFE_API_KEY`)* | TypeSafe API key——设了即启用 Jev judge(优先级高于 LLM judge) | +| `SHEPHERD_JEV_URL` | `https://api.typesafe.ai/v1/systemone` | Jev System One 端点 | +| `SHEPHERD_JEV_MODEL` | `jev-latest` | Jev 模型别名 | +| `SHEPHERD_JEV_THRESHOLD` | `0.8` | 每条标准的最低 `noul` 通过概率 | + +失败一律 fail-closed:任何 HTTP/解析错误都判为不通过,即 Jev 配置坏了会阻断交付, +而不是静默放行。 + ### 5.2 Agent-runtime(`agent-runtime`) | 变量 | 默认 | 含义 | @@ -202,6 +218,7 @@ cargo clippy --workspace -- -D warnings | `CODEX_CMD` | `codex exec` | Codex CLI 调用 | | `OPENCODE_CMD` | `opencode run` | OpenCode CLI 调用 | | `CODEBUDDY_CMD` | `codebuddy -p --permission-mode acceptEdits` | CodeBuddy CLI 调用 | +| `DEEPCODE_CMD` | `deepcode exec --json` | DeepCode CLI 调用 | --- @@ -271,6 +288,7 @@ runtime 按任务的 `executor` 类型选后端,除非 `AGENT_MOCK=1` 强制走 | `CODEX` | 通用 CLI | `codex exec`(`CODEX_CMD`) | | `OPENCODE` | 通用 CLI | `opencode run`(`OPENCODE_CMD`) | | `CODEBUDDY` | 通用 CLI | `codebuddy -p --permission-mode acceptEdits`(`CODEBUDDY_CMD`) | +| `DEEPCODE` | 通用 CLI | `deepcode exec --json`(`DEEPCODE_CMD`) | | 任意(配 `AGENT_MOCK=1`) | mock——返回固定输出 | 无 | 真实后端需 `git` 与 CLI 在 `PATH`(或经覆盖 env 指定)。各执行者的运行指南(登录、权限模式、派发示例)见 [EXECUTORS.zh-CN.md](./EXECUTORS.zh-CN.md)。新增后端只需实现一个 `CliAgentBackend`(`async fn execute(prompt, cwd, sink)`)并注册一个枚举变体——见 `crates/agent-runtime/src/backend.rs`。 @@ -340,7 +358,7 @@ python3 .agents/skills/openapi-bootstrap/scenarios_all.py | dev 下控制台空白 / API 404 | Vite 代理目标不匹配。dev 代理指向 `:9180`;把服务端绑到那里,或设 `SHEPHERD_API` 为你的服务端 URL。 | | 任务始终无人认领 | 服务端未开机群模式(`SHEPHERD_AGENT_FLEET=1`)、无 runtime 在线,或能力不匹配——核对 `SHEPHERD_CAPS` 与任务 executor 类型,以及 `GET /agent/work/stats`。 | | 多机 runtime 无法共享任务 | 各服务端副本未设(或非同一个)`SHEPHERD_FLEET_REDIS` → 各自回落到自己的进程内队列。 | -| 真实 agent 不动作 / spawn 报错 | CLI 不在 `PATH`;设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD`,或先用 `AGENT_MOCK=1` 验证链路。 | +| 真实 agent 不动作 / spawn 报错 | CLI 不在 `PATH`;设 `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` / `CODEBUDDY_CMD` / `DEEPCODE_CMD`,或先用 `AGENT_MOCK=1` 验证链路。 | | 接口批量运行卡在 `RUNNING` 无结果 | 设了 `SHEPHERD_RUNNER=noop`(演示占位)。取消它以走原生 runner。 | | 新迁移未生效 | 重启服务端——迁移在启动时跑;新迁移文件需重新构建。 | | OIDC 端点 404 | provider 仅在 id 与 secret 两个 env **都**设置时才注册。 | diff --git a/web/src/api.ts b/web/src/api.ts index aeb4f81..1181146 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1154,6 +1154,7 @@ export const EXECUTOR_LABEL: Record = { CODEX: 'Codex', OPENCODE: 'OpenCode', CODEBUDDY: 'CodeBuddy', + DEEPCODE: 'DeepCode', } /** One task-center row: system-wide delivery attempts aggregated by status/executor/result/completion rate. */ @@ -1166,7 +1167,7 @@ export interface TaskCenterItem { description: string /** Module (title of the owning requirement; empty string when none). */ module: string - /** Executor: CLAUDE_CODE / CODEX / OPENCODE / CODEBUDDY. */ + /** Executor: CLAUDE_CODE / CODEX / OPENCODE / CODEBUDDY / DEEPCODE. */ executor: string /** Target runtime name for directed dispatch; empty = any runtime with the capability. */ targetRuntime?: string | null