@@ -226,7 +226,7 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
226226 // Default: update status to deploying and call runtime.
227227 e .DB .Model (& team ).Update ("status" , models .TeamStatusDeploying )
228228
229- // Simplified deployment — just deploy infrastructure and leader .
229+ // Deploy infrastructure.
230230 infraCfg := runtime.InfraConfig {
231231 TeamName : team .Name ,
232232 NATSEnabled : true ,
@@ -238,11 +238,30 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
238238 return fmt .Errorf ("deploying infrastructure: %w" , err )
239239 }
240240
241- // Find the leader.
241+ provider := team .Provider
242+ if provider == "" {
243+ provider = models .ProviderClaude
244+ }
245+
246+ // Load settings from DB for environment variables.
247+ env := map [string ]string {}
248+ if e .LoadSettingsEnvFunc != nil {
249+ env = e .LoadSettingsEnvFunc ()
250+ }
251+
252+ natsURL := e .Runtime .GetNATSURL (team .Name )
253+
254+ // Find the leader and extract leader skills.
242255 var leader * models.Agent
256+ var leaderSkills json.RawMessage
257+ var leaderSkillConfigs []protocol.SkillConfig
243258 for i := range team .Agents {
244259 if team .Agents [i ].Role == models .AgentRoleLeader {
245260 leader = & team .Agents [i ]
261+ if len (leader .SubAgentSkills ) > 0 && string (leader .SubAgentSkills ) != "null" {
262+ leaderSkills = json .RawMessage (leader .SubAgentSkills )
263+ _ = json .Unmarshal (leader .SubAgentSkills , & leaderSkillConfigs )
264+ }
246265 break
247266 }
248267 }
@@ -251,20 +270,200 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
251270 return fmt .Errorf ("no leader agent found in team" )
252271 }
253272
254- env := map [string ]string {}
255- if e .LoadSettingsEnvFunc != nil {
256- env = e .LoadSettingsEnvFunc ()
273+ // Build team member list for the leader's instructions.
274+ var teamMembers []runtime.TeamMemberInfo
275+ for _ , a := range team .Agents {
276+ teamMembers = append (teamMembers , runtime.TeamMemberInfo {
277+ Name : sanitizeTeamName (a .Name ),
278+ Role : a .Role ,
279+ Specialty : a .Specialty ,
280+ })
281+ }
282+
283+ // Generate sub-agent files for workers based on provider.
284+ subAgentFiles := map [string ]string {}
285+ var openCodeWorkers []runtime.SubAgentInfo
286+ for i := range team .Agents {
287+ agent := & team .Agents [i ]
288+ if agent .Role == models .AgentRoleLeader {
289+ continue
290+ }
291+
292+ if provider == models .ProviderOpenCode {
293+ subInfo := runtime.SubAgentInfo {
294+ Name : agent .Name ,
295+ Description : agent .SubAgentDescription ,
296+ Model : agent .SubAgentModel ,
297+ Skills : json .RawMessage (agent .SubAgentSkills ),
298+ ClaudeMD : agent .InstructionsMD ,
299+ }
300+ filename := runtime .SubAgentFileName (agent .Name )
301+ subAgentFiles [filename ] = runtime .GenerateOpenCodeSubAgentContent (subInfo , leaderSkillConfigs )
302+ openCodeWorkers = append (openCodeWorkers , subInfo )
303+ } else {
304+ info := runtime.AgentWorkspaceInfo {
305+ Name : agent .Name ,
306+ Role : agent .Role ,
307+ Specialty : agent .Specialty ,
308+ SystemPrompt : agent .SystemPrompt ,
309+ ClaudeMD : agent .InstructionsMD ,
310+ Skills : json .RawMessage (agent .Skills ),
311+ }
312+ subInfo := runtime.SubAgentInfo {
313+ Name : agent .Name ,
314+ Description : agent .SubAgentDescription ,
315+ Model : agent .SubAgentModel ,
316+ Skills : json .RawMessage (agent .SubAgentSkills ),
317+ GlobalSkills : leaderSkills ,
318+ ClaudeMD : agent .InstructionsMD ,
319+ }
320+ if subInfo .ClaudeMD == "" {
321+ subInfo .ClaudeMD = runtime .GenerateClaudeMD (info )
322+ }
323+ filename := runtime .SubAgentFileName (agent .Name )
324+ subAgentFiles [filename ] = runtime .GenerateSubAgentContent (subInfo )
325+
326+ if team .WorkspacePath != "" {
327+ if _ , err := runtime .SetupSubAgentFile (team .WorkspacePath , subInfo ); err != nil {
328+ slog .Error ("executor: failed to setup sub-agent file" , "agent" , agent .Name , "error" , err )
329+ }
330+ }
331+ }
332+ }
333+
334+ // Setup host workspace for the leader based on provider.
335+ if team .WorkspacePath != "" {
336+ if provider == models .ProviderOpenCode {
337+ leaderSub := runtime.SubAgentInfo {
338+ Name : leader .Name ,
339+ Description : leader .Specialty ,
340+ Skills : json .RawMessage (leader .Skills ),
341+ ClaudeMD : leader .InstructionsMD ,
342+ }
343+ if err := runtime .SetupOpenCodeWorkspace (team .WorkspacePath , team .Name , leaderSub , openCodeWorkers , leaderSkillConfigs ); err != nil {
344+ slog .Error ("executor: failed to setup opencode workspace" , "team" , team .Name , "error" , err )
345+ }
346+ } else {
347+ info := runtime.AgentWorkspaceInfo {
348+ Name : leader .Name ,
349+ Role : leader .Role ,
350+ Specialty : leader .Specialty ,
351+ SystemPrompt : leader .SystemPrompt ,
352+ ClaudeMD : leader .InstructionsMD ,
353+ Skills : json .RawMessage (leader .Skills ),
354+ TeamMembers : teamMembers ,
355+ }
356+ if _ , err := runtime .SetupAgentWorkspace (team .WorkspacePath , info ); err != nil {
357+ slog .Error ("executor: failed to setup agent workspace" , "agent" , leader .Name , "error" , err )
358+ }
359+ }
360+ }
361+
362+ // Collect all unique skills from all agents for sidecar installation.
363+ type skillKey struct { RepoURL , SkillName string }
364+ skillsSet := map [skillKey ]struct {}{}
365+ var allSkills []protocol.SkillConfig
366+ for _ , a := range team .Agents {
367+ var agentSkills []protocol.SkillConfig
368+ if err := json .Unmarshal (a .SubAgentSkills , & agentSkills ); err == nil {
369+ for _ , s := range agentSkills {
370+ key := skillKey {s .RepoURL , s .SkillName }
371+ if s .RepoURL != "" && s .SkillName != "" {
372+ if _ , exists := skillsSet [key ]; ! exists {
373+ skillsSet [key ] = struct {}{}
374+ allSkills = append (allSkills , s )
375+ }
376+ }
377+ }
378+ }
379+ }
380+ if len (allSkills ) > 0 {
381+ skillsJSON , _ := json .Marshal (allSkills )
382+ env ["AGENT_SKILLS_INSTALL" ] = string (skillsJSON )
383+ }
384+
385+ // Set model env var based on provider.
386+ leaderModel := leader .SubAgentModel
387+ if leaderModel != "" && leaderModel != "inherit" {
388+ if provider == models .ProviderOpenCode {
389+ env ["OPENCODE_MODEL" ] = leaderModel
390+ } else {
391+ if fullModel := schedulerClaudeModelID (leaderModel ); fullModel != "" {
392+ env ["CLAUDE_MODEL" ] = fullModel
393+ }
394+ }
395+ } else if provider == models .ProviderOpenCode {
396+ if m := env ["OPENCODE_MODEL" ]; m != "" {
397+ env ["OPENCODE_MODEL" ] = m
398+ }
399+ }
400+
401+ // Validate OpenCode model credentials before deployment.
402+ if provider == models .ProviderOpenCode {
403+ effectiveModel := env ["OPENCODE_MODEL" ]
404+ if err := schedulerValidateOpenCodeCredentials (effectiveModel , env ); err != nil {
405+ e .DB .Model (& team ).Update ("status" , models .TeamStatusError )
406+ return fmt .Errorf ("credential validation: %w" , err )
407+ }
408+ for _ , a := range team .Agents {
409+ if a .Role == models .AgentRoleWorker && a .SubAgentModel != "" && a .SubAgentModel != "inherit" {
410+ if err := schedulerValidateOpenCodeCredentials (a .SubAgentModel , env ); err != nil {
411+ e .DB .Model (& team ).Update ("status" , models .TeamStatusError )
412+ return fmt .Errorf ("credential validation for worker %s: %w" , a .Name , err )
413+ }
414+ }
415+ }
416+ }
417+
418+ // Generate leader instructions content based on provider.
419+ var instructionsMDContent string
420+ if provider == models .ProviderOpenCode {
421+ if leader .InstructionsMD != "" {
422+ instructionsMDContent = leader .InstructionsMD
423+ } else {
424+ leaderSubInfo := runtime.SubAgentInfo {
425+ Name : leader .Name ,
426+ Description : leader .Specialty ,
427+ Skills : json .RawMessage (leader .Skills ),
428+ ClaudeMD : leader .InstructionsMD ,
429+ }
430+ workers := make ([]runtime.SubAgentInfo , 0 )
431+ for _ , a := range team .Agents {
432+ if a .Role != models .AgentRoleLeader {
433+ workers = append (workers , runtime.SubAgentInfo {
434+ Name : a .Name ,
435+ Description : a .SubAgentDescription ,
436+ })
437+ }
438+ }
439+ instructionsMDContent = runtime .GenerateOpenCodeAgentsMD (team .Name , leaderSubInfo , workers )
440+ }
441+ } else {
442+ leaderInfo := runtime.AgentWorkspaceInfo {
443+ Name : leader .Name ,
444+ Role : leader .Role ,
445+ Specialty : leader .Specialty ,
446+ SystemPrompt : leader .SystemPrompt ,
447+ ClaudeMD : leader .InstructionsMD ,
448+ Skills : json .RawMessage (leader .Skills ),
449+ TeamMembers : teamMembers ,
450+ }
451+ instructionsMDContent = leader .InstructionsMD
452+ if instructionsMDContent == "" {
453+ instructionsMDContent = runtime .GenerateClaudeMD (leaderInfo )
454+ }
257455 }
258456
259- natsURL := e .Runtime .GetNATSURL (team .Name )
260457 agentCfg := runtime.AgentConfig {
261458 Name : leader .Name ,
262459 TeamName : team .Name ,
263460 Role : leader .Role ,
461+ Provider : provider ,
264462 SystemPrompt : leader .SystemPrompt ,
265- ClaudeMD : leader . InstructionsMD ,
463+ ClaudeMD : instructionsMDContent ,
266464 NATSUrl : natsURL ,
267465 WorkspacePath : team .WorkspacePath ,
466+ SubAgentFiles : subAgentFiles ,
268467 Env : env ,
269468 }
270469
@@ -283,6 +482,47 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
283482 return nil
284483}
285484
485+ // schedulerClaudeModelID maps short model names to full Claude model IDs.
486+ func schedulerClaudeModelID (short string ) string {
487+ switch short {
488+ case "sonnet" :
489+ return "claude-sonnet-4-20250514"
490+ case "opus" :
491+ return "claude-opus-4-20250514"
492+ case "haiku" :
493+ return "claude-haiku-4-5-20251001"
494+ default :
495+ return ""
496+ }
497+ }
498+
499+ // schedulerValidateOpenCodeCredentials checks that the required API key for the
500+ // given OpenCode model is present in the environment.
501+ func schedulerValidateOpenCodeCredentials (model string , env map [string ]string ) error {
502+ if model == "" || model == "inherit" {
503+ return nil
504+ }
505+ parts := strings .SplitN (model , "/" , 2 )
506+ if len (parts ) < 2 {
507+ return nil
508+ }
509+ required := map [string ]string {
510+ "anthropic" : "ANTHROPIC_API_KEY" ,
511+ "openai" : "OPENAI_API_KEY" ,
512+ "google" : "GOOGLE_GENERATIVE_AI_API_KEY" ,
513+ "ollama" : "OLLAMA_BASE_URL" ,
514+ "lmstudio" : "LM_STUDIO_BASE_URL" ,
515+ }
516+ key , ok := required [parts [0 ]]
517+ if ! ok {
518+ return nil
519+ }
520+ if env [key ] == "" {
521+ return fmt .Errorf ("missing credential for model %q: set %s in the Settings page" , model , key )
522+ }
523+ return nil
524+ }
525+
286526// stopTeam stops a running team.
287527func (e * Executor ) stopTeam (ctx context.Context , team models.Team ) error {
288528 if e .StopTeamFunc != nil {
0 commit comments