fix: make mission workspaces backend-aware
This commit is contained in:
@@ -132,9 +132,9 @@ pub async fn get_backend_config(
|
|||||||
let api_key_configured = state
|
let api_key_configured = state
|
||||||
.secrets
|
.secrets
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|s| {
|
.map(|_s| {
|
||||||
// Check async context
|
// TODO: implement proper secret check
|
||||||
false // TODO: implement proper secret check
|
false
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
|
|||||||
@@ -1059,7 +1059,7 @@ pub async fn create_mission(
|
|||||||
) -> Result<Json<Mission>, (StatusCode, String)> {
|
) -> Result<Json<Mission>, (StatusCode, String)> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let (title, workspace_id, agent, model_override, backend) = body
|
let (title, workspace_id, agent, model_override, mut backend) = body
|
||||||
.map(|b| {
|
.map(|b| {
|
||||||
(
|
(
|
||||||
b.title.clone(),
|
b.title.clone(),
|
||||||
@@ -1071,6 +1071,12 @@ pub async fn create_mission(
|
|||||||
})
|
})
|
||||||
.unwrap_or((None, None, None, None, None));
|
.unwrap_or((None, None, None, None, None));
|
||||||
|
|
||||||
|
if let Some(value) = backend.as_ref() {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
backend = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate agent exists before creating mission (fail fast with clear error)
|
// Validate agent exists before creating mission (fail fast with clear error)
|
||||||
if let Some(ref agent_name) = agent {
|
if let Some(ref agent_name) = agent {
|
||||||
super::library::validate_agent_exists(&state, agent_name)
|
super::library::validate_agent_exists(&state, agent_name)
|
||||||
@@ -1078,6 +1084,16 @@ pub async fn create_mission(
|
|||||||
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
.map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref backend_id) = backend {
|
||||||
|
let registry = state.backend_registry.read().await;
|
||||||
|
if registry.get(backend_id).is_none() {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
format!("Unknown backend: {}", backend_id),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let control = control_for_user(&state, &user).await;
|
let control = control_for_user(&state, &user).await;
|
||||||
control
|
control
|
||||||
.cmd_tx
|
.cmd_tx
|
||||||
@@ -2318,6 +2334,7 @@ async fn control_actor_loop(
|
|||||||
tid,
|
tid,
|
||||||
mission.workspace_id,
|
mission.workspace_id,
|
||||||
mission.agent.clone(),
|
mission.agent.clone(),
|
||||||
|
Some(mission.backend.clone()),
|
||||||
);
|
);
|
||||||
// Load existing history
|
// Load existing history
|
||||||
for entry in &mission.history {
|
for entry in &mission.history {
|
||||||
@@ -2449,19 +2466,20 @@ async fn control_actor_loop(
|
|||||||
let progress_ref = Arc::clone(&progress);
|
let progress_ref = Arc::clone(&progress);
|
||||||
// Capture which mission this task is working on
|
// Capture which mission this task is working on
|
||||||
let mission_id = current_mission.read().await.clone();
|
let mission_id = current_mission.read().await.clone();
|
||||||
let (workspace_id, model_override, mission_agent) = if let Some(mid) = mission_id {
|
let (workspace_id, model_override, mission_agent, backend_id) = if let Some(mid) = mission_id {
|
||||||
match mission_store.get_mission(mid).await {
|
match mission_store.get_mission(mid).await {
|
||||||
Ok(Some(mission)) => (
|
Ok(Some(mission)) => (
|
||||||
Some(mission.workspace_id),
|
Some(mission.workspace_id),
|
||||||
mission.model_override.clone(),
|
mission.model_override.clone(),
|
||||||
mission.agent.clone(),
|
mission.agent.clone(),
|
||||||
|
Some(mission.backend.clone()),
|
||||||
),
|
),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Mission {} not found while resolving workspace",
|
"Mission {} not found while resolving workspace",
|
||||||
mid
|
mid
|
||||||
);
|
);
|
||||||
(None, None, None)
|
(None, None, None, None)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -2469,11 +2487,11 @@ async fn control_actor_loop(
|
|||||||
mid,
|
mid,
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
(None, None, None)
|
(None, None, None, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(None, None, None)
|
(None, None, None, None)
|
||||||
};
|
};
|
||||||
// Per-message agent overrides mission agent
|
// Per-message agent overrides mission agent
|
||||||
let agent_override = per_msg_agent.or(mission_agent);
|
let agent_override = per_msg_agent.or(mission_agent);
|
||||||
@@ -2499,6 +2517,7 @@ async fn control_actor_loop(
|
|||||||
progress_ref,
|
progress_ref,
|
||||||
mission_id,
|
mission_id,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
|
backend_id,
|
||||||
model_override,
|
model_override,
|
||||||
agent_override,
|
agent_override,
|
||||||
)
|
)
|
||||||
@@ -2680,6 +2699,7 @@ async fn control_actor_loop(
|
|||||||
mission_id,
|
mission_id,
|
||||||
mission.workspace_id,
|
mission.workspace_id,
|
||||||
mission.agent.clone(),
|
mission.agent.clone(),
|
||||||
|
Some(mission.backend.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load existing history into runner to preserve conversation context
|
// Load existing history into runner to preserve conversation context
|
||||||
@@ -2847,6 +2867,7 @@ async fn control_actor_loop(
|
|||||||
let tree_ref = Arc::clone(¤t_tree);
|
let tree_ref = Arc::clone(¤t_tree);
|
||||||
let progress_ref = Arc::clone(&progress);
|
let progress_ref = Arc::clone(&progress);
|
||||||
let workspace_id = Some(mission.workspace_id);
|
let workspace_id = Some(mission.workspace_id);
|
||||||
|
let backend_id = Some(mission.backend.clone());
|
||||||
let model_override = mission.model_override.clone();
|
let model_override = mission.model_override.clone();
|
||||||
// Resume uses mission agent (no per-message override for resumes)
|
// Resume uses mission agent (no per-message override for resumes)
|
||||||
let agent_override = mission.agent.clone();
|
let agent_override = mission.agent.clone();
|
||||||
@@ -2871,6 +2892,7 @@ async fn control_actor_loop(
|
|||||||
progress_ref,
|
progress_ref,
|
||||||
Some(mission_id),
|
Some(mission_id),
|
||||||
workspace_id,
|
workspace_id,
|
||||||
|
backend_id,
|
||||||
model_override,
|
model_override,
|
||||||
agent_override,
|
agent_override,
|
||||||
)
|
)
|
||||||
@@ -3303,19 +3325,20 @@ async fn control_actor_loop(
|
|||||||
running_cancel = Some(cancel.clone());
|
running_cancel = Some(cancel.clone());
|
||||||
// Capture which mission this task is working on
|
// Capture which mission this task is working on
|
||||||
let mission_id = current_mission.read().await.clone();
|
let mission_id = current_mission.read().await.clone();
|
||||||
let (workspace_id, model_override, mission_agent) = if let Some(mid) = mission_id {
|
let (workspace_id, model_override, mission_agent, backend_id) = if let Some(mid) = mission_id {
|
||||||
match mission_store.get_mission(mid).await {
|
match mission_store.get_mission(mid).await {
|
||||||
Ok(Some(mission)) => (
|
Ok(Some(mission)) => (
|
||||||
Some(mission.workspace_id),
|
Some(mission.workspace_id),
|
||||||
mission.model_override.clone(),
|
mission.model_override.clone(),
|
||||||
mission.agent.clone(),
|
mission.agent.clone(),
|
||||||
|
Some(mission.backend.clone()),
|
||||||
),
|
),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"Mission {} not found while resolving workspace",
|
"Mission {} not found while resolving workspace",
|
||||||
mid
|
mid
|
||||||
);
|
);
|
||||||
(None, None, None)
|
(None, None, None, None)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -3323,11 +3346,11 @@ async fn control_actor_loop(
|
|||||||
mid,
|
mid,
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
(None, None, None)
|
(None, None, None, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(None, None, None)
|
(None, None, None, None)
|
||||||
};
|
};
|
||||||
// Per-message agent overrides mission agent
|
// Per-message agent overrides mission agent
|
||||||
let agent_override = per_msg_agent.or(mission_agent);
|
let agent_override = per_msg_agent.or(mission_agent);
|
||||||
@@ -3352,6 +3375,7 @@ async fn control_actor_loop(
|
|||||||
progress_ref,
|
progress_ref,
|
||||||
mission_id,
|
mission_id,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
|
backend_id,
|
||||||
model_override,
|
model_override,
|
||||||
agent_override,
|
agent_override,
|
||||||
)
|
)
|
||||||
@@ -3572,6 +3596,7 @@ async fn run_single_control_turn(
|
|||||||
progress_snapshot: Arc<RwLock<ExecutionProgress>>,
|
progress_snapshot: Arc<RwLock<ExecutionProgress>>,
|
||||||
mission_id: Option<Uuid>,
|
mission_id: Option<Uuid>,
|
||||||
workspace_id: Option<Uuid>,
|
workspace_id: Option<Uuid>,
|
||||||
|
backend_id: Option<String>,
|
||||||
model_override: Option<String>,
|
model_override: Option<String>,
|
||||||
agent_override: Option<String>,
|
agent_override: Option<String>,
|
||||||
) -> crate::agents::AgentResult {
|
) -> crate::agents::AgentResult {
|
||||||
@@ -3587,13 +3612,20 @@ async fn run_single_control_turn(
|
|||||||
// Get library for skill syncing
|
// Get library for skill syncing
|
||||||
let lib_guard = library.read().await;
|
let lib_guard = library.read().await;
|
||||||
let lib_ref = lib_guard.as_ref().map(|l| l.as_ref());
|
let lib_ref = lib_guard.as_ref().map(|l| l.as_ref());
|
||||||
let dir =
|
let dir = match workspace::prepare_mission_workspace_with_skills_backend(
|
||||||
match workspace::prepare_mission_workspace_with_skills(&ws, &mcp, lib_ref, mid).await {
|
&ws,
|
||||||
Ok(dir) => dir,
|
&mcp,
|
||||||
Err(e) => {
|
lib_ref,
|
||||||
tracing::warn!("Failed to prepare mission workspace: {}", e);
|
mid,
|
||||||
ws.path.clone()
|
backend_id.as_deref().unwrap_or("opencode"),
|
||||||
}
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(dir) => dir,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to prepare mission workspace: {}", e);
|
||||||
|
ws.path.clone()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
(dir, Some(ws))
|
(dir, Some(ws))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ pub struct MissionRunner {
|
|||||||
/// Workspace ID where this mission should run
|
/// Workspace ID where this mission should run
|
||||||
pub workspace_id: Uuid,
|
pub workspace_id: Uuid,
|
||||||
|
|
||||||
|
/// Backend ID used for this mission
|
||||||
|
pub backend_id: String,
|
||||||
|
|
||||||
/// Current state
|
/// Current state
|
||||||
pub state: MissionRunState,
|
pub state: MissionRunState,
|
||||||
|
|
||||||
@@ -111,10 +114,16 @@ pub struct MissionRunner {
|
|||||||
|
|
||||||
impl MissionRunner {
|
impl MissionRunner {
|
||||||
/// Create a new mission runner.
|
/// Create a new mission runner.
|
||||||
pub fn new(mission_id: Uuid, workspace_id: Uuid, agent_override: Option<String>) -> Self {
|
pub fn new(
|
||||||
|
mission_id: Uuid,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
agent_override: Option<String>,
|
||||||
|
backend_id: Option<String>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mission_id,
|
mission_id,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
|
backend_id: backend_id.unwrap_or_else(|| "opencode".to_string()),
|
||||||
state: MissionRunState::Queued,
|
state: MissionRunState::Queued,
|
||||||
agent_override,
|
agent_override,
|
||||||
queue: VecDeque::new(),
|
queue: VecDeque::new(),
|
||||||
@@ -239,6 +248,7 @@ impl MissionRunner {
|
|||||||
let mission_id = self.mission_id;
|
let mission_id = self.mission_id;
|
||||||
let workspace_id = self.workspace_id;
|
let workspace_id = self.workspace_id;
|
||||||
let agent_override = self.agent_override.clone();
|
let agent_override = self.agent_override.clone();
|
||||||
|
let backend_id = self.backend_id.clone();
|
||||||
let user_message = msg.content.clone();
|
let user_message = msg.content.clone();
|
||||||
let msg_id = msg.id;
|
let msg_id = msg.id;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -282,6 +292,7 @@ impl MissionRunner {
|
|||||||
progress_ref,
|
progress_ref,
|
||||||
mission_id,
|
mission_id,
|
||||||
Some(workspace_id),
|
Some(workspace_id),
|
||||||
|
backend_id,
|
||||||
agent_override,
|
agent_override,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -384,6 +395,7 @@ async fn run_mission_turn(
|
|||||||
progress_snapshot: Arc<RwLock<ExecutionProgress>>,
|
progress_snapshot: Arc<RwLock<ExecutionProgress>>,
|
||||||
mission_id: Uuid,
|
mission_id: Uuid,
|
||||||
workspace_id: Option<Uuid>,
|
workspace_id: Option<Uuid>,
|
||||||
|
backend_id: String,
|
||||||
agent_override: Option<String>,
|
agent_override: Option<String>,
|
||||||
) -> AgentResult {
|
) -> AgentResult {
|
||||||
let mut config = config;
|
let mut config = config;
|
||||||
@@ -465,8 +477,14 @@ async fn run_mission_turn(
|
|||||||
let mission_work_dir = match {
|
let mission_work_dir = match {
|
||||||
let lib_guard = library.read().await;
|
let lib_guard = library.read().await;
|
||||||
let lib_ref = lib_guard.as_ref().map(|l| l.as_ref());
|
let lib_ref = lib_guard.as_ref().map(|l| l.as_ref());
|
||||||
workspace::prepare_mission_workspace_with_skills(&workspace, &mcp, lib_ref, mission_id)
|
workspace::prepare_mission_workspace_with_skills_backend(
|
||||||
.await
|
&workspace,
|
||||||
|
&mcp,
|
||||||
|
lib_ref,
|
||||||
|
mission_id,
|
||||||
|
&backend_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
} {
|
} {
|
||||||
Ok(dir) => {
|
Ok(dir) => {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|||||||
211
src/workspace.rs
211
src/workspace.rs
@@ -692,6 +692,62 @@ fn opencode_entry_from_mcp(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn claude_entry_from_mcp(
|
||||||
|
config: &McpServerConfig,
|
||||||
|
workspace_dir: &Path,
|
||||||
|
workspace_root: &Path,
|
||||||
|
workspace_type: WorkspaceType,
|
||||||
|
workspace_env: &HashMap<String, String>,
|
||||||
|
shared_network: Option<bool>,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
match &config.transport {
|
||||||
|
McpTransport::Http { endpoint, headers } => {
|
||||||
|
let mut entry = serde_json::Map::new();
|
||||||
|
entry.insert("url".to_string(), json!(endpoint));
|
||||||
|
if !headers.is_empty() {
|
||||||
|
entry.insert("headers".to_string(), json!(headers));
|
||||||
|
}
|
||||||
|
serde_json::Value::Object(entry)
|
||||||
|
}
|
||||||
|
McpTransport::Stdio { .. } => {
|
||||||
|
let opencode_entry = opencode_entry_from_mcp(
|
||||||
|
config,
|
||||||
|
workspace_dir,
|
||||||
|
workspace_root,
|
||||||
|
workspace_type,
|
||||||
|
workspace_env,
|
||||||
|
shared_network,
|
||||||
|
);
|
||||||
|
|
||||||
|
let command_vec = opencode_entry
|
||||||
|
.get("command")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let command = command_vec
|
||||||
|
.first()
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
let args: Vec<String> = command_vec
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut entry = serde_json::Map::new();
|
||||||
|
entry.insert("command".to_string(), json!(command));
|
||||||
|
entry.insert("args".to_string(), json!(args));
|
||||||
|
|
||||||
|
if let Some(env) = opencode_entry.get("environment").and_then(|v| v.as_object()) {
|
||||||
|
entry.insert("env".to_string(), serde_json::Value::Object(env.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::Value::Object(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn write_opencode_config(
|
async fn write_opencode_config(
|
||||||
workspace_dir: &Path,
|
workspace_dir: &Path,
|
||||||
mcp_configs: Vec<McpServerConfig>,
|
mcp_configs: Vec<McpServerConfig>,
|
||||||
@@ -810,6 +866,7 @@ async fn write_claudecode_config(
|
|||||||
workspace_type: WorkspaceType,
|
workspace_type: WorkspaceType,
|
||||||
workspace_env: &HashMap<String, String>,
|
workspace_env: &HashMap<String, String>,
|
||||||
skill_contents: Option<&[SkillContent]>,
|
skill_contents: Option<&[SkillContent]>,
|
||||||
|
shared_network: Option<bool>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// Create .claude directory
|
// Create .claude directory
|
||||||
let claude_dir = workspace_dir.join(".claude");
|
let claude_dir = workspace_dir.join(".claude");
|
||||||
@@ -817,64 +874,24 @@ async fn write_claudecode_config(
|
|||||||
|
|
||||||
// Build MCP servers config in Claude Code format
|
// Build MCP servers config in Claude Code format
|
||||||
let mut mcp_servers = serde_json::Map::new();
|
let mut mcp_servers = serde_json::Map::new();
|
||||||
|
let mut used = std::collections::HashSet::new();
|
||||||
|
|
||||||
let filtered_configs = mcp_configs.into_iter().filter(|c| c.enabled);
|
let filtered_configs = mcp_configs.into_iter().filter(|c| c.enabled);
|
||||||
|
|
||||||
for config in filtered_configs {
|
for config in filtered_configs {
|
||||||
let server_config = match &config.transport {
|
let base = sanitize_key(&config.name);
|
||||||
McpTransport::Stdio(stdio) => {
|
let key = unique_key(&base, &mut used);
|
||||||
// For container workspaces, wrap commands with nspawn
|
mcp_servers.insert(
|
||||||
let (command, args) = match workspace_type {
|
key,
|
||||||
WorkspaceType::Chroot => {
|
claude_entry_from_mcp(
|
||||||
let container_name = workspace_root
|
&config,
|
||||||
.file_name()
|
workspace_dir,
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
workspace_root,
|
||||||
.unwrap_or_else(|| "default".to_string());
|
workspace_type,
|
||||||
let mut nspawn_args = vec![
|
workspace_env,
|
||||||
"-q".to_string(),
|
shared_network,
|
||||||
"-D".to_string(),
|
),
|
||||||
workspace_root.to_string_lossy().to_string(),
|
);
|
||||||
"-M".to_string(),
|
|
||||||
container_name,
|
|
||||||
"--".to_string(),
|
|
||||||
];
|
|
||||||
nspawn_args.extend(stdio.command.iter().cloned());
|
|
||||||
nspawn_args.extend(stdio.args.iter().cloned());
|
|
||||||
("systemd-nspawn".to_string(), nspawn_args)
|
|
||||||
}
|
|
||||||
WorkspaceType::Host => {
|
|
||||||
let command = stdio.command.first().cloned().unwrap_or_default();
|
|
||||||
let args: Vec<String> = stdio
|
|
||||||
.command
|
|
||||||
.iter()
|
|
||||||
.skip(1)
|
|
||||||
.chain(stdio.args.iter())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
(command, args)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Merge environment variables
|
|
||||||
let mut env = workspace_env.clone();
|
|
||||||
env.extend(stdio.env.clone());
|
|
||||||
|
|
||||||
json!({
|
|
||||||
"command": command,
|
|
||||||
"args": args,
|
|
||||||
"env": env,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
McpTransport::Http(http) => {
|
|
||||||
json!({
|
|
||||||
"url": http.endpoint,
|
|
||||||
"headers": http.headers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let key = sanitize_key(&config.name);
|
|
||||||
mcp_servers.insert(key, server_config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write settings.local.json
|
// Write settings.local.json
|
||||||
@@ -887,10 +904,15 @@ async fn write_claudecode_config(
|
|||||||
|
|
||||||
// Generate CLAUDE.md from skills
|
// Generate CLAUDE.md from skills
|
||||||
if let Some(skills) = skill_contents {
|
if let Some(skills) = skill_contents {
|
||||||
if !skills.is_empty() {
|
let claude_md_path = workspace_dir.join("CLAUDE.md");
|
||||||
|
if skills.is_empty() {
|
||||||
|
let _ = tokio::fs::remove_file(&claude_md_path).await;
|
||||||
|
} else {
|
||||||
let mut claude_md = String::new();
|
let mut claude_md = String::new();
|
||||||
claude_md.push_str("# Project Context\n\n");
|
claude_md.push_str("# Project Context\n\n");
|
||||||
claude_md.push_str("This file was generated by Open Agent. It contains context from configured skills.\n\n");
|
claude_md.push_str(
|
||||||
|
"This file was generated by Open Agent. It contains context from configured skills.\n\n",
|
||||||
|
);
|
||||||
|
|
||||||
for skill in skills {
|
for skill in skills {
|
||||||
claude_md.push_str(&format!("## {}\n\n", skill.name));
|
claude_md.push_str(&format!("## {}\n\n", skill.name));
|
||||||
@@ -908,7 +930,6 @@ async fn write_claudecode_config(
|
|||||||
claude_md.push_str("\n\n");
|
claude_md.push_str("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
let claude_md_path = workspace_dir.join("CLAUDE.md");
|
|
||||||
tokio::fs::write(&claude_md_path, claude_md).await?;
|
tokio::fs::write(&claude_md_path, claude_md).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,6 +964,17 @@ pub async fn write_backend_config(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
"claudecode" => {
|
"claudecode" => {
|
||||||
|
// Keep OpenCode config in sync for compatibility with existing execution pipeline.
|
||||||
|
write_opencode_config(
|
||||||
|
workspace_dir,
|
||||||
|
mcp_configs.clone(),
|
||||||
|
workspace_root,
|
||||||
|
workspace_type,
|
||||||
|
workspace_env,
|
||||||
|
skill_allowlist,
|
||||||
|
shared_network,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
write_claudecode_config(
|
write_claudecode_config(
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
mcp_configs,
|
mcp_configs,
|
||||||
@@ -950,6 +982,7 @@ pub async fn write_backend_config(
|
|||||||
workspace_type,
|
workspace_type,
|
||||||
workspace_env,
|
workspace_env,
|
||||||
skill_contents,
|
skill_contents,
|
||||||
|
shared_network,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -1173,6 +1206,25 @@ pub async fn sync_skills_to_dir(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let skills_to_write = collect_skill_contents(skill_names, context_name, library).await;
|
||||||
|
|
||||||
|
write_skills_to_workspace(target_dir, &skills_to_write).await?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
context = %context_name,
|
||||||
|
skills = ?skill_names,
|
||||||
|
target = %target_dir.display(),
|
||||||
|
"Synced skills to directory"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_skill_contents(
|
||||||
|
skill_names: &[String],
|
||||||
|
context_name: &str,
|
||||||
|
library: &LibraryStore,
|
||||||
|
) -> Vec<SkillContent> {
|
||||||
let mut skills_to_write: Vec<SkillContent> = Vec::new();
|
let mut skills_to_write: Vec<SkillContent> = Vec::new();
|
||||||
|
|
||||||
for skill_name in skill_names {
|
for skill_name in skill_names {
|
||||||
@@ -1200,16 +1252,7 @@ pub async fn sync_skills_to_dir(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write_skills_to_workspace(target_dir, &skills_to_write).await?;
|
skills_to_write
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
context = %context_name,
|
|
||||||
skills = ?skill_names,
|
|
||||||
target = %target_dir.display(),
|
|
||||||
"Synced skills to directory"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tool content to be written to the workspace.
|
/// Tool content to be written to the workspace.
|
||||||
@@ -1500,6 +1543,24 @@ pub async fn prepare_mission_workspace_with_skills(
|
|||||||
mcp: &McpRegistry,
|
mcp: &McpRegistry,
|
||||||
library: Option<&LibraryStore>,
|
library: Option<&LibraryStore>,
|
||||||
mission_id: Uuid,
|
mission_id: Uuid,
|
||||||
|
) -> anyhow::Result<PathBuf> {
|
||||||
|
prepare_mission_workspace_with_skills_backend(
|
||||||
|
workspace,
|
||||||
|
mcp,
|
||||||
|
library,
|
||||||
|
mission_id,
|
||||||
|
"opencode",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare a workspace directory for a mission with skill and tool syncing for a specific backend.
|
||||||
|
pub async fn prepare_mission_workspace_with_skills_backend(
|
||||||
|
workspace: &Workspace,
|
||||||
|
mcp: &McpRegistry,
|
||||||
|
library: Option<&LibraryStore>,
|
||||||
|
mission_id: Uuid,
|
||||||
|
backend_id: &str,
|
||||||
) -> anyhow::Result<PathBuf> {
|
) -> anyhow::Result<PathBuf> {
|
||||||
let dir = mission_workspace_dir_for_root(&workspace.path, mission_id);
|
let dir = mission_workspace_dir_for_root(&workspace.path, mission_id);
|
||||||
prepare_workspace_dir(&dir).await?;
|
prepare_workspace_dir(&dir).await?;
|
||||||
@@ -1509,13 +1570,27 @@ pub async fn prepare_mission_workspace_with_skills(
|
|||||||
} else {
|
} else {
|
||||||
Some(workspace.skills.as_slice())
|
Some(workspace.skills.as_slice())
|
||||||
};
|
};
|
||||||
write_opencode_config(
|
let mut skill_contents: Option<Vec<SkillContent>> = None;
|
||||||
|
|
||||||
|
if backend_id == "claudecode" {
|
||||||
|
if let Some(lib) = library {
|
||||||
|
let context = format!("mission-{}", mission_id);
|
||||||
|
let skill_names =
|
||||||
|
resolve_workspace_skill_names(workspace, lib).await.unwrap_or_default();
|
||||||
|
let skills = collect_skill_contents(&skill_names, &context, lib).await;
|
||||||
|
skill_contents = Some(skills);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_backend_config(
|
||||||
&dir,
|
&dir,
|
||||||
|
backend_id,
|
||||||
mcp_configs,
|
mcp_configs,
|
||||||
&workspace.path,
|
&workspace.path,
|
||||||
workspace.workspace_type,
|
workspace.workspace_type,
|
||||||
&workspace.env_vars,
|
&workspace.env_vars,
|
||||||
skill_allowlist,
|
skill_allowlist,
|
||||||
|
skill_contents.as_deref(),
|
||||||
workspace.shared_network,
|
workspace.shared_network,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user