1#![cfg_attr(not(test), allow(dead_code))]
2
3use std::collections::{BTreeMap, BTreeSet};
21
22use super::config::RoleType;
23use super::hierarchy::MemberInstance;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub enum WorkflowCapability {
27 Planner,
28 Dispatcher,
29 Executor,
30 Reviewer,
31 Orchestrator,
32 Operator,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub enum CapabilitySubject {
37 Member(String),
38 Orchestrator,
39 Operator,
40}
41
42pub type CapabilitySet = BTreeSet<WorkflowCapability>;
43pub type CapabilityMap = BTreeMap<CapabilitySubject, CapabilitySet>;
44
45pub fn resolve_member_capabilities(
50 member: &MemberInstance,
51 members: &[MemberInstance],
52) -> CapabilitySet {
53 let has_architect = members
54 .iter()
55 .any(|candidate| candidate.role_type == RoleType::Architect);
56 let has_manager = members
57 .iter()
58 .any(|candidate| candidate.role_type == RoleType::Manager);
59 let is_top_level = member.reports_to.is_none();
60
61 let mut capabilities = CapabilitySet::new();
62 match member.role_type {
63 RoleType::User => {}
64 RoleType::Architect => {
65 capabilities.insert(WorkflowCapability::Planner);
66 capabilities.insert(WorkflowCapability::Reviewer);
67 if !has_manager {
68 capabilities.insert(WorkflowCapability::Dispatcher);
69 }
70 }
71 RoleType::Manager => {
72 capabilities.insert(WorkflowCapability::Dispatcher);
73 capabilities.insert(WorkflowCapability::Reviewer);
74 if !has_architect && is_top_level {
75 capabilities.insert(WorkflowCapability::Planner);
76 }
77 }
78 RoleType::Engineer => {
79 capabilities.insert(WorkflowCapability::Executor);
80 if !has_architect && !has_manager && is_top_level {
81 capabilities.insert(WorkflowCapability::Planner);
82 capabilities.insert(WorkflowCapability::Dispatcher);
83 }
84 }
85 }
86
87 capabilities
88}
89
90pub fn resolve_capability_map(members: &[MemberInstance]) -> CapabilityMap {
96 let mut map = CapabilityMap::new();
97
98 for member in members {
99 if member.role_type == RoleType::User {
100 continue;
101 }
102 let capabilities = resolve_member_capabilities(member, members);
103 if !capabilities.is_empty() {
104 map.insert(CapabilitySubject::Member(member.name.clone()), capabilities);
105 }
106 }
107
108 let mut operator_capabilities = CapabilitySet::from([WorkflowCapability::Operator]);
109 if !has_agent_reviewer(members) {
110 operator_capabilities.insert(WorkflowCapability::Reviewer);
111 }
112 map.insert(CapabilitySubject::Operator, operator_capabilities);
113 map.insert(
114 CapabilitySubject::Orchestrator,
115 CapabilitySet::from([WorkflowCapability::Orchestrator]),
116 );
117
118 map
119}
120
121fn has_agent_reviewer(members: &[MemberInstance]) -> bool {
122 members.iter().any(|member| {
123 matches!(member.role_type, RoleType::Architect | RoleType::Manager)
124 && !resolve_member_capabilities(member, members).is_empty()
125 && resolve_member_capabilities(member, members).contains(&WorkflowCapability::Reviewer)
126 })
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::team::config::TeamConfig;
133 use crate::team::hierarchy;
134
135 fn members_from_yaml(yaml: &str) -> Vec<MemberInstance> {
136 let config: TeamConfig = serde_yaml::from_str(yaml).unwrap();
137 hierarchy::resolve_hierarchy(&config).unwrap()
138 }
139
140 fn member_capabilities(map: &CapabilityMap, member_name: &str) -> CapabilitySet {
141 map.get(&CapabilitySubject::Member(member_name.to_string()))
142 .cloned()
143 .unwrap_or_default()
144 }
145
146 fn capability_set(capabilities: &[WorkflowCapability]) -> CapabilitySet {
147 capabilities.iter().copied().collect()
148 }
149
150 #[test]
151 fn solo_topology_falls_back_to_operator_review() {
152 let members = members_from_yaml(
153 r#"
154name: solo
155roles:
156 - name: builder
157 role_type: engineer
158 agent: codex
159 instances: 1
160"#,
161 );
162
163 let capability_map = resolve_capability_map(&members);
164
165 assert_eq!(
166 member_capabilities(&capability_map, "builder"),
167 capability_set(&[
168 WorkflowCapability::Planner,
169 WorkflowCapability::Dispatcher,
170 WorkflowCapability::Executor,
171 ])
172 );
173 assert_eq!(
174 capability_map
175 .get(&CapabilitySubject::Operator)
176 .cloned()
177 .unwrap(),
178 capability_set(&[WorkflowCapability::Operator, WorkflowCapability::Reviewer])
179 );
180 assert_eq!(
181 capability_map
182 .get(&CapabilitySubject::Orchestrator)
183 .cloned()
184 .unwrap(),
185 capability_set(&[WorkflowCapability::Orchestrator])
186 );
187 }
188
189 #[test]
190 fn pair_topology_assigns_architect_planning_dispatch_and_review() {
191 let members = members_from_yaml(
192 r#"
193name: pair
194roles:
195 - name: planner
196 role_type: architect
197 agent: claude
198 instances: 1
199 - name: builder
200 role_type: engineer
201 agent: codex
202 instances: 1
203"#,
204 );
205
206 let capability_map = resolve_capability_map(&members);
207
208 assert_eq!(
209 member_capabilities(&capability_map, "planner"),
210 capability_set(&[
211 WorkflowCapability::Planner,
212 WorkflowCapability::Dispatcher,
213 WorkflowCapability::Reviewer,
214 ])
215 );
216 assert_eq!(
217 member_capabilities(&capability_map, "builder"),
218 capability_set(&[WorkflowCapability::Executor])
219 );
220 assert_eq!(
221 capability_map
222 .get(&CapabilitySubject::Operator)
223 .cloned()
224 .unwrap(),
225 capability_set(&[WorkflowCapability::Operator])
226 );
227 }
228
229 #[test]
230 fn manager_led_topology_promotes_top_level_manager_to_planner() {
231 let members = members_from_yaml(
232 r#"
233name: manager-led
234roles:
235 - name: lead
236 role_type: manager
237 agent: claude
238 instances: 1
239 - name: implementer
240 role_type: engineer
241 agent: codex
242 instances: 2
243"#,
244 );
245
246 let capability_map = resolve_capability_map(&members);
247
248 assert_eq!(
249 member_capabilities(&capability_map, "lead"),
250 capability_set(&[
251 WorkflowCapability::Planner,
252 WorkflowCapability::Dispatcher,
253 WorkflowCapability::Reviewer,
254 ])
255 );
256 assert_eq!(
257 member_capabilities(&capability_map, "implementer-1-1"),
258 capability_set(&[WorkflowCapability::Executor])
259 );
260 assert_eq!(
261 member_capabilities(&capability_map, "implementer-1-2"),
262 capability_set(&[WorkflowCapability::Executor])
263 );
264 }
265
266 #[test]
267 fn multi_manager_topology_keeps_architect_planning_and_managers_dispatching() {
268 let members = members_from_yaml(
269 r#"
270name: squad
271roles:
272 - name: architect
273 role_type: architect
274 agent: claude
275 instances: 1
276 - name: manager
277 role_type: manager
278 agent: claude
279 instances: 2
280 - name: engineer
281 role_type: engineer
282 agent: codex
283 instances: 2
284"#,
285 );
286
287 let capability_map = resolve_capability_map(&members);
288
289 assert_eq!(
290 member_capabilities(&capability_map, "architect"),
291 capability_set(&[WorkflowCapability::Planner, WorkflowCapability::Reviewer])
292 );
293 assert_eq!(
294 member_capabilities(&capability_map, "manager-1"),
295 capability_set(&[WorkflowCapability::Dispatcher, WorkflowCapability::Reviewer])
296 );
297 assert_eq!(
298 member_capabilities(&capability_map, "manager-2"),
299 capability_set(&[WorkflowCapability::Dispatcher, WorkflowCapability::Reviewer])
300 );
301 assert_eq!(
302 member_capabilities(&capability_map, "eng-1-1"),
303 capability_set(&[WorkflowCapability::Executor])
304 );
305 assert_eq!(
306 member_capabilities(&capability_map, "eng-2-2"),
307 capability_set(&[WorkflowCapability::Executor])
308 );
309 }
310
311 #[test]
312 fn renamed_roles_resolve_from_role_type_instead_of_role_name() {
313 let members = members_from_yaml(
314 r#"
315name: renamed
316roles:
317 - name: human
318 role_type: user
319 - name: tech-lead
320 role_type: architect
321 agent: claude
322 instances: 1
323 - name: backend-mgr
324 role_type: manager
325 agent: claude
326 instances: 1
327 - name: frontend-mgr
328 role_type: manager
329 agent: claude
330 instances: 1
331 - name: developer
332 role_type: engineer
333 agent: codex
334 instances: 1
335"#,
336 );
337
338 let capability_map = resolve_capability_map(&members);
339
340 assert_eq!(
341 member_capabilities(&capability_map, "tech-lead"),
342 capability_set(&[WorkflowCapability::Planner, WorkflowCapability::Reviewer])
343 );
344 assert_eq!(
345 member_capabilities(&capability_map, "backend-mgr"),
346 capability_set(&[WorkflowCapability::Dispatcher, WorkflowCapability::Reviewer])
347 );
348 assert_eq!(
349 member_capabilities(&capability_map, "frontend-mgr"),
350 capability_set(&[WorkflowCapability::Dispatcher, WorkflowCapability::Reviewer])
351 );
352 assert_eq!(
353 member_capabilities(&capability_map, "developer-1-1"),
354 capability_set(&[WorkflowCapability::Executor])
355 );
356 }
357
358 #[test]
361 fn user_role_gets_no_capabilities_and_is_excluded_from_map() {
362 let members = members_from_yaml(
363 r#"
364name: with-user
365roles:
366 - name: human
367 role_type: user
368 - name: builder
369 role_type: engineer
370 agent: codex
371 instances: 1
372"#,
373 );
374
375 let capability_map = resolve_capability_map(&members);
376
377 assert!(!capability_map.contains_key(&CapabilitySubject::Member("human".to_string())));
379 assert_eq!(
381 member_capabilities(&capability_map, "builder"),
382 capability_set(&[
383 WorkflowCapability::Planner,
384 WorkflowCapability::Dispatcher,
385 WorkflowCapability::Executor,
386 ])
387 );
388 }
389
390 #[test]
391 fn resolve_member_capabilities_returns_empty_for_user() {
392 let member = MemberInstance {
393 name: "human".to_string(),
394 role_name: "human".to_string(),
395 role_type: RoleType::User,
396 agent: None,
397 prompt: None,
398 reports_to: None,
399 use_worktrees: false,
400 };
401 let members = vec![member.clone()];
402 let caps = resolve_member_capabilities(&member, &members);
403 assert!(caps.is_empty());
404 }
405
406 #[test]
407 fn empty_member_list_produces_operator_and_orchestrator_only() {
408 let capability_map = resolve_capability_map(&[]);
409
410 assert!(
412 !capability_map
413 .keys()
414 .any(|k| matches!(k, CapabilitySubject::Member(_)))
415 );
416
417 assert_eq!(
419 capability_map
420 .get(&CapabilitySubject::Operator)
421 .cloned()
422 .unwrap(),
423 capability_set(&[WorkflowCapability::Operator, WorkflowCapability::Reviewer])
424 );
425 assert_eq!(
426 capability_map
427 .get(&CapabilitySubject::Orchestrator)
428 .cloned()
429 .unwrap(),
430 capability_set(&[WorkflowCapability::Orchestrator])
431 );
432 }
433
434 #[test]
435 fn architect_gets_dispatch_when_no_manager_exists() {
436 let members = members_from_yaml(
437 r#"
438name: no-manager
439roles:
440 - name: arch
441 role_type: architect
442 agent: claude
443 instances: 1
444 - name: dev
445 role_type: engineer
446 agent: codex
447 instances: 1
448"#,
449 );
450
451 let capability_map = resolve_capability_map(&members);
452
453 assert!(
455 member_capabilities(&capability_map, "arch").contains(&WorkflowCapability::Dispatcher)
456 );
457 }
458
459 #[test]
460 fn architect_loses_dispatch_when_manager_present() {
461 let members = members_from_yaml(
462 r#"
463name: full-team
464roles:
465 - name: arch
466 role_type: architect
467 agent: claude
468 instances: 1
469 - name: mgr
470 role_type: manager
471 agent: claude
472 instances: 1
473 - name: dev
474 role_type: engineer
475 agent: codex
476 instances: 1
477"#,
478 );
479
480 let capability_map = resolve_capability_map(&members);
481
482 assert!(
484 !member_capabilities(&capability_map, "arch").contains(&WorkflowCapability::Dispatcher)
485 );
486 assert!(
488 member_capabilities(&capability_map, "mgr").contains(&WorkflowCapability::Dispatcher)
489 );
490 }
491
492 #[test]
493 fn subordinate_engineer_never_gets_planner_or_dispatcher() {
494 let members = members_from_yaml(
495 r#"
496name: hierarchy
497roles:
498 - name: lead
499 role_type: manager
500 agent: claude
501 instances: 1
502 - name: worker
503 role_type: engineer
504 agent: codex
505 instances: 3
506"#,
507 );
508
509 let capability_map = resolve_capability_map(&members);
510
511 for i in 1..=3 {
512 let name = format!("worker-1-{}", i);
513 let caps = member_capabilities(&capability_map, &name);
514 assert_eq!(caps, capability_set(&[WorkflowCapability::Executor]));
515 assert!(!caps.contains(&WorkflowCapability::Planner));
516 assert!(!caps.contains(&WorkflowCapability::Dispatcher));
517 }
518 }
519
520 #[test]
521 fn manager_without_architect_and_top_level_gets_planner() {
522 let members = members_from_yaml(
523 r#"
524name: manager-only
525roles:
526 - name: mgr
527 role_type: manager
528 agent: claude
529 instances: 1
530"#,
531 );
532
533 let capability_map = resolve_capability_map(&members);
534
535 assert!(member_capabilities(&capability_map, "mgr").contains(&WorkflowCapability::Planner));
536 }
537
538 #[test]
539 fn manager_with_architect_does_not_get_planner() {
540 let members = members_from_yaml(
541 r#"
542name: with-arch
543roles:
544 - name: arch
545 role_type: architect
546 agent: claude
547 instances: 1
548 - name: mgr
549 role_type: manager
550 agent: claude
551 instances: 1
552"#,
553 );
554
555 let capability_map = resolve_capability_map(&members);
556
557 assert!(
558 !member_capabilities(&capability_map, "mgr").contains(&WorkflowCapability::Planner)
559 );
560 }
561
562 #[test]
563 fn operator_gets_reviewer_when_no_architect_or_manager() {
564 let members = members_from_yaml(
565 r#"
566name: engineers-only
567roles:
568 - name: dev
569 role_type: engineer
570 agent: codex
571 instances: 2
572"#,
573 );
574
575 let capability_map = resolve_capability_map(&members);
576
577 let operator_caps = capability_map
579 .get(&CapabilitySubject::Operator)
580 .cloned()
581 .unwrap();
582 assert!(operator_caps.contains(&WorkflowCapability::Reviewer));
583 }
584
585 #[test]
586 fn operator_does_not_get_reviewer_when_architect_exists() {
587 let members = members_from_yaml(
588 r#"
589name: with-arch
590roles:
591 - name: arch
592 role_type: architect
593 agent: claude
594 instances: 1
595 - name: dev
596 role_type: engineer
597 agent: codex
598 instances: 1
599"#,
600 );
601
602 let capability_map = resolve_capability_map(&members);
603
604 let operator_caps = capability_map
605 .get(&CapabilitySubject::Operator)
606 .cloned()
607 .unwrap();
608 assert!(!operator_caps.contains(&WorkflowCapability::Reviewer));
609 }
610
611 #[test]
612 fn operator_does_not_get_reviewer_when_manager_exists() {
613 let members = members_from_yaml(
614 r#"
615name: with-mgr
616roles:
617 - name: mgr
618 role_type: manager
619 agent: claude
620 instances: 1
621 - name: dev
622 role_type: engineer
623 agent: codex
624 instances: 1
625"#,
626 );
627
628 let capability_map = resolve_capability_map(&members);
629
630 let operator_caps = capability_map
631 .get(&CapabilitySubject::Operator)
632 .cloned()
633 .unwrap();
634 assert!(!operator_caps.contains(&WorkflowCapability::Reviewer));
635 }
636
637 #[test]
638 fn orchestrator_always_has_only_orchestrator_capability() {
639 let topologies = vec![
641 r#"
643name: solo
644roles:
645 - name: dev
646 role_type: engineer
647 agent: codex
648 instances: 1
649"#,
650 r#"
652name: full
653roles:
654 - name: arch
655 role_type: architect
656 agent: claude
657 instances: 1
658 - name: mgr
659 role_type: manager
660 agent: claude
661 instances: 1
662 - name: dev
663 role_type: engineer
664 agent: codex
665 instances: 1
666"#,
667 ];
668
669 for yaml in topologies {
670 let members = members_from_yaml(yaml);
671 let capability_map = resolve_capability_map(&members);
672 assert_eq!(
673 capability_map
674 .get(&CapabilitySubject::Orchestrator)
675 .cloned()
676 .unwrap(),
677 capability_set(&[WorkflowCapability::Orchestrator])
678 );
679 }
680 }
681
682 #[test]
683 fn has_agent_reviewer_returns_true_with_architect() {
684 let members = members_from_yaml(
685 r#"
686name: test
687roles:
688 - name: arch
689 role_type: architect
690 agent: claude
691 instances: 1
692 - name: dev
693 role_type: engineer
694 agent: codex
695 instances: 1
696"#,
697 );
698
699 assert!(has_agent_reviewer(&members));
700 }
701
702 #[test]
703 fn has_agent_reviewer_returns_false_with_only_engineers() {
704 let members = members_from_yaml(
705 r#"
706name: test
707roles:
708 - name: dev
709 role_type: engineer
710 agent: codex
711 instances: 2
712"#,
713 );
714
715 assert!(!has_agent_reviewer(&members));
716 }
717
718 #[test]
719 fn has_agent_reviewer_returns_false_with_only_user_roles() {
720 let member = MemberInstance {
721 name: "human".to_string(),
722 role_name: "human".to_string(),
723 role_type: RoleType::User,
724 agent: None,
725 prompt: None,
726 reports_to: None,
727 use_worktrees: false,
728 };
729 assert!(!has_agent_reviewer(&[member]));
730 }
731
732 #[test]
733 fn capability_map_member_count_matches_non_user_members() {
734 let members = members_from_yaml(
735 r#"
736name: mixed
737roles:
738 - name: human
739 role_type: user
740 - name: arch
741 role_type: architect
742 agent: claude
743 instances: 1
744 - name: dev
745 role_type: engineer
746 agent: codex
747 instances: 2
748"#,
749 );
750
751 let capability_map = resolve_capability_map(&members);
752
753 let member_entries = capability_map
754 .keys()
755 .filter(|k| matches!(k, CapabilitySubject::Member(_)))
756 .count();
757 assert_eq!(member_entries, 3);
759 }
760
761 #[test]
762 fn solo_engineer_gets_all_three_agent_capabilities() {
763 let members = members_from_yaml(
764 r#"
765name: solo
766roles:
767 - name: lone-wolf
768 role_type: engineer
769 agent: codex
770 instances: 1
771"#,
772 );
773
774 let capability_map = resolve_capability_map(&members);
775 let caps = member_capabilities(&capability_map, "lone-wolf");
776
777 assert!(caps.contains(&WorkflowCapability::Planner));
778 assert!(caps.contains(&WorkflowCapability::Dispatcher));
779 assert!(caps.contains(&WorkflowCapability::Executor));
780 assert!(!caps.contains(&WorkflowCapability::Reviewer));
781 assert!(!caps.contains(&WorkflowCapability::Orchestrator));
782 assert!(!caps.contains(&WorkflowCapability::Operator));
783 }
784
785 #[test]
786 fn multi_instance_engineers_all_get_same_capabilities() {
787 let members = members_from_yaml(
788 r#"
789name: team
790roles:
791 - name: arch
792 role_type: architect
793 agent: claude
794 instances: 1
795 - name: eng
796 role_type: engineer
797 agent: codex
798 instances: 4
799"#,
800 );
801
802 let capability_map = resolve_capability_map(&members);
803 let expected = capability_set(&[WorkflowCapability::Executor]);
804
805 for i in 1..=4 {
807 let name = format!("eng-{}", i);
808 assert_eq!(member_capabilities(&capability_map, &name), expected);
809 }
810 }
811
812 #[test]
813 fn capability_subject_ordering_is_deterministic() {
814 let a = CapabilitySubject::Member("aaa".to_string());
816 let b = CapabilitySubject::Member("zzz".to_string());
817 let orch = CapabilitySubject::Orchestrator;
818 let op = CapabilitySubject::Operator;
819
820 assert!(a < b);
821 assert!(a < orch);
822 assert!(orch < op);
823 }
824}