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
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
1"""Work verification utilities for little-loops.
3Contains shared functions for verifying that actual implementation work
4was done, used by both issue_manager (ll-auto) and worker_pool (ll-parallel).
5"""
7from __future__ import annotations
9import subprocess
10from typing import TYPE_CHECKING
12if TYPE_CHECKING:
13 from little_loops.logger import Logger
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)
28def filter_excluded_files(files: list[str]) -> list[str]:
29 """Filter out files in excluded directories.
31 Args:
32 files: List of file paths to filter
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 ]
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).
51 Returns True if there's evidence of implementation work - changes to files
52 outside of excluded directories like .issues/, thoughts/, etc.
54 This prevents marking issues as "completed" when no actual fix was implemented.
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).
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
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])
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])
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}")
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
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