Coverage for little_loops / work_verification.py: 15%

40 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:18 -0500

1"""Work verification utilities for little-loops. 

2 

3Contains shared functions for verifying that actual implementation work 

4was done, used by both issue_manager (ll-auto) and worker_pool (ll-parallel). 

5""" 

6 

7from __future__ import annotations 

8 

9import subprocess 

10from typing import TYPE_CHECKING 

11 

12if TYPE_CHECKING: 

13 from little_loops.logger import Logger 

14 

15 

16# Directories that are excluded when verifying work was done. 

17# Changes to files in these directories don't count as "real work". 

18EXCLUDED_DIRECTORIES = ( 

19 ".issues/", 

20 "issues/", # Support non-dotted variant (issues.base_dir = "issues") 

21 ".speckit/", 

22 "thoughts/", 

23 ".worktrees/", 

24 ".auto-manage", 

25) 

26 

27 

28def filter_excluded_files(files: list[str]) -> list[str]: 

29 """Filter out files in excluded directories. 

30 

31 Args: 

32 files: List of file paths to filter 

33 

34 Returns: 

35 List of files not in excluded directories 

36 """ 

37 return [ 

38 f 

39 for f in files 

40 if f and not any(f.startswith(excluded) for excluded in EXCLUDED_DIRECTORIES) 

41 ] 

42 

43 

44def verify_work_was_done(logger: Logger, changed_files: list[str] | None = None) -> bool: 

45 """Verify that actual work was done (not just issue file moves). 

46 

47 Returns True if there's evidence of implementation work - changes to files 

48 outside of excluded directories like .issues/, thoughts/, etc. 

49 

50 This prevents marking issues as "completed" when no actual fix was implemented. 

51 

52 Args: 

53 logger: Logger for output 

54 changed_files: Optional list of changed files. If not provided, 

55 will detect via git diff commands. 

56 

57 Returns: 

58 True if meaningful file changes were detected 

59 """ 

60 # If changed_files provided, use them directly (ll-parallel case) 

61 if changed_files is not None: 

62 meaningful_changes = filter_excluded_files(changed_files) 

63 if meaningful_changes: 

64 logger.info( 

65 f"Found {len(meaningful_changes)} file(s) changed: {meaningful_changes[:5]}" 

66 ) 

67 return True 

68 # Log which excluded files were modified for diagnostic purposes 

69 excluded_files = [f for f in changed_files if f] 

70 logger.warning( 

71 f"No meaningful changes detected - only excluded files modified: {excluded_files[:10]}" 

72 ) 

73 return False 

74 

75 # Otherwise detect via git (ll-auto case) 

76 all_excluded_files: list[str] = [] 

77 try: 

78 # Check for uncommitted changes 

79 result = subprocess.run( 

80 ["git", "diff", "--name-only"], 

81 capture_output=True, 

82 text=True, 

83 ) 

84 if result.returncode == 0: 

85 files = result.stdout.strip().split("\n") 

86 meaningful_changes = filter_excluded_files(files) 

87 if meaningful_changes: 

88 logger.info( 

89 f"Found {len(meaningful_changes)} file(s) changed: {meaningful_changes[:5]}" 

90 ) 

91 return True 

92 # Collect excluded files for diagnostic logging 

93 all_excluded_files.extend([f for f in files if f]) 

94 

95 # Also check staged changes 

96 result = subprocess.run( 

97 ["git", "diff", "--cached", "--name-only"], 

98 capture_output=True, 

99 text=True, 

100 ) 

101 if result.returncode == 0: 

102 staged = result.stdout.strip().split("\n") 

103 meaningful_staged = filter_excluded_files(staged) 

104 if meaningful_staged: 

105 logger.info( 

106 f"Found {len(meaningful_staged)} staged file(s): {meaningful_staged[:5]}" 

107 ) 

108 return True 

109 # Collect excluded files for diagnostic logging 

110 all_excluded_files.extend([f for f in staged if f and f not in all_excluded_files]) 

111 

112 # Log which excluded files were modified for diagnostic purposes 

113 if all_excluded_files: 

114 logger.warning( 

115 f"No meaningful changes detected - only excluded files modified: " 

116 f"{all_excluded_files[:10]}" 

117 ) 

118 else: 

119 logger.warning("No meaningful changes detected - no files modified") 

120 return False 

121 

122 except Exception as e: 

123 logger.error(f"Could not verify work: {e}") 

124 # Be conservative - don't assume work was done if we can't verify 

125 return False