1#![cfg_attr(not(test), allow(dead_code))]
2
3use std::collections::{BTreeMap, HashMap};
6use std::path::Path;
7
8use anyhow::Result;
9
10use super::capability::{WorkflowCapability, resolve_member_capabilities};
11use super::hierarchy::MemberInstance;
12use super::resolver::{ResolutionStatus, resolve_board};
13use super::standup::MemberState;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct NudgeTarget {
17 pub member: String,
18 pub reason: String,
19 pub capability: WorkflowCapability,
20}
21
22pub fn compute_nudges(
23 board_dir: &Path,
24 members: &[MemberInstance],
25 states: &HashMap<String, MemberState>,
26 pending_inbox: &HashMap<String, usize>,
27) -> Result<Vec<NudgeTarget>> {
28 let resolutions = resolve_board(board_dir, members)?;
29 let member_capabilities: HashMap<String, _> = members
30 .iter()
31 .map(|member| {
32 (
33 member.name.clone(),
34 resolve_member_capabilities(member, members),
35 )
36 })
37 .collect();
38
39 let mut targets = BTreeMap::new();
40 let mut has_runnable = false;
41 let mut has_blocked = false;
42
43 for resolution in &resolutions {
44 match resolution.status {
45 ResolutionStatus::Runnable => {
46 has_runnable = true;
47 if let Some(owner) = resolution.execution_owner.as_deref() {
48 let owner = resolve_member_reference(owner, members);
49 if member_is_eligible(
50 &owner,
51 WorkflowCapability::Executor,
52 states,
53 pending_inbox,
54 &member_capabilities,
55 ) {
56 record_target(
57 &mut targets,
58 &owner,
59 WorkflowCapability::Executor,
60 format!(
61 "resume runnable owned task #{}: {}",
62 resolution.task_id, resolution.title
63 ),
64 );
65 }
66 } else {
67 for member in members {
68 if member_is_eligible(
69 &member.name,
70 WorkflowCapability::Dispatcher,
71 states,
72 pending_inbox,
73 &member_capabilities,
74 ) {
75 record_target(
76 &mut targets,
77 &member.name,
78 WorkflowCapability::Dispatcher,
79 format!(
80 "dispatch unassigned runnable task #{}: {}",
81 resolution.task_id, resolution.title
82 ),
83 );
84 }
85 }
86 }
87 }
88 ResolutionStatus::NeedsReview => {
89 if let Some(owner) = resolution.review_owner.as_deref() {
90 let owner = resolve_member_reference(owner, members);
91 if member_is_eligible(
92 &owner,
93 WorkflowCapability::Reviewer,
94 states,
95 pending_inbox,
96 &member_capabilities,
97 ) {
98 record_target(
99 &mut targets,
100 &owner,
101 WorkflowCapability::Reviewer,
102 format!(
103 "review backlog for task #{}: {}",
104 resolution.task_id, resolution.title
105 ),
106 );
107 }
108 } else {
109 for member in members {
110 if member_is_eligible(
111 &member.name,
112 WorkflowCapability::Reviewer,
113 states,
114 pending_inbox,
115 &member_capabilities,
116 ) {
117 record_target(
118 &mut targets,
119 &member.name,
120 WorkflowCapability::Reviewer,
121 format!(
122 "review backlog for task #{}: {}",
123 resolution.task_id, resolution.title
124 ),
125 );
126 }
127 }
128 }
129 }
130 ResolutionStatus::Blocked => {
131 has_blocked = true;
132 }
133 ResolutionStatus::NeedsAction => {}
134 }
135 }
136
137 if !has_runnable && has_blocked {
138 for member in members {
139 if member_is_eligible(
140 &member.name,
141 WorkflowCapability::Planner,
142 states,
143 pending_inbox,
144 &member_capabilities,
145 ) {
146 record_target(
147 &mut targets,
148 &member.name,
149 WorkflowCapability::Planner,
150 "blocked frontier needs planning attention".to_string(),
151 );
152 }
153 }
154 }
155
156 Ok(targets
157 .into_iter()
158 .map(|((member, capability), reason)| NudgeTarget {
159 member,
160 reason,
161 capability,
162 })
163 .collect())
164}
165
166fn member_is_eligible(
167 member_name: &str,
168 capability: WorkflowCapability,
169 states: &HashMap<String, MemberState>,
170 pending_inbox: &HashMap<String, usize>,
171 member_capabilities: &HashMap<String, super::capability::CapabilitySet>,
172) -> bool {
173 matches!(states.get(member_name), Some(MemberState::Idle))
174 && pending_inbox.get(member_name).copied().unwrap_or(0) == 0
175 && member_capabilities
176 .get(member_name)
177 .is_some_and(|capabilities| capabilities.contains(&capability))
178}
179
180fn record_target(
181 targets: &mut BTreeMap<(String, WorkflowCapability), String>,
182 member_name: &str,
183 capability: WorkflowCapability,
184 reason: String,
185) {
186 targets
187 .entry((member_name.to_string(), capability))
188 .or_insert(reason);
189}
190
191fn resolve_member_reference(member_name: &str, members: &[MemberInstance]) -> String {
192 if members.iter().any(|member| member.name == member_name) {
193 return member_name.to_string();
194 }
195
196 let mut matches = members
197 .iter()
198 .filter(|member| member.role_name == member_name)
199 .map(|member| member.name.as_str());
200 let Some(first) = matches.next() else {
201 return member_name.to_string();
202 };
203
204 if matches.next().is_none() {
205 first.to_string()
206 } else {
207 member_name.to_string()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::team::config::TeamConfig;
215 use crate::team::hierarchy::resolve_hierarchy;
216
217 fn members(yaml: &str) -> Vec<MemberInstance> {
218 let config: TeamConfig = serde_yaml::from_str(yaml).unwrap();
219 resolve_hierarchy(&config).unwrap()
220 }
221
222 fn write_task(tasks_dir: &Path, id: u32, extra_frontmatter: &str) {
223 let path = tasks_dir.join(format!("{id:03}-task-{id}.md"));
224 std::fs::write(
225 path,
226 format!(
227 "---\nid: {id}\ntitle: Task {id}\npriority: high\n{extra_frontmatter}class: standard\n---\n\nBody.\n"
228 ),
229 )
230 .unwrap();
231 }
232
233 #[test]
234 fn zero_members_produces_no_nudges() {
235 let tmp = tempfile::tempdir().unwrap();
236 std::fs::create_dir_all(tmp.path().join("tasks")).unwrap();
237
238 let nudges = compute_nudges(tmp.path(), &[], &HashMap::new(), &HashMap::new()).unwrap();
239
240 assert!(nudges.is_empty());
241 }
242
243 #[test]
244 fn idle_executor_with_runnable_owned_task_gets_nudged() {
245 let tmp = tempfile::tempdir().unwrap();
246 let tasks_dir = tmp.path().join("tasks");
247 std::fs::create_dir_all(&tasks_dir).unwrap();
248 write_task(
249 &tasks_dir,
250 1,
251 "status: todo\nexecution_owner: builder-1-1\nclaimed_by: builder-1-1\n",
252 );
253 let members = members(
254 r#"
255name: team
256roles:
257 - name: lead
258 role_type: manager
259 agent: claude
260 - name: builder
261 role_type: engineer
262 agent: codex
263 instances: 1
264"#,
265 );
266 let states = HashMap::from([
267 ("lead".to_string(), MemberState::Working),
268 ("builder-1-1".to_string(), MemberState::Idle),
269 ]);
270
271 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
272
273 assert_eq!(
274 nudges,
275 vec![NudgeTarget {
276 member: "builder-1-1".to_string(),
277 reason: "resume runnable owned task #1: Task 1".to_string(),
278 capability: WorkflowCapability::Executor,
279 }]
280 );
281 }
282
283 #[test]
284 fn all_members_busy_produces_no_nudges() {
285 let tmp = tempfile::tempdir().unwrap();
286 let tasks_dir = tmp.path().join("tasks");
287 std::fs::create_dir_all(&tasks_dir).unwrap();
288 write_task(
289 &tasks_dir,
290 2,
291 "status: todo\nexecution_owner: builder-1-1\nclaimed_by: builder-1-1\n",
292 );
293 let members = members(
294 r#"
295name: team
296roles:
297 - name: lead
298 role_type: manager
299 agent: claude
300 - name: builder
301 role_type: engineer
302 agent: codex
303"#,
304 );
305 let states = HashMap::from([
306 ("lead".to_string(), MemberState::Working),
307 ("builder-1-1".to_string(), MemberState::Working),
308 ]);
309
310 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
311
312 assert!(nudges.is_empty());
313 }
314
315 #[test]
316 fn idle_reviewer_with_review_backlog_gets_nudged() {
317 let tmp = tempfile::tempdir().unwrap();
318 let tasks_dir = tmp.path().join("tasks");
319 std::fs::create_dir_all(&tasks_dir).unwrap();
320 write_task(&tasks_dir, 2, "status: review\nreview_owner: lead\n");
321 let members = members(
322 r#"
323name: pair
324roles:
325 - name: lead
326 role_type: manager
327 agent: claude
328 - name: builder
329 role_type: engineer
330 agent: codex
331"#,
332 );
333 let states = HashMap::from([
334 ("lead".to_string(), MemberState::Idle),
335 ("builder-1-1".to_string(), MemberState::Working),
336 ]);
337
338 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
339
340 assert_eq!(
341 nudges,
342 vec![NudgeTarget {
343 member: "lead".to_string(),
344 reason: "review backlog for task #2: Task 2".to_string(),
345 capability: WorkflowCapability::Reviewer,
346 }]
347 );
348 }
349
350 #[test]
351 fn pending_inbox_suppresses_nudge() {
352 let tmp = tempfile::tempdir().unwrap();
353 let tasks_dir = tmp.path().join("tasks");
354 std::fs::create_dir_all(&tasks_dir).unwrap();
355 write_task(
356 &tasks_dir,
357 3,
358 "status: todo\nexecution_owner: builder-1-1\nclaimed_by: builder-1-1\n",
359 );
360 let members = members(
361 r#"
362name: team
363roles:
364 - name: builder
365 role_type: engineer
366 agent: codex
367"#,
368 );
369 let states = HashMap::from([("builder-1-1".to_string(), MemberState::Idle)]);
370 let pending = HashMap::from([("builder-1-1".to_string(), 1usize)]);
371
372 let nudges = compute_nudges(tmp.path(), &members, &states, &pending).unwrap();
373
374 assert!(nudges.is_empty());
375 }
376
377 #[test]
378 fn pending_inbox_suppresses_planner_nudge() {
379 let tmp = tempfile::tempdir().unwrap();
380 let tasks_dir = tmp.path().join("tasks");
381 std::fs::create_dir_all(&tasks_dir).unwrap();
382 write_task(
383 &tasks_dir,
384 4,
385 "status: todo\nblocked_on: waiting-on-decision\n",
386 );
387 let members = members(
388 r#"
389name: team
390roles:
391 - name: lead
392 role_type: manager
393 agent: claude
394 - name: builder
395 role_type: engineer
396 agent: codex
397"#,
398 );
399 let states = HashMap::from([
400 ("lead".to_string(), MemberState::Idle),
401 ("builder-1-1".to_string(), MemberState::Working),
402 ]);
403 let pending = HashMap::from([("lead".to_string(), 1usize)]);
404
405 let nudges = compute_nudges(tmp.path(), &members, &states, &pending).unwrap();
406
407 assert!(nudges.is_empty());
408 }
409
410 #[test]
411 fn blocked_frontier_without_runnable_work_nudges_planner() {
412 let tmp = tempfile::tempdir().unwrap();
413 let tasks_dir = tmp.path().join("tasks");
414 std::fs::create_dir_all(&tasks_dir).unwrap();
415 write_task(
416 &tasks_dir,
417 4,
418 "status: todo\nblocked_on: waiting-on-decision\n",
419 );
420 let members = members(
421 r#"
422name: team
423roles:
424 - name: lead
425 role_type: manager
426 agent: claude
427 - name: builder
428 role_type: engineer
429 agent: codex
430"#,
431 );
432 let states = HashMap::from([
433 ("lead".to_string(), MemberState::Idle),
434 ("builder".to_string(), MemberState::Working),
435 ]);
436
437 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
438
439 assert_eq!(
440 nudges,
441 vec![NudgeTarget {
442 member: "lead".to_string(),
443 reason: "blocked frontier needs planning attention".to_string(),
444 capability: WorkflowCapability::Planner,
445 }]
446 );
447 }
448
449 #[test]
450 fn multi_hop_blocked_dependency_chain_nudges_planner() {
451 let tmp = tempfile::tempdir().unwrap();
452 let tasks_dir = tmp.path().join("tasks");
453 std::fs::create_dir_all(&tasks_dir).unwrap();
454 write_task(&tasks_dir, 1, "status: todo\ndepends_on:\n - 2\n");
455 write_task(&tasks_dir, 2, "status: todo\ndepends_on:\n - 3\n");
456 write_task(
457 &tasks_dir,
458 3,
459 "status: todo\nblocked_on: waiting-on-decision\n",
460 );
461 let members = members(
462 r#"
463name: team
464roles:
465 - name: architect
466 role_type: architect
467 agent: claude
468 - name: builder
469 role_type: engineer
470 agent: codex
471"#,
472 );
473 let states = HashMap::from([
474 ("architect".to_string(), MemberState::Idle),
475 ("builder-1-1".to_string(), MemberState::Working),
476 ]);
477
478 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
479
480 assert_eq!(
481 nudges,
482 vec![NudgeTarget {
483 member: "architect".to_string(),
484 reason: "blocked frontier needs planning attention".to_string(),
485 capability: WorkflowCapability::Planner,
486 }]
487 );
488 }
489
490 #[test]
491 fn single_architect_can_receive_dispatcher_and_reviewer_nudges() {
492 let tmp = tempfile::tempdir().unwrap();
493 let tasks_dir = tmp.path().join("tasks");
494 std::fs::create_dir_all(&tasks_dir).unwrap();
495 write_task(&tasks_dir, 1, "status: review\nreview_owner: architect\n");
496 write_task(&tasks_dir, 2, "status: todo\n");
497 let members = members(
498 r#"
499name: solo-architect
500roles:
501 - name: architect
502 role_type: architect
503 agent: claude
504"#,
505 );
506 let states = HashMap::from([("architect".to_string(), MemberState::Idle)]);
507
508 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
509
510 assert_eq!(
511 nudges,
512 vec![
513 NudgeTarget {
514 member: "architect".to_string(),
515 reason: "dispatch unassigned runnable task #2: Task 2".to_string(),
516 capability: WorkflowCapability::Dispatcher,
517 },
518 NudgeTarget {
519 member: "architect".to_string(),
520 reason: "review backlog for task #1: Task 1".to_string(),
521 capability: WorkflowCapability::Reviewer,
522 },
523 ]
524 );
525 }
526
527 #[test]
528 fn renamed_roles_from_config_still_receive_role_type_capabilities() {
529 let tmp = tempfile::tempdir().unwrap();
530 let tasks_dir = tmp.path().join("tasks");
531 std::fs::create_dir_all(&tasks_dir).unwrap();
532 write_task(&tasks_dir, 1, "status: review\nreview_owner: triage-lead\n");
533 write_task(&tasks_dir, 2, "status: todo\n");
534 let members = members(
535 r#"
536name: renamed
537roles:
538 - name: planner
539 role_type: architect
540 agent: claude
541 - name: triage-lead
542 role_type: manager
543 agent: claude
544 - name: implementer
545 role_type: engineer
546 agent: codex
547"#,
548 );
549 let states = HashMap::from([
550 ("planner".to_string(), MemberState::Working),
551 ("triage-lead".to_string(), MemberState::Idle),
552 ("implementer-1-1".to_string(), MemberState::Working),
553 ]);
554
555 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
556
557 assert_eq!(
558 nudges,
559 vec![
560 NudgeTarget {
561 member: "triage-lead".to_string(),
562 reason: "dispatch unassigned runnable task #2: Task 2".to_string(),
563 capability: WorkflowCapability::Dispatcher,
564 },
565 NudgeTarget {
566 member: "triage-lead".to_string(),
567 reason: "review backlog for task #1: Task 1".to_string(),
568 capability: WorkflowCapability::Reviewer,
569 },
570 ]
571 );
572 }
573
574 #[test]
575 fn deterministic_ordering_with_member_name_ties_is_stable() {
576 let tmp = tempfile::tempdir().unwrap();
577 let tasks_dir = tmp.path().join("tasks");
578 std::fs::create_dir_all(&tasks_dir).unwrap();
579 write_task(
580 &tasks_dir,
581 1,
582 "status: todo\nexecution_owner: builder-1-1\nclaimed_by: builder-1-1\n",
583 );
584 write_task(
585 &tasks_dir,
586 2,
587 "status: todo\nexecution_owner: builder-2-1\nclaimed_by: builder-2-1\n",
588 );
589 let members = members(
590 r#"
591name: team
592roles:
593 - name: lead
594 role_type: manager
595 agent: claude
596 instances: 2
597 - name: builder
598 role_type: engineer
599 agent: codex
600 instances: 1
601"#,
602 );
603 let states = HashMap::from([
604 ("lead-1".to_string(), MemberState::Working),
605 ("lead-2".to_string(), MemberState::Working),
606 ("builder-1-1".to_string(), MemberState::Idle),
607 ("builder-2-1".to_string(), MemberState::Idle),
608 ]);
609
610 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
611
612 assert_eq!(
613 nudges,
614 vec![
615 NudgeTarget {
616 member: "builder-1-1".to_string(),
617 reason: "resume runnable owned task #1: Task 1".to_string(),
618 capability: WorkflowCapability::Executor,
619 },
620 NudgeTarget {
621 member: "builder-2-1".to_string(),
622 reason: "resume runnable owned task #2: Task 2".to_string(),
623 capability: WorkflowCapability::Executor,
624 },
625 ]
626 );
627 }
628
629 #[test]
630 fn multiple_targets_are_computed_deterministically() {
631 let tmp = tempfile::tempdir().unwrap();
632 let tasks_dir = tmp.path().join("tasks");
633 std::fs::create_dir_all(&tasks_dir).unwrap();
634 write_task(
635 &tasks_dir,
636 1,
637 "status: todo\nexecution_owner: builder-1-1\nclaimed_by: builder-1-1\n",
638 );
639 write_task(&tasks_dir, 2, "status: review\nreview_owner: lead\n");
640 write_task(&tasks_dir, 3, "status: todo\n");
641 let members = members(
642 r#"
643name: team
644roles:
645 - name: architect
646 role_type: architect
647 agent: claude
648 - name: lead
649 role_type: manager
650 agent: claude
651 - name: builder
652 role_type: engineer
653 agent: codex
654"#,
655 );
656 let states = HashMap::from([
657 ("architect".to_string(), MemberState::Idle),
658 ("lead".to_string(), MemberState::Idle),
659 ("builder-1-1".to_string(), MemberState::Idle),
660 ]);
661
662 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
663
664 assert_eq!(
665 nudges,
666 vec![
667 NudgeTarget {
668 member: "builder-1-1".to_string(),
669 reason: "resume runnable owned task #1: Task 1".to_string(),
670 capability: WorkflowCapability::Executor,
671 },
672 NudgeTarget {
673 member: "lead".to_string(),
674 reason: "dispatch unassigned runnable task #3: Task 3".to_string(),
675 capability: WorkflowCapability::Dispatcher,
676 },
677 NudgeTarget {
678 member: "lead".to_string(),
679 reason: "review backlog for task #2: Task 2".to_string(),
680 capability: WorkflowCapability::Reviewer,
681 },
682 ]
683 );
684 }
685
686 #[test]
689 fn resolve_member_reference_exact_name_match() {
690 let members = members(
691 "name: team\nroles:\n - name: lead\n role_type: manager\n agent: claude\n - name: builder\n role_type: engineer\n agent: codex\n",
692 );
693 assert_eq!(resolve_member_reference("lead", &members), "lead");
694 assert_eq!(
695 resolve_member_reference("builder-1-1", &members),
696 "builder-1-1"
697 );
698 }
699
700 #[test]
701 fn resolve_member_reference_role_name_fallback() {
702 let members = members(
706 "name: team\nroles:\n - name: lead\n role_type: manager\n agent: claude\n - name: builder\n role_type: engineer\n agent: codex\n",
707 );
708 assert_eq!(resolve_member_reference("builder", &members), "builder-1-1");
711 }
712
713 #[test]
714 fn resolve_member_reference_unknown_returns_as_is() {
715 let members = members(
716 "name: team\nroles:\n - name: lead\n role_type: manager\n agent: claude\n",
717 );
718 assert_eq!(
719 resolve_member_reference("unknown-member", &members),
720 "unknown-member"
721 );
722 }
723
724 #[test]
727 fn empty_board_produces_no_nudges() {
728 let tmp = tempfile::tempdir().unwrap();
729 std::fs::create_dir_all(tmp.path().join("tasks")).unwrap();
730 let members = members(
731 "name: team\nroles:\n - name: lead\n role_type: manager\n agent: claude\n",
732 );
733 let states = HashMap::from([("lead".to_string(), MemberState::Idle)]);
734
735 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
736 assert!(nudges.is_empty());
737 }
738
739 #[test]
742 fn review_without_owner_nudges_all_eligible_reviewers() {
743 let tmp = tempfile::tempdir().unwrap();
744 let tasks_dir = tmp.path().join("tasks");
745 std::fs::create_dir_all(&tasks_dir).unwrap();
746 write_task(&tasks_dir, 1, "status: review\n"); let members = members(
749 r#"
750name: team
751roles:
752 - name: lead
753 role_type: manager
754 agent: claude
755 instances: 2
756"#,
757 );
758 let states = HashMap::from([
759 ("lead-1".to_string(), MemberState::Idle),
760 ("lead-2".to_string(), MemberState::Idle),
761 ]);
762
763 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
764 let reviewer_names: Vec<&str> = nudges.iter().map(|n| n.member.as_str()).collect();
765 assert!(reviewer_names.contains(&"lead-1"));
766 assert!(reviewer_names.contains(&"lead-2"));
767 assert!(
768 nudges
769 .iter()
770 .all(|n| n.capability == WorkflowCapability::Reviewer)
771 );
772 }
773
774 #[test]
777 fn mixed_blocked_and_runnable_no_planner_nudge() {
778 let tmp = tempfile::tempdir().unwrap();
779 let tasks_dir = tmp.path().join("tasks");
780 std::fs::create_dir_all(&tasks_dir).unwrap();
781 write_task(&tasks_dir, 1, "status: todo\nblocked_on: waiting\n");
782 write_task(&tasks_dir, 2, "status: todo\n"); let members = members(
785 r#"
786name: team
787roles:
788 - name: lead
789 role_type: manager
790 agent: claude
791 - name: builder
792 role_type: engineer
793 agent: codex
794"#,
795 );
796 let states = HashMap::from([
797 ("lead".to_string(), MemberState::Idle),
798 ("builder-1-1".to_string(), MemberState::Idle),
799 ]);
800
801 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
802 assert!(
804 !nudges
805 .iter()
806 .any(|n| n.capability == WorkflowCapability::Planner)
807 );
808 }
809
810 #[test]
813 fn unknown_member_state_not_nudged() {
814 let tmp = tempfile::tempdir().unwrap();
815 let tasks_dir = tmp.path().join("tasks");
816 std::fs::create_dir_all(&tasks_dir).unwrap();
817 write_task(
818 &tasks_dir,
819 1,
820 "status: todo\nexecution_owner: builder-1-1\nclaimed_by: builder-1-1\n",
821 );
822 let members = members(
823 "name: team\nroles:\n - name: builder\n role_type: engineer\n agent: codex\n",
824 );
825 let states = HashMap::new();
827
828 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
829 assert!(nudges.is_empty());
830 }
831
832 #[test]
835 fn working_member_not_nudged_for_dispatch() {
836 let tmp = tempfile::tempdir().unwrap();
837 let tasks_dir = tmp.path().join("tasks");
838 std::fs::create_dir_all(&tasks_dir).unwrap();
839 write_task(&tasks_dir, 1, "status: todo\n"); let members = members(
841 "name: team\nroles:\n - name: lead\n role_type: manager\n agent: claude\n",
842 );
843 let states = HashMap::from([("lead".to_string(), MemberState::Working)]);
844
845 let nudges = compute_nudges(tmp.path(), &members, &states, &HashMap::new()).unwrap();
846 assert!(nudges.is_empty());
847 }
848}