feat(orchestrator): create_skill action + wire per-project watchers (§14.3)

- domain: OrchestratorCommand::CreateSkill with optional scope field
  (defaults to Project, case-insensitive, UnknownScope on bad value)
- application: dispatch CreateSkill through the CreateSkill use case
- app-tauri: build OrchestratorService and start/stop per-project request
  watchers on open/create/close_project (ensure/stop_orchestrator_watch)
- tests: domain validation, service dispatch, infra watcher e2e,
  app-tauri wiring lifecycle (idempotent, isolated, stop)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:06:41 +02:00
parent b9fd2fb925
commit d11eaaa8c0
13 changed files with 619 additions and 52 deletions

View File

@ -23,6 +23,7 @@ use crate::agent::{
ListAgentsInput, UpdateAgentContext, UpdateAgentContextInput,
};
use crate::error::AppError;
use crate::skill::{CreateSkill, CreateSkillInput};
use crate::terminal::{CloseTerminal, CloseTerminalInput, TerminalSessions};
/// Default terminal geometry for an orchestrator-launched agent cell. The UI
@ -39,6 +40,7 @@ pub struct OrchestratorService {
list_agents: Arc<ListAgents>,
close_terminal: Arc<CloseTerminal>,
update_context: Arc<UpdateAgentContext>,
create_skill: Arc<CreateSkill>,
profiles: Arc<dyn ProfileStore>,
sessions: Arc<TerminalSessions>,
}
@ -61,6 +63,7 @@ impl OrchestratorService {
list_agents: Arc<ListAgents>,
close_terminal: Arc<CloseTerminal>,
update_context: Arc<UpdateAgentContext>,
create_skill: Arc<CreateSkill>,
profiles: Arc<dyn ProfileStore>,
sessions: Arc<TerminalSessions>,
) -> Self {
@ -70,6 +73,7 @@ impl OrchestratorService {
list_agents,
close_terminal,
update_context,
create_skill,
profiles,
sessions,
}
@ -96,6 +100,11 @@ impl OrchestratorService {
OrchestratorCommand::UpdateAgentContext { name, context } => {
self.update_agent_context(project, name, context).await
}
OrchestratorCommand::CreateSkill {
name,
content,
scope,
} => self.create_skill(project, name, content, scope).await,
}
}
@ -193,6 +202,32 @@ impl OrchestratorService {
})
}
/// `create_skill`: create a reusable skill in the requested scope — the same
/// path the UI's "New skill" action takes. For [`SkillScope::Project`] the
/// skill lands under `<root>/.ideai/skills/`; for [`SkillScope::Global`] the
/// project root is ignored by the store.
async fn create_skill(
&self,
project: &Project,
name: String,
content: String,
scope: domain::SkillScope,
) -> Result<OrchestratorOutcome, AppError> {
let created = self
.create_skill
.execute(CreateSkillInput {
name: name.clone(),
content,
scope,
project_root: project.root.clone(),
})
.await?;
Ok(OrchestratorOutcome {
detail: format!("created skill {} ({:?})", created.skill.name, scope),
})
}
/// Finds an agent id by display name (case-insensitive) in the project manifest.
async fn find_agent_id_by_name(
&self,