Coverage for little_loops / config / automation.py: 100%

71 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-04-11 23:20 -0500

1"""Automation and execution configuration dataclasses. 

2 

3Covers automation scripts, parallel execution, confidence gates, 

4command behavior, and dependency analysis configuration. 

5""" 

6 

7from __future__ import annotations 

8 

9from dataclasses import dataclass, field 

10from typing import Any 

11 

12 

13@dataclass 

14class AutomationConfig: 

15 """Automation script configuration.""" 

16 

17 timeout_seconds: int = 3600 

18 idle_timeout_seconds: int = 0 # Kill if no output for N seconds (0 to disable) 

19 state_file: str = ".auto-manage-state.json" 

20 worktree_base: str = ".worktrees" 

21 max_workers: int = 2 

22 stream_output: bool = True 

23 max_continuations: int = 3 # Max session restarts on context handoff 

24 

25 @classmethod 

26 def from_dict(cls, data: dict[str, Any]) -> AutomationConfig: 

27 """Create AutomationConfig from dictionary.""" 

28 return cls( 

29 timeout_seconds=data.get("timeout_seconds", 3600), 

30 idle_timeout_seconds=data.get("idle_timeout_seconds", 0), 

31 state_file=data.get("state_file", ".auto-manage-state.json"), 

32 worktree_base=data.get("worktree_base", ".worktrees"), 

33 max_workers=data.get("max_workers", 2), 

34 stream_output=data.get("stream_output", True), 

35 max_continuations=data.get("max_continuations", 3), 

36 ) 

37 

38 

39@dataclass 

40class ParallelAutomationConfig: 

41 """Parallel automation configuration using composition. 

42 

43 Uses AutomationConfig for shared settings (max_workers, worktree_base, 

44 state_file, timeout_seconds, stream_output) plus parallel-specific fields. 

45 """ 

46 

47 base: AutomationConfig 

48 p0_sequential: bool = True 

49 max_merge_retries: int = 2 

50 command_prefix: str = "/ll:" 

51 ready_command: str = "ready-issue {{issue_id}}" 

52 manage_command: str = "manage-issue {{issue_type}} {{action}} {{issue_id}}" 

53 worktree_copy_files: list[str] = field( 

54 default_factory=lambda: [".claude/settings.local.json", ".env"] 

55 ) 

56 require_code_changes: bool = True 

57 use_feature_branches: bool = False 

58 remote_name: str = "origin" 

59 

60 @classmethod 

61 def from_dict(cls, data: dict[str, Any]) -> ParallelAutomationConfig: 

62 """Create ParallelAutomationConfig from dictionary. 

63 

64 Shared fields use parallel-specific defaults: 

65 - state_file: ".parallel-manage-state.json" 

66 - stream_output: False 

67 """ 

68 base = AutomationConfig( 

69 timeout_seconds=data.get("timeout_per_issue", data.get("timeout_seconds", 3600)), 

70 state_file=data.get("state_file", ".parallel-manage-state.json"), 

71 worktree_base=data.get("worktree_base", ".worktrees"), 

72 max_workers=data.get("max_workers", 2), 

73 stream_output=data.get("stream_subprocess_output", data.get("stream_output", False)), 

74 ) 

75 return cls( 

76 base=base, 

77 p0_sequential=data.get("p0_sequential", True), 

78 max_merge_retries=data.get("max_merge_retries", 2), 

79 command_prefix=data.get("command_prefix", "/ll:"), 

80 ready_command=data.get("ready_command", "ready-issue {{issue_id}}"), 

81 manage_command=data.get( 

82 "manage_command", "manage-issue {{issue_type}} {{action}} {{issue_id}}" 

83 ), 

84 worktree_copy_files=data.get( 

85 "worktree_copy_files", [".claude/settings.local.json", ".env"] 

86 ), 

87 require_code_changes=data.get("require_code_changes", True), 

88 use_feature_branches=data.get("use_feature_branches", False), 

89 remote_name=data.get("remote_name", "origin"), 

90 ) 

91 

92 

93@dataclass 

94class ConfidenceGateConfig: 

95 """Confidence score gate configuration for manage-issue.""" 

96 

97 enabled: bool = False 

98 readiness_threshold: int = 85 

99 outcome_threshold: int = 70 

100 

101 @classmethod 

102 def from_dict(cls, data: dict[str, Any]) -> ConfidenceGateConfig: 

103 """Create ConfidenceGateConfig from dictionary.""" 

104 legacy = data.get("threshold", 85) 

105 return cls( 

106 enabled=data.get("enabled", False), 

107 readiness_threshold=data.get("readiness_threshold", legacy), 

108 outcome_threshold=data.get("outcome_threshold", 70), 

109 ) 

