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

@ -32,8 +32,8 @@ use domain::{PtySize, SessionId};
use uuid::Uuid;
use application::{
CloseTerminal, CreateAgentFromScratch, LaunchAgent, ListAgents, OrchestratorService,
TerminalSessions, UpdateAgentContext,
CloseTerminal, CreateAgentFromScratch, CreateSkill, LaunchAgent, ListAgents,
OrchestratorService, TerminalSessions, UpdateAgentContext,
};
use infrastructure::{process_request_file, OrchestratorResponse};
@ -305,12 +305,17 @@ fn build_service(contexts: FakeContexts) -> Arc<OrchestratorService> {
let list = Arc::new(ListAgents::new(Arc::new(contexts.clone())));
let close = Arc::new(CloseTerminal::new(Arc::new(FakePty), Arc::clone(&sessions)));
let update = Arc::new(UpdateAgentContext::new(Arc::new(contexts)));
let create_skill = Arc::new(CreateSkill::new(
Arc::new(FakeSkills) as Arc<dyn SkillStore>,
Arc::new(SeqIds(Mutex::new(1))),
));
Arc::new(OrchestratorService::new(
create,
launch,
list,
close,
update,
create_skill,
Arc::clone(&profiles) as Arc<dyn ProfileStore>,
sessions,
))
@ -350,6 +355,27 @@ async fn valid_spawn_request_succeeds_and_is_consumed() {
assert_eq!(contexts.entries()[0].name, "dev-backend");
}
#[tokio::test]
async fn valid_create_skill_request_succeeds_and_is_consumed() {
let tmp = TempDir::new();
let service = build_service(FakeContexts::new());
let req = tmp.0.join("skill-req.json");
std::fs::write(
&req,
br##"{ "action": "create_skill", "name": "deploy", "context": "# Deploy steps" }"##,
)
.unwrap();
let response = process_request_file(&req, &project(), &service).await;
assert!(response.ok, "expected ok, got {response:?}");
assert_eq!(response.action.as_deref(), Some("create_skill"));
// Request consumed; a response sibling written and marked ok.
assert!(!req.exists(), "request file must be removed");
assert!(read_response(&req).ok);
}
#[tokio::test]
async fn invalid_json_request_yields_error_response() {
let tmp = TempDir::new();