Coverage for little_loops / work_verification.py: 96%

54 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-05-22 16:19 -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( 

45 logger: Logger, 

46 changed_files: list[str] | None = None, 

47 baseline_sha: str | None = None, 

48) -> bool: 

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

50 

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

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

53 

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

55 

56 Args: 

57 logger: Logger for output 

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

59 will detect via git diff commands. 

60 baseline_sha: Optional git SHA captured before Phase 2 began. When provided 

61 and the working tree is clean, checks for commits made since this SHA 

62 (covers the case where the agent commits mid-phase and exits cleanly). 

63 

64 Returns: 

65 True if meaningful file changes were detected 

66 """ 

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

68 if changed_files is not None: 

69 meaningful_changes = filter_excluded_files(changed_files) 

70 if meaningful_changes: 

71 logger.info( 

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

73 ) 

74 return True 

75 # Log which excluded files were modified for diagnostic purposes 

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

77 logger.warning( 

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

79 ) 

80 return False 

81 

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

83 all_excluded_files: list[str] = [] 

84 try: 

85 # Check for uncommitted changes 

86 result = subprocess.run( 

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

88 capture_output=True, 

89 text=True, 

90 ) 

91 if result.returncode == 0: 

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

93 meaningful_changes = filter_excluded_files(files) 

94 if meaningful_changes: 

95 logger.info( 

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

97 ) 

98 return True 

99 # Collect excluded files for diagnostic logging 

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

101 

102 # Also check staged changes 

103 result = subprocess.run( 

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

105 capture_output=True, 

106 text=True, 

107 ) 

108 if result.returncode == 0: 

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

110 meaningful_staged = filter_excluded_files(staged) 

111 if meaningful_staged: 

112 logger.info( 

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

114 ) 

115 return True 

116 # Collect excluded files for diagnostic logging 

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

118 

119 # Check commits made since baseline (covers mid-phase commits in ll-auto) 

120 if baseline_sha: 

121 try: 

122 current_head = subprocess.run( 

123 ["git", "rev-parse", "HEAD"], 

124 capture_output=True, 

125 text=True, 

126 ) 

127 if current_head.returncode == 0 and current_head.stdout.strip() != baseline_sha: 

128 result = subprocess.run( 

129 ["git", "diff", "--name-only", f"{baseline_sha}..HEAD"], 

130 capture_output=True, 

131 text=True, 

132 ) 

133 if result.returncode == 0: 

134 committed = result.stdout.strip().split("\n") 

135 meaningful_committed = filter_excluded_files(committed) 

136 if meaningful_committed: 

137 logger.info( 

138 f"Found {len(meaningful_committed)} file(s) committed since " 

139 f"baseline: {meaningful_committed[:5]}" 

140 ) 

141 return True 

142 all_excluded_files.extend( 

143 [f for f in committed if f and f not in all_excluded_files] 

144 ) 

145 except Exception as e: 

146 logger.error(f"Could not check committed changes: {e}") 

147 

148 # Log which excluded files were modified for diagnostic purposes 

149 if all_excluded_files: 

150 logger.warning( 

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

152 f"{all_excluded_files[:10]}" 

153 ) 

154 else: 

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

156 return False 

157 

158 except Exception as e: 

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

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

161 return False