110 

111 

112@dataclass 

113class CommandsConfig: 

114 """Command customization configuration.""" 

115 

116 pre_implement: str | None = None 

117 post_implement: str | None = None 

118 custom_verification: list[str] = field(default_factory=list) 

119 confidence_gate: ConfidenceGateConfig = field(default_factory=ConfidenceGateConfig) 

120 tdd_mode: bool = False 

121 

122 @classmethod 

123 def from_dict(cls, data: dict[str, Any]) -> CommandsConfig: 

124 """Create CommandsConfig from dictionary.""" 

125 return cls( 

126 pre_implement=data.get("pre_implement"), 

127 post_implement=data.get("post_implement"), 

128 custom_verification=data.get("custom_verification", []), 

129 confidence_gate=ConfidenceGateConfig.from_dict(data.get("confidence_gate", {})), 

130 tdd_mode=data.get("tdd_mode", False), 

131 ) 

132 

133 

134@dataclass 

135class ScoringWeightsConfig: 

136 """Scoring weights for semantic conflict analysis. 

137 

138 Weights for the three signals used in compute_conflict_score(). 

139 Should sum to 1.0 for normalized scoring. 

140 

141 Attributes: 

142 semantic: Weight for semantic target overlap (component/function names) 

143 section: Weight for section mention overlap (UI regions) 

144 type: Weight for modification type match 

145 """ 

146 

147 semantic: float = 0.5 

148 section: float = 0.3 

149 type: float = 0.2 

150 

151 @classmethod 

152 def from_dict(cls, data: dict[str, Any]) -> ScoringWeightsConfig: 

153 """Create ScoringWeightsConfig from dictionary.""" 

154 return cls( 

155 semantic=data.get("semantic", 0.5), 

156 section=data.get("section", 0.3), 

157 type=data.get("type", 0.2), 

158 ) 

159 

160 

161@dataclass 

162class DependencyMappingConfig: 

163 """Dependency mapping threshold configuration. 

164 

165 Controls overlap detection sensitivity and conflict scoring thresholds. 

166 Default values match the previously hardcoded constants for backwards 

167 compatibility. 

168 

169 Attributes: 

170 overlap_min_files: Minimum overlapping files to trigger overlap 

171 overlap_min_ratio: Minimum ratio of overlapping files to smaller set 

172 min_directory_depth: Minimum path segments for directory overlap 

173 conflict_threshold: Below = parallel-safe, above = dependency proposed 

174 high_conflict_threshold: Above = HIGH conflict label 

175 confidence_modifier: Applied when dependency direction is ambiguous 

176 scoring_weights: Weights for semantic/section/type signals 

177 exclude_common_files: Infrastructure files excluded from overlap detection 

178 """ 

179 

180 overlap_min_files: int = 2 

181 overlap_min_ratio: float = 0.25 

182 min_directory_depth: int = 2 

183 conflict_threshold: float = 0.4 

184 high_conflict_threshold: float = 0.7 

185 confidence_modifier: float = 0.5 

186 scoring_weights: ScoringWeightsConfig = field(default_factory=ScoringWeightsConfig) 

187 exclude_common_files: list[str] = field( 

188 default_factory=lambda: [ 

189 "__init__.py", 

190 "pyproject.toml", 

191 "setup.py", 

192 "setup.cfg", 

193 "CHANGELOG.md", 

194 "README.md", 

195 "conftest.py", 

196 ] 

197 ) 

198 

199 @classmethod 

200 def from_dict(cls, data: dict[str, Any]) -> DependencyMappingConfig: 

201 """Create DependencyMappingConfig from dictionary.""" 

202 return cls( 

203 overlap_min_files=data.get("overlap_min_files", 2), 

204 overlap_min_ratio=data.get("overlap_min_ratio", 0.25), 

205 min_directory_depth=data.get("min_directory_depth", 2), 

206 conflict_threshold=data.get("conflict_threshold", 0.4), 

207 high_conflict_threshold=data.get("high_conflict_threshold", 0.7), 

208 confidence_modifier=data.get("confidence_modifier", 0.5), 

209 scoring_weights=ScoringWeightsConfig.from_dict(data.get("scoring_weights", {})), 

210 exclude_common_files=data.get( 

211 "exclude_common_files", 

212 [ 

213 "__init__.py", 

214 "pyproject.toml", 

215 "setup.py", 

216 "setup.cfg", 

217 "CHANGELOG.md", 

218 "README.md", 

219 "conftest.py", 

220 ], 

221 ), 

222 )