gdmcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gdmcode-0.1.0.dist-info/METADATA +240 -0
- gdmcode-0.1.0.dist-info/RECORD +131 -0
- gdmcode-0.1.0.dist-info/WHEEL +4 -0
- gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/_internal/__init__.py +0 -0
- src/_internal/constants.py +244 -0
- src/_internal/domain_skills.py +339 -0
- src/agent/__init__.py +0 -0
- src/agent/commit_classifier.py +91 -0
- src/agent/context_budget.py +391 -0
- src/agent/daemon.py +681 -0
- src/agent/dag_validator.py +153 -0
- src/agent/debug_loop.py +473 -0
- src/agent/impact_analyzer.py +149 -0
- src/agent/impact_graph.py +117 -0
- src/agent/loop.py +1410 -0
- src/agent/orchestrator.py +141 -0
- src/agent/regression_guard.py +251 -0
- src/agent/review_gate.py +648 -0
- src/agent/risk_scorer.py +169 -0
- src/agent/self_healing.py +145 -0
- src/agent/smart_test_selector.py +89 -0
- src/agent/system_prompt.py +226 -0
- src/agent/task_tracker.py +320 -0
- src/agent/test_validator.py +210 -0
- src/agent/tool_orchestrator.py +402 -0
- src/agent/transcript.py +230 -0
- src/agent/verification_loop.py +133 -0
- src/agent/work_director.py +136 -0
- src/agent/worktree_manager.py +53 -0
- src/artifacts/__init__.py +16 -0
- src/artifacts/artifact_store.py +456 -0
- src/artifacts/verification_graph.py +75 -0
- src/auth.py +411 -0
- src/cli.py +1290 -0
- src/commands.py +1398 -0
- src/config.py +762 -0
- src/cost_tracker.py +348 -0
- src/db/__init__.py +4 -0
- src/db/migrations.py +337 -0
- src/enterprise/__init__.py +3 -0
- src/enterprise/audit_log.py +182 -0
- src/enterprise/identity.py +90 -0
- src/enterprise/rbac.py +100 -0
- src/enterprise/team_config.py +125 -0
- src/enterprise/usage_analytics.py +261 -0
- src/exceptions.py +207 -0
- src/git_workflow.py +651 -0
- src/integrations/__init__.py +6 -0
- src/integrations/github_actions.py +106 -0
- src/integrations/mcp_server.py +333 -0
- src/integrations/sentry_integration.py +100 -0
- src/integrations/sentry_server.py +82 -0
- src/integrations/webhook_security.py +19 -0
- src/main.py +27 -0
- src/memory/__init__.py +0 -0
- src/memory/code_index.py +376 -0
- src/memory/compressor.py +378 -0
- src/memory/context_memory.py +135 -0
- src/memory/continuous_memory.py +234 -0
- src/memory/conventions.py +495 -0
- src/memory/db.py +1119 -0
- src/memory/document_index.py +205 -0
- src/memory/file_cache.py +128 -0
- src/memory/project_scanner.py +178 -0
- src/memory/session_store.py +201 -0
- src/models/__init__.py +0 -0
- src/models/client.py +715 -0
- src/models/definitions.py +459 -0
- src/models/router.py +418 -0
- src/models/schemas.py +389 -0
- src/permissions.py +294 -0
- src/remote/__init__.py +5 -0
- src/remote/command_filter.py +33 -0
- src/remote/models.py +31 -0
- src/remote/permission_handler.py +79 -0
- src/remote/phone_ui.py +48 -0
- src/remote/protocol.py +59 -0
- src/remote/qr.py +65 -0
- src/remote/server.py +586 -0
- src/remote/token_manager.py +61 -0
- src/remote/tunnel.py +212 -0
- src/repl.py +475 -0
- src/runtime/__init__.py +1 -0
- src/runtime/branch_farm.py +372 -0
- src/runtime/replay.py +351 -0
- src/sandbox/__init__.py +2 -0
- src/sandbox/hermetic.py +214 -0
- src/sandbox/policy.py +44 -0
- src/sdk/__init__.py +3 -0
- src/sdk/plugin_base.py +39 -0
- src/sdk/plugin_host.py +100 -0
- src/sdk/plugin_loader.py +101 -0
- src/security.py +409 -0
- src/server/__init__.py +7 -0
- src/server/bridge.py +427 -0
- src/server/bridge_cli.py +103 -0
- src/server/bridge_client.py +170 -0
- src/server/protocol_version.py +103 -0
- src/session/__init__.py +10 -0
- src/session/event_fanout.py +46 -0
- src/session/input_broker.py +38 -0
- src/session/permission_bridge.py +100 -0
- src/tools/__init__.py +160 -0
- src/tools/_atomic.py +72 -0
- src/tools/agent_tools.py +423 -0
- src/tools/ask_user_tool.py +83 -0
- src/tools/bash_tool.py +384 -0
- src/tools/browser_tool.py +352 -0
- src/tools/browser_tools.py +179 -0
- src/tools/dep_tools.py +210 -0
- src/tools/document_reader.py +167 -0
- src/tools/document_tool.py +240 -0
- src/tools/document_writer.py +171 -0
- src/tools/impact_tools.py +240 -0
- src/tools/playwright_tool.py +172 -0
- src/tools/quality_tools.py +366 -0
- src/tools/read_tools.py +318 -0
- src/tools/result_cache.py +157 -0
- src/tools/search_tools.py +310 -0
- src/tools/shell_tools.py +311 -0
- src/tools/write_tools.py +337 -0
- src/voice/__init__.py +25 -0
- src/voice/audio_capture.py +92 -0
- src/voice/audio_playback.py +68 -0
- src/voice/errors.py +14 -0
- src/voice/models.py +35 -0
- src/voice/providers.py +143 -0
- src/voice/vad.py +55 -0
- src/voice/voice_loop.py +156 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Branch Farm — parallel exploration of N independent solution approaches.
|
|
2
|
+
|
|
3
|
+
Workflow::
|
|
4
|
+
|
|
5
|
+
farm = BranchFarm(git_workflow, config=FarmConfig(n_branches=3))
|
|
6
|
+
best = farm.run("Fix the failing auth tests")
|
|
7
|
+
|
|
8
|
+
Each approach runs in its own ``git worktree`` via a ``ProcessPoolExecutor``
|
|
9
|
+
so workers have completely isolated filesystems. The winning branch is
|
|
10
|
+
squash- or cherry-pick-merged back onto the originating branch.
|
|
11
|
+
|
|
12
|
+
Requires ``autonomy_level >= 4`` to run unattended.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed, Future
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from src.exceptions import DirtyWorkingTreeError, MergeConflictError
|
|
25
|
+
from src.git_workflow import GitWorkflow, WorktreeInfo
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BranchFarm",
|
|
29
|
+
"BranchResult",
|
|
30
|
+
"BranchScore",
|
|
31
|
+
"FarmConfig",
|
|
32
|
+
"SCORE_WEIGHTS",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
log = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_FARM_BRANCH_PREFIX = "gdm/farm"
|
|
38
|
+
_WORKTREE_ROOT = Path(".context-memory/worktrees")
|
|
39
|
+
|
|
40
|
+
SCORE_WEIGHTS: dict[str, float] = {
|
|
41
|
+
"test_pass_rate": 0.40,
|
|
42
|
+
"lint_clean": 0.20,
|
|
43
|
+
"security_clean": 0.20,
|
|
44
|
+
"diff_size_inv": 0.10,
|
|
45
|
+
"conflict_risk_inv": 0.10,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Data classes
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FarmConfig:
|
|
55
|
+
"""Configuration for a BranchFarm run."""
|
|
56
|
+
n_branches: int = 3
|
|
57
|
+
max_workers: int = 3
|
|
58
|
+
timeout_secs: float = 300.0
|
|
59
|
+
autonomy_level: int = 4
|
|
60
|
+
merge_strategy: str = "squash" # "squash" | "cherry-pick" | "patch"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class BranchResult:
|
|
65
|
+
"""Outcome from one worker branch."""
|
|
66
|
+
branch: str
|
|
67
|
+
worktree: WorktreeInfo
|
|
68
|
+
ok: bool
|
|
69
|
+
error: str | None = None
|
|
70
|
+
tests_passed: int = 0
|
|
71
|
+
tests_total: int = 0
|
|
72
|
+
lint_errors: int = 0
|
|
73
|
+
security_findings: int = 0
|
|
74
|
+
diff_lines_added: int = 0
|
|
75
|
+
diff_lines_removed: int = 0
|
|
76
|
+
changed_files: int = 0
|
|
77
|
+
conflict_risk: float = 0.0 # 0.0–1.0; populated by dry-run merge analysis
|
|
78
|
+
head_sha: str | None = None # HEAD sha on the farm branch after the run
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class BranchScore:
|
|
83
|
+
"""Composite quality score for one branch result."""
|
|
84
|
+
branch: str
|
|
85
|
+
tests_passed: int = 0
|
|
86
|
+
tests_total: int = 0
|
|
87
|
+
lint_errors: int = 0
|
|
88
|
+
security_findings: int = 0
|
|
89
|
+
diff_lines_added: int = 0
|
|
90
|
+
diff_lines_removed: int = 0
|
|
91
|
+
changed_files: int = 0
|
|
92
|
+
conflict_risk: float = 0.0 # 0.0–1.0
|
|
93
|
+
composite: float = 0.0 # weighted sum; higher = better
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Module-level worker function (must be pickleable for ProcessPoolExecutor)
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def _worker_run(
|
|
101
|
+
worktree_path: Path,
|
|
102
|
+
branch: str,
|
|
103
|
+
task: str,
|
|
104
|
+
extra: dict[str, Any] | None = None,
|
|
105
|
+
) -> BranchResult:
|
|
106
|
+
"""Top-level pickleable entry point for each farm worker subprocess.
|
|
107
|
+
|
|
108
|
+
Override for testing by setting *extra["_worker_fn"]*. In production
|
|
109
|
+
this calls ``_run_gdm_in_worktree()``.
|
|
110
|
+
"""
|
|
111
|
+
if extra and "_worker_fn" in extra:
|
|
112
|
+
fn = extra["_worker_fn"]
|
|
113
|
+
return fn(worktree_path, branch, task)
|
|
114
|
+
return _run_gdm_in_worktree(worktree_path, branch, task)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run_gdm_in_worktree(
|
|
118
|
+
worktree_path: Path,
|
|
119
|
+
branch: str,
|
|
120
|
+
task: str,
|
|
121
|
+
) -> BranchResult:
|
|
122
|
+
"""Run gdm inside *worktree_path* and return scored result.
|
|
123
|
+
|
|
124
|
+
Subprocesses do NOT have access to the parent's in-memory state; all
|
|
125
|
+
information must come through function arguments and the filesystem.
|
|
126
|
+
"""
|
|
127
|
+
# Lazy imports to avoid circular dependencies inside the subprocess
|
|
128
|
+
import subprocess # noqa: PLC0415
|
|
129
|
+
|
|
130
|
+
from src.git_workflow import GitWorkflow
|
|
131
|
+
|
|
132
|
+
wf = GitWorkflow(worktree_path)
|
|
133
|
+
wt_info = WorktreeInfo(
|
|
134
|
+
branch=branch,
|
|
135
|
+
path=worktree_path,
|
|
136
|
+
db_path=worktree_path / ".context-memory" / "gdm.db",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# --- run pytest to collect pass/fail counts ---
|
|
140
|
+
pytest_result = subprocess.run(
|
|
141
|
+
["python", "-m", "pytest", "--tb=no", "-q"],
|
|
142
|
+
cwd=str(worktree_path),
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
)
|
|
146
|
+
tests_passed = 0
|
|
147
|
+
tests_total = 0
|
|
148
|
+
for line in pytest_result.stdout.splitlines():
|
|
149
|
+
import re
|
|
150
|
+
m = re.search(r"(\d+) passed", line)
|
|
151
|
+
if m:
|
|
152
|
+
tests_passed = int(m.group(1))
|
|
153
|
+
m2 = re.search(r"(\d+) failed", line)
|
|
154
|
+
if m2:
|
|
155
|
+
tests_total += int(m2.group(1))
|
|
156
|
+
tests_total += tests_passed
|
|
157
|
+
|
|
158
|
+
# --- diff size vs base HEAD ---
|
|
159
|
+
diff_result = subprocess.run(
|
|
160
|
+
["git", "diff", "--stat", "HEAD"],
|
|
161
|
+
cwd=str(worktree_path),
|
|
162
|
+
capture_output=True,
|
|
163
|
+
text=True,
|
|
164
|
+
)
|
|
165
|
+
lines_added = 0
|
|
166
|
+
lines_removed = 0
|
|
167
|
+
changed_files = 0
|
|
168
|
+
for line in diff_result.stdout.splitlines():
|
|
169
|
+
import re
|
|
170
|
+
m = re.search(r"(\d+) insertion", line)
|
|
171
|
+
if m:
|
|
172
|
+
lines_added = int(m.group(1))
|
|
173
|
+
m = re.search(r"(\d+) deletion", line)
|
|
174
|
+
if m:
|
|
175
|
+
lines_removed = int(m.group(1))
|
|
176
|
+
m = re.search(r"(\d+) file", line)
|
|
177
|
+
if m:
|
|
178
|
+
changed_files = int(m.group(1))
|
|
179
|
+
|
|
180
|
+
# --- current HEAD sha ---
|
|
181
|
+
sha_result = subprocess.run(
|
|
182
|
+
["git", "rev-parse", "HEAD"],
|
|
183
|
+
cwd=str(worktree_path),
|
|
184
|
+
capture_output=True,
|
|
185
|
+
text=True,
|
|
186
|
+
)
|
|
187
|
+
head_sha = sha_result.stdout.strip() or None
|
|
188
|
+
|
|
189
|
+
return BranchResult(
|
|
190
|
+
branch=branch,
|
|
191
|
+
worktree=wt_info,
|
|
192
|
+
ok=pytest_result.returncode == 0,
|
|
193
|
+
tests_passed=tests_passed,
|
|
194
|
+
tests_total=tests_total,
|
|
195
|
+
diff_lines_added=lines_added,
|
|
196
|
+
diff_lines_removed=lines_removed,
|
|
197
|
+
changed_files=changed_files,
|
|
198
|
+
head_sha=head_sha,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# BranchFarm
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
class BranchFarm:
|
|
207
|
+
"""Orchestrates parallel branch exploration and selects the best result.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
git_workflow: GitWorkflow for the project root.
|
|
211
|
+
config: FarmConfig tuning parameters.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(
|
|
215
|
+
self,
|
|
216
|
+
git_workflow: GitWorkflow,
|
|
217
|
+
config: FarmConfig | None = None,
|
|
218
|
+
*,
|
|
219
|
+
_worker_fn: Any = None, # injectable for tests
|
|
220
|
+
) -> None:
|
|
221
|
+
self._wf = git_workflow
|
|
222
|
+
self._config = config or FarmConfig()
|
|
223
|
+
self._worker_fn = _worker_fn # override _run_gdm_in_worktree in tests
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
# Public API
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def run(self, task: str) -> BranchResult:
|
|
230
|
+
"""Guard → create → dispatch → score → merge → cleanup."""
|
|
231
|
+
self._require_autonomy()
|
|
232
|
+
task_id = uuid.uuid4().hex[:8]
|
|
233
|
+
branches: list[WorktreeInfo] = []
|
|
234
|
+
try:
|
|
235
|
+
branches = self.create_branches(self._config.n_branches, task_id)
|
|
236
|
+
results = self._dispatch(branches, task)
|
|
237
|
+
scores = self.evaluate_results(results)
|
|
238
|
+
best_score = scores[0]
|
|
239
|
+
best_result = next(r for r in results if r.branch == best_score.branch)
|
|
240
|
+
self._merge_best(best_result)
|
|
241
|
+
return best_result
|
|
242
|
+
finally:
|
|
243
|
+
self._cleanup(branches)
|
|
244
|
+
|
|
245
|
+
def create_branches(self, n: int, task_id: str) -> list[WorktreeInfo]:
|
|
246
|
+
"""Guard dirty tree, then create N worktrees on fresh farm branches."""
|
|
247
|
+
if not self._wf.is_clean():
|
|
248
|
+
raise DirtyWorkingTreeError()
|
|
249
|
+
worktrees: list[WorktreeInfo] = []
|
|
250
|
+
root = self._wf._root
|
|
251
|
+
worktree_root = root / _WORKTREE_ROOT
|
|
252
|
+
for i in range(n):
|
|
253
|
+
branch = f"{_FARM_BRANCH_PREFIX}-{task_id}-{i}"
|
|
254
|
+
path = worktree_root / f"{task_id}-{i}"
|
|
255
|
+
wt = self._wf.create_worktree(branch, path)
|
|
256
|
+
worktrees.append(wt)
|
|
257
|
+
log.info("Created worktree %s on branch %s", path, branch)
|
|
258
|
+
return worktrees
|
|
259
|
+
|
|
260
|
+
def evaluate_results(self, results: list[BranchResult]) -> list[BranchScore]:
|
|
261
|
+
"""Score each result and return sorted list (best first)."""
|
|
262
|
+
scores: list[BranchScore] = []
|
|
263
|
+
for r in results:
|
|
264
|
+
if not r.ok:
|
|
265
|
+
scores.append(BranchScore(branch=r.branch, composite=-1.0))
|
|
266
|
+
continue
|
|
267
|
+
test_rate = r.tests_passed / max(r.tests_total, 1)
|
|
268
|
+
lint_score = 1.0 / (1.0 + r.lint_errors)
|
|
269
|
+
sec_score = 1.0 / (1.0 + r.security_findings)
|
|
270
|
+
diff_score = 1.0 / (1.0 + r.diff_lines_added + r.diff_lines_removed)
|
|
271
|
+
risk_score = 1.0 - r.conflict_risk
|
|
272
|
+
composite = (
|
|
273
|
+
SCORE_WEIGHTS["test_pass_rate"] * test_rate
|
|
274
|
+
+ SCORE_WEIGHTS["lint_clean"] * lint_score
|
|
275
|
+
+ SCORE_WEIGHTS["security_clean"] * sec_score
|
|
276
|
+
+ SCORE_WEIGHTS["diff_size_inv"] * diff_score
|
|
277
|
+
+ SCORE_WEIGHTS["conflict_risk_inv"] * risk_score
|
|
278
|
+
)
|
|
279
|
+
scores.append(BranchScore(
|
|
280
|
+
branch=r.branch,
|
|
281
|
+
tests_passed=r.tests_passed,
|
|
282
|
+
tests_total=r.tests_total,
|
|
283
|
+
lint_errors=r.lint_errors,
|
|
284
|
+
security_findings=r.security_findings,
|
|
285
|
+
diff_lines_added=r.diff_lines_added,
|
|
286
|
+
diff_lines_removed=r.diff_lines_removed,
|
|
287
|
+
changed_files=r.changed_files,
|
|
288
|
+
conflict_risk=r.conflict_risk,
|
|
289
|
+
composite=composite,
|
|
290
|
+
))
|
|
291
|
+
# Primary: composite desc; secondary: smaller diff (tiebreak)
|
|
292
|
+
return sorted(
|
|
293
|
+
scores,
|
|
294
|
+
key=lambda s: (s.composite, -(s.diff_lines_added + s.diff_lines_removed)),
|
|
295
|
+
reverse=True,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def pick_best(self, results: list[BranchResult]) -> BranchResult:
|
|
299
|
+
"""Return the BranchResult with the highest composite score."""
|
|
300
|
+
scores = self.evaluate_results(results)
|
|
301
|
+
best_branch = scores[0].branch
|
|
302
|
+
return next(r for r in results if r.branch == best_branch)
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# Private helpers
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def _require_autonomy(self) -> None:
|
|
309
|
+
if self._config.autonomy_level < 4:
|
|
310
|
+
raise PermissionError(
|
|
311
|
+
f"Branch farm requires autonomy_level >= 4 "
|
|
312
|
+
f"(current: {self._config.autonomy_level}). "
|
|
313
|
+
"Set autonomy_level=4 in FarmConfig or approve the run."
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _dispatch(
|
|
317
|
+
self,
|
|
318
|
+
worktrees: list[WorktreeInfo],
|
|
319
|
+
task: str,
|
|
320
|
+
) -> list[BranchResult]:
|
|
321
|
+
"""Submit all workers to a ProcessPoolExecutor, collect results."""
|
|
322
|
+
results: list[BranchResult] = []
|
|
323
|
+
extra = {"_worker_fn": self._worker_fn} if self._worker_fn else None
|
|
324
|
+
|
|
325
|
+
with ProcessPoolExecutor(max_workers=self._config.max_workers) as pool:
|
|
326
|
+
futures: dict[Future[BranchResult], WorktreeInfo] = {
|
|
327
|
+
pool.submit(_worker_run, wt.path, wt.branch, task, extra): wt
|
|
328
|
+
for wt in worktrees
|
|
329
|
+
}
|
|
330
|
+
for future in as_completed(futures, timeout=self._config.timeout_secs):
|
|
331
|
+
wt = futures[future]
|
|
332
|
+
try:
|
|
333
|
+
result = future.result()
|
|
334
|
+
except Exception as exc:
|
|
335
|
+
log.error("Worker %s failed: %s", wt.branch, exc)
|
|
336
|
+
result = BranchResult(
|
|
337
|
+
branch=wt.branch,
|
|
338
|
+
worktree=wt,
|
|
339
|
+
ok=False,
|
|
340
|
+
error=str(exc),
|
|
341
|
+
)
|
|
342
|
+
results.append(result)
|
|
343
|
+
return results
|
|
344
|
+
|
|
345
|
+
def _merge_best(self, best: BranchResult) -> None:
|
|
346
|
+
"""Dry-run check then merge the winning branch back to HEAD."""
|
|
347
|
+
dry_run = self._wf.merge_tree_dry_run(best.branch)
|
|
348
|
+
if "<<<<<<<" in dry_run:
|
|
349
|
+
raise MergeConflictError(best.branch, "Use --no-farm-auto-merge to resolve manually.")
|
|
350
|
+
|
|
351
|
+
strategy = self._config.merge_strategy
|
|
352
|
+
if strategy == "squash":
|
|
353
|
+
self._wf.squash_merge(
|
|
354
|
+
best.branch,
|
|
355
|
+
message=f"[gdm-farm] squash-merge {best.branch} (tests={best.tests_passed}/{best.tests_total})",
|
|
356
|
+
)
|
|
357
|
+
elif strategy == "cherry-pick":
|
|
358
|
+
if best.head_sha:
|
|
359
|
+
self._wf.cherry_pick(best.head_sha)
|
|
360
|
+
else:
|
|
361
|
+
raise MergeConflictError(best.branch, "No HEAD sha available for cherry-pick.")
|
|
362
|
+
else:
|
|
363
|
+
raise ValueError(f"Unknown merge_strategy: {strategy!r}")
|
|
364
|
+
|
|
365
|
+
def _cleanup(self, worktrees: list[WorktreeInfo]) -> None:
|
|
366
|
+
"""Remove all worktrees and farm branches (best-effort)."""
|
|
367
|
+
for wt in worktrees:
|
|
368
|
+
try:
|
|
369
|
+
self._wf.remove_worktree(wt.path)
|
|
370
|
+
self._wf.delete_branch(wt.branch)
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
log.warning("Cleanup failed for %s: %s", wt.path, exc)
|