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

1"""Automation and execution configuration dataclasses.

2

3 Covers automation scripts, parallel execution, confidence gates,

4 command behavior, and dependency analysis configuration.

5"""

6

7 from __future__ import annotations

8

9 from dataclasses import dataclass, field

10 from typing import Any

11

12

13@dataclass

14 class 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

40 class 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

58 @classmethod

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

60 """Create ParallelAutomationConfig from dictionary.

61

62 Shared fields use parallel-specific defaults:

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

64 - stream_output: False

65 """

66 base = AutomationConfig(

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

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

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

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

71 stream_output=data.get("stream_output", False),

72 )

73 return cls(

74 base=base,

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

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

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

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

79 manage_command=data.get(

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

81 ),

82 worktree_copy_files=data.get(

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

84 ),

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

86 )

87

88

89@dataclass

90 class ConfidenceGateConfig:

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

92

93 enabled: bool = False

94 threshold: int = 85

95

96 @classmethod

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

98 """Create ConfidenceGateConfig from dictionary."""

99 return cls(

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

101 threshold=data.get("threshold", 85),

102 )

103

104

105@dataclass

106 class CommandsConfig:

107 """Command customization configuration."""

108

109 pre_implement: str | None = None

110 post_implement: str | None = None

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

112 confidence_gate: ConfidenceGateConfig = field(default_factory=ConfidenceGateConfig)

113 tdd_mode: bool = False

114

115 @classmethod

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

117 """Create CommandsConfig from dictionary."""

118 return cls(

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

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

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

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

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

124 )

125

126

127@dataclass

128 class ScoringWeightsConfig:

129 """Scoring weights for semantic conflict analysis.

130

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

132 Should sum to 1.0 for normalized scoring.

133

134 Attributes:

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

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

137 type: Weight for modification type match

138 """

139

140 semantic: float = 0.5

141 section: float = 0.3

142 type: float = 0.2

143

144 @classmethod

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

146 """Create ScoringWeightsConfig from dictionary."""

147 return cls(

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

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

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

151 )

152

153

154@dataclass

155 class DependencyMappingConfig:

156 """Dependency mapping threshold configuration.

157

158 Controls overlap detection sensitivity and conflict scoring thresholds.

159 Default values match the previously hardcoded constants for backwards

160 compatibility.

161

162 Attributes:

163 overlap_min_files: Minimum overlapping files to trigger overlap

164 overlap_min_ratio: Minimum ratio of overlapping files to smaller set

165 min_directory_depth: Minimum path segments for directory overlap

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

167 high_conflict_threshold: Above = HIGH conflict label

168 confidence_modifier: Applied when dependency direction is ambiguous

169 scoring_weights: Weights for semantic/section/type signals

170 exclude_common_files: Infrastructure files excluded from overlap detection

171 """

172

173 overlap_min_files: int = 2

174 overlap_min_ratio: float = 0.25

175 min_directory_depth: int = 2

176 conflict_threshold: float = 0.4

177 high_conflict_threshold: float = 0.7

178 confidence_modifier: float = 0.5

179 scoring_weights: ScoringWeightsConfig = field(default_factory=ScoringWeightsConfig)

180 exclude_common_files: list[str] = field(

181 default_factory=lambda: [

182 "__init__.py",

183 "pyproject.toml",

184 "setup.py",

185 "setup.cfg",

186 "CHANGELOG.md",

187 "README.md",

188 "conftest.py",

189 ]

190 )

191

192 @classmethod

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

194 """Create DependencyMappingConfig from dictionary."""

195 return cls(

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

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

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

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

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

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

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

203 exclude_common_files=data.get(

204 "exclude_common_files",

205 [

206 "__init__.py",

207 "pyproject.toml",

208 "setup.py",

209 "setup.cfg",

210 "CHANGELOG.md",

211 "README.md",

212 "conftest.py",

213 ],

214 ),

215 )