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,141 @@
1
+ """Multi-file edit orchestrator with atomic rollback."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import tempfile
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass
11
+ class FileEdit:
12
+ path: str # relative to project root
13
+ content: str # new file content (full replacement)
14
+ create: bool = False # True if creating new file
15
+
16
+
17
+ @dataclass
18
+ class OrchestratorPlan:
19
+ edits: list[FileEdit]
20
+ description: str = ""
21
+ dependencies: dict[str, list[str]] = field(default_factory=dict)
22
+ # dependencies: {file_path: [files_that_must_be_edited_first]}
23
+
24
+
25
+ @dataclass
26
+ class OrchestratorResult:
27
+ success: bool
28
+ applied: list[str] = field(default_factory=list) # paths successfully written
29
+ rolled_back: list[str] = field(default_factory=list) # paths restored on failure
30
+ error: str | None = None
31
+
32
+
33
+ class MultiFileOrchestrator:
34
+ """Coordinates multi-file edits with dependency ordering and atomic rollback.
35
+
36
+ On any failure, all already-applied edits are reverted to original content.
37
+ """
38
+
39
+ def __init__(self, project_root: str | Path = "."):
40
+ self._root = Path(project_root)
41
+
42
+ def apply(self, plan: OrchestratorPlan) -> OrchestratorResult:
43
+ """Apply all edits in dependency order. Roll back all on any failure."""
44
+ try:
45
+ ordered = self._topological_sort(plan.edits, plan.dependencies)
46
+ except ValueError as exc:
47
+ return OrchestratorResult(success=False, error=str(exc))
48
+
49
+ paths = [e.path for e in ordered]
50
+ backups = self._backup(paths)
51
+ applied: list[str] = []
52
+
53
+ try:
54
+ for edit in ordered:
55
+ self._write(edit)
56
+ applied.append(edit.path)
57
+ except Exception as exc: # noqa: BLE001
58
+ self._restore(backups)
59
+ return OrchestratorResult(
60
+ success=False,
61
+ rolled_back=list(backups.keys()),
62
+ error=str(exc),
63
+ )
64
+
65
+ return OrchestratorResult(success=True, applied=applied)
66
+
67
+ def _topological_sort(
68
+ self, edits: list[FileEdit], deps: dict[str, list[str]]
69
+ ) -> list[FileEdit]:
70
+ """Return edits sorted by dependency order (Kahn''s algorithm).
71
+
72
+ Raises ValueError on circular dependency.
73
+ """
74
+ edit_by_path: dict[str, FileEdit] = {e.path: e for e in edits}
75
+
76
+ # Build in-degree and adjacency: prereq -> [dependents]
77
+ in_degree: dict[str, int] = {e.path: 0 for e in edits}
78
+ dependents: dict[str, list[str]] = {e.path: [] for e in edits}
79
+
80
+ for path, prereqs in deps.items():
81
+ if path not in in_degree:
82
+ continue
83
+ for prereq in prereqs:
84
+ if prereq not in in_degree:
85
+ continue
86
+ in_degree[path] += 1
87
+ dependents[prereq].append(path)
88
+
89
+ # Kahn''s: start with zero-in-degree nodes (sorted for determinism)
90
+ queue: list[str] = sorted(p for p, d in in_degree.items() if d == 0)
91
+ result: list[FileEdit] = []
92
+
93
+ while queue:
94
+ path = queue.pop(0)
95
+ result.append(edit_by_path[path])
96
+ for dep in sorted(dependents[path]):
97
+ in_degree[dep] -= 1
98
+ if in_degree[dep] == 0:
99
+ queue.append(dep)
100
+
101
+ if len(result) != len(edits):
102
+ raise ValueError("Circular dependency detected in edit plan")
103
+
104
+ return result
105
+
106
+ def _backup(self, paths: list[str]) -> dict[str, str | None]:
107
+ """Snapshot current file contents. None if file does not exist."""
108
+ backups: dict[str, str | None] = {}
109
+ for path in paths:
110
+ abs_path = self._root / path
111
+ backups[path] = abs_path.read_text(encoding="utf-8") if abs_path.exists() else None
112
+ return backups
113
+
114
+ def _restore(self, backups: dict[str, str | None]) -> None:
115
+ """Restore files from backup snapshot."""
116
+ for path, content in backups.items():
117
+ abs_path = self._root / path
118
+ if content is None:
119
+ # File did not exist before: remove it if it was created
120
+ if abs_path.exists():
121
+ abs_path.unlink()
122
+ else:
123
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
124
+ abs_path.write_text(content, encoding="utf-8")
125
+
126
+ def _write(self, edit: FileEdit) -> None:
127
+ """Write a single FileEdit atomically (write to temp, then rename)."""
128
+ abs_path = self._root / edit.path
129
+ abs_path.parent.mkdir(parents=True, exist_ok=True)
130
+
131
+ fd, tmp_str = tempfile.mkstemp(dir=abs_path.parent, prefix=".gdm_tmp_")
132
+ try:
133
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
134
+ fh.write(edit.content)
135
+ os.replace(tmp_str, str(abs_path))
136
+ except Exception:
137
+ try:
138
+ os.unlink(tmp_str)
139
+ except OSError:
140
+ pass
141
+ raise
@@ -0,0 +1,251 @@
1
+ """Regression guard — capture test baselines before edits, verify after.
2
+
3
+ Protocol:
4
+ BEFORE editing file X:
5
+ 1. Locate test files that import X
6
+ 2. Run those tests and record pass/fail baseline
7
+ AFTER editing file X:
8
+ 1. Re-run the same tests
9
+ 2. Compare against baseline — report any newly failing tests
10
+ On 2 consecutive failures: recommend git reset to last checkpoint.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import random
16
+ import re
17
+ import subprocess
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ __all__ = ["RegressionGuard", "RegressionResult"]
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+ _TEST_RUN_TIMEOUT: int = 120
27
+ _MAX_SCOPED_TESTS: int = 50 # cap for performance scoping
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Data classes
32
+ # ---------------------------------------------------------------------------
33
+
34
+ @dataclass
35
+ class TestRun:
36
+ """Result of a single test run."""
37
+
38
+ passed: bool
39
+ failed_tests: list[str] = field(default_factory=list)
40
+ output: str = ""
41
+ exit_code: int = 0
42
+ coverage_pct: float | None = None
43
+
44
+
45
+ @dataclass
46
+ class RegressionResult:
47
+ """Outcome of a before/after regression comparison."""
48
+
49
+ file_edited: Path
50
+ baseline: TestRun
51
+ after_edit: TestRun
52
+ regressions: list[str] # test names that were passing and now fail
53
+ new_passes: list[str] # test names that were failing and now pass
54
+ rollback_recommended: bool
55
+ coverage_drop: float | None = None # pp drop in coverage, if measurable
56
+
57
+ @property
58
+ def new_failures(self) -> list[str]:
59
+ """Alias for regressions — tests that newly fail after the edit."""
60
+ return self.regressions
61
+
62
+ @property
63
+ def has_regression(self) -> bool:
64
+ return bool(self.regressions)
65
+
66
+ def summary(self) -> str:
67
+ """Human-readable summary for the agent."""
68
+ if not self.has_regression:
69
+ return f"✓ No regressions detected in {self.file_edited.name}"
70
+ lines = [f"⚠ Regressions in {self.file_edited.name}:"]
71
+ for t in self.regressions:
72
+ lines.append(f" ✗ {t}")
73
+ if self.rollback_recommended:
74
+ lines.append(" → Rollback recommended (2+ consecutive failures)")
75
+ return "\n".join(lines)
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # RegressionGuard
80
+ # ---------------------------------------------------------------------------
81
+
82
+ class RegressionGuard:
83
+ """Captures test baselines and detects regressions caused by file edits.
84
+
85
+ Usage::
86
+
87
+ guard = RegressionGuard(project_root)
88
+ guard.capture_baseline(Path("src/auth.py"))
89
+ # ... make edits ...
90
+ result = guard.verify_after_edit(Path("src/auth.py"))
91
+ if result.has_regression:
92
+ console.print(result.summary())
93
+ """
94
+
95
+ def __init__(self, project_root: Path) -> None:
96
+ self._root = project_root
97
+ self._baselines: dict[Path, TestRun] = {}
98
+ self._fail_streak: dict[Path, int] = {}
99
+
100
+ # ------------------------------------------------------------------
101
+ # Public API
102
+ # ------------------------------------------------------------------
103
+
104
+ def capture_baseline(self, file_path: Path) -> TestRun:
105
+ """Run tests for *file_path* and store baseline. Returns the run result."""
106
+ test_files = self._find_test_files(file_path)
107
+ if not test_files:
108
+ log.debug("No test files found for %s — skipping baseline", file_path)
109
+ baseline = TestRun(passed=True, output="(no tests found)")
110
+ else:
111
+ baseline = self._run_tests(test_files)
112
+ self._baselines[file_path] = baseline
113
+ return baseline
114
+
115
+ def verify_after_edit(self, file_path: Path) -> RegressionResult:
116
+ """Re-run tests and compare against stored baseline."""
117
+ baseline = self._baselines.get(file_path, TestRun(passed=True))
118
+ test_files = self._find_test_files(file_path)
119
+
120
+ if not test_files:
121
+ return RegressionResult(
122
+ file_edited=file_path,
123
+ baseline=baseline,
124
+ after_edit=TestRun(passed=True, output="(no tests found)"),
125
+ regressions=[],
126
+ new_passes=[],
127
+ rollback_recommended=False,
128
+ )
129
+
130
+ after = self._run_tests(test_files)
131
+ regressions = _compute_regressions(baseline, after)
132
+ new_passes = _compute_new_passes(baseline, after)
133
+
134
+ if regressions:
135
+ self._fail_streak[file_path] = self._fail_streak.get(file_path, 0) + 1
136
+ else:
137
+ self._fail_streak[file_path] = 0
138
+
139
+ rollback = self._fail_streak.get(file_path, 0) >= 2
140
+
141
+ coverage_drop: float | None = None
142
+ if baseline.coverage_pct is not None and after.coverage_pct is not None:
143
+ drop = baseline.coverage_pct - after.coverage_pct
144
+ if drop > 5.0:
145
+ coverage_drop = drop
146
+ log.warning(
147
+ "Coverage drop of %.1fpp detected for %s (%.1f%% → %.1f%%)",
148
+ drop, file_path, baseline.coverage_pct, after.coverage_pct,
149
+ )
150
+
151
+ return RegressionResult(
152
+ file_edited=file_path,
153
+ baseline=baseline,
154
+ after_edit=after,
155
+ regressions=regressions,
156
+ new_passes=new_passes,
157
+ rollback_recommended=rollback,
158
+ coverage_drop=coverage_drop,
159
+ )
160
+
161
+ def reset_streak(self, file_path: Path) -> None:
162
+ """Reset consecutive failure count for a file (e.g., after rollback)."""
163
+ self._fail_streak.pop(file_path, None)
164
+
165
+ # ------------------------------------------------------------------
166
+ # Private helpers
167
+ # ------------------------------------------------------------------
168
+
169
+ def _find_test_files(self, file_path: Path) -> list[Path]:
170
+ """Find test files that likely test the given source file.
171
+
172
+ Performance scoping: skip baseline if 0 tests found; sample randomly
173
+ (max _MAX_SCOPED_TESTS) if too many are found.
174
+ """
175
+ stem = file_path.stem
176
+ candidates: list[Path] = []
177
+
178
+ for pattern in (f"test_{stem}.py", f"{stem}_test.py", f"test_{stem}.ts"):
179
+ found = list(self._root.rglob(pattern))
180
+ candidates.extend(found)
181
+
182
+ if not candidates:
183
+ candidates = [
184
+ Path(p) for p in self._grep_import_refs(stem, self._root)
185
+ ]
186
+
187
+ if not candidates:
188
+ return []
189
+
190
+ if len(candidates) > _MAX_SCOPED_TESTS:
191
+ candidates = random.sample(candidates, _MAX_SCOPED_TESTS)
192
+
193
+ return candidates[:_MAX_SCOPED_TESTS]
194
+
195
+ def _grep_import_refs(self, module_name: str, root: Path) -> list[str]:
196
+ """Find test files that reference the given module name (Windows-safe)."""
197
+ results: list[str] = []
198
+ for py_file in root.rglob("test_*.py"):
199
+ try:
200
+ content = py_file.read_text(encoding="utf-8", errors="ignore")
201
+ if module_name in content:
202
+ results.append(str(py_file))
203
+ except OSError:
204
+ pass
205
+ return results
206
+
207
+ def _run_tests(self, test_files: list[Path]) -> TestRun:
208
+ """Run pytest on specific test files. Returns TestRun."""
209
+ args = ["python", "-m", "pytest", "--tb=line", "-q",
210
+ *[str(f) for f in test_files]]
211
+ try:
212
+ proc = subprocess.run(
213
+ args, capture_output=True, text=True,
214
+ cwd=self._root, timeout=_TEST_RUN_TIMEOUT,
215
+ )
216
+ combined = proc.stdout + proc.stderr
217
+ failed = _extract_failed_tests(combined)
218
+ return TestRun(
219
+ passed=proc.returncode == 0,
220
+ failed_tests=failed,
221
+ output=combined[:3_000],
222
+ exit_code=proc.returncode,
223
+ )
224
+ except subprocess.TimeoutExpired:
225
+ return TestRun(passed=False, output="Tests timed out", exit_code=1)
226
+ except OSError as exc:
227
+ return TestRun(passed=False, output=str(exc), exit_code=1)
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # Pure helpers
232
+ # ---------------------------------------------------------------------------
233
+
234
+ def _extract_failed_tests(output: str) -> list[str]:
235
+ """Parse pytest --tb=line output to extract failed test names."""
236
+ pattern = re.compile(r"FAILED (.+?) -")
237
+ return [m.group(1).strip() for m in pattern.finditer(output)]
238
+
239
+
240
+ def _compute_regressions(before: TestRun, after: TestRun) -> list[str]:
241
+ """Tests that were passing before and are failing after."""
242
+ before_failed = set(before.failed_tests)
243
+ after_failed = set(after.failed_tests)
244
+ return sorted(after_failed - before_failed)
245
+
246
+
247
+ def _compute_new_passes(before: TestRun, after: TestRun) -> list[str]:
248
+ """Tests that were failing before and are passing after."""
249
+ before_failed = set(before.failed_tests)
250
+ after_failed = set(after.failed_tests)
251
+ return sorted(before_failed - after_failed)