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.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. 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)