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,133 @@
1
+ """Verification loop: run tests → debug → retry until green or max attempts."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Callable
8
+
9
+
10
+ @dataclass
11
+ class VerificationRound:
12
+ attempt: int
13
+ passed: bool
14
+ test_output: str
15
+ patch_applied: str | None = None # description of fix attempted
16
+ elapsed_s: float = 0.0
17
+
18
+
19
+ @dataclass
20
+ class VerificationResult:
21
+ success: bool # True if tests green at end
22
+ rounds: list[VerificationRound] = field(default_factory=list)
23
+ final_output: str = ""
24
+ total_attempts: int = 0
25
+
26
+
27
+ class VerificationLoop:
28
+ """Run tests, debug failures, retry until green or max_attempts reached.
29
+
30
+ Workflow per round:
31
+ 1. Run test suite (pytest)
32
+ 2. If green → done
33
+ 3. If red → extract failure message → call debug_fn → apply patch → go to 1
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ test_cmd: list[str] | None = None, # default: ["python", "-m", "pytest", "tests/", "-x", "-q"]
39
+ max_attempts: int = 3,
40
+ debug_fn: Callable[[str], str] | None = None, # receives failure output, returns patch description
41
+ apply_fn: Callable[[str], None] | None = None, # receives patch description, applies it
42
+ timeout_s: float = 120.0,
43
+ ):
44
+ self._test_cmd = test_cmd or ["python", "-m", "pytest", "tests/", "-x", "-q"]
45
+ self._max_attempts = max_attempts
46
+ self._debug_fn = debug_fn
47
+ self._apply_fn = apply_fn
48
+ self._timeout_s = timeout_s
49
+
50
+ def run(self, test_paths: list[str] | None = None) -> VerificationResult:
51
+ """Run the verification loop. Returns result after green or exhausted attempts."""
52
+ rounds: list[VerificationRound] = []
53
+
54
+ for attempt in range(1, self._max_attempts + 1):
55
+ t0 = time.monotonic()
56
+ passed, output = self._run_tests(test_paths)
57
+ elapsed = time.monotonic() - t0
58
+
59
+ if passed:
60
+ rounds.append(
61
+ VerificationRound(
62
+ attempt=attempt,
63
+ passed=True,
64
+ test_output=output,
65
+ elapsed_s=elapsed,
66
+ )
67
+ )
68
+ return VerificationResult(
69
+ success=True,
70
+ rounds=rounds,
71
+ final_output=output,
72
+ total_attempts=attempt,
73
+ )
74
+
75
+ patch_desc: str | None = None
76
+ if self._debug_fn is not None:
77
+ failure_msg = self._extract_failure(output)
78
+ patch_desc = self._debug_fn(failure_msg)
79
+
80
+ if self._apply_fn is not None and patch_desc is not None:
81
+ self._apply_fn(patch_desc)
82
+
83
+ rounds.append(
84
+ VerificationRound(
85
+ attempt=attempt,
86
+ passed=False,
87
+ test_output=output,
88
+ patch_applied=patch_desc,
89
+ elapsed_s=elapsed,
90
+ )
91
+ )
92
+
93
+ final_output = rounds[-1].test_output if rounds else ""
94
+ return VerificationResult(
95
+ success=False,
96
+ rounds=rounds,
97
+ final_output=final_output,
98
+ total_attempts=len(rounds),
99
+ )
100
+
101
+ def _run_tests(self, test_paths: list[str] | None) -> tuple[bool, str]:
102
+ """Run pytest subprocess. Returns (passed, output)."""
103
+ cmd = list(self._test_cmd)
104
+ if test_paths:
105
+ cmd.extend(test_paths)
106
+ try:
107
+ result = subprocess.run(
108
+ cmd,
109
+ capture_output=True,
110
+ text=True,
111
+ timeout=self._timeout_s,
112
+ )
113
+ output = result.stdout + result.stderr
114
+ return result.returncode == 0, output
115
+ except subprocess.TimeoutExpired:
116
+ return False, "Test timed out"
117
+ except OSError as exc:
118
+ return False, f"Failed to run tests: {exc}"
119
+
120
+ def _extract_failure(self, output: str) -> str:
121
+ """Extract the most relevant failure message from pytest output."""
122
+ lines = output.splitlines()
123
+ failed_lines = [ln for ln in lines if ln.startswith("FAILED ")]
124
+ if failed_lines:
125
+ last = failed_lines[-1]
126
+ if " - " in last:
127
+ return last.split(" - ", 1)[1][:120]
128
+ return last[len("FAILED "):][:120]
129
+ # Fall back to last non-empty line
130
+ for ln in reversed(lines):
131
+ if ln.strip():
132
+ return ln.strip()[:120]
133
+ return output[:120]
@@ -0,0 +1,136 @@
1
+ import json
2
+ import logging
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional
6
+
7
+ from src.agent.dag_validator import DagValidator, DagNode, ValidationResult
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class SubTaskResult:
14
+ task_id: str
15
+ status: str # "ok" | "failed" | "skipped"
16
+ output: str = ""
17
+ error: str = ""
18
+
19
+
20
+ PLANNER_PROMPT = """
21
+ Break the following instruction into specific subtasks.
22
+ Return ONLY a JSON array. Each item must have these exact keys:
23
+ "id" (unique kebab-id), "description", "depends_on" (list of ids),
24
+ "model_tier" (haiku|sonnet|opus), "write_capable" (bool)
25
+ Max 12 tasks. Keep dependencies minimal.
26
+ Instruction: {instruction}
27
+ """
28
+
29
+
30
+ class WorkDirector:
31
+ def __init__(self, client=None, worktree_manager=None):
32
+ self._client = client # GdmClient or mock
33
+ self._wm = worktree_manager # WorktreeManager or mock
34
+
35
+ def decompose(self, instruction: str) -> list[dict]:
36
+ """Stage 1: LLM proposes a DAG as JSON."""
37
+ if self._client is None:
38
+ # Fallback for tests: single-task linear plan
39
+ return [
40
+ {
41
+ "id": "task-1",
42
+ "description": instruction,
43
+ "depends_on": [],
44
+ "model_tier": "sonnet",
45
+ "write_capable": False,
46
+ }
47
+ ]
48
+ prompt = PLANNER_PROMPT.format(instruction=instruction)
49
+ response = self._client.complete([{"role": "user", "content": prompt}])
50
+ content = (
51
+ response.choices[0].message.content
52
+ if hasattr(response, "choices")
53
+ else str(response)
54
+ )
55
+ # Extract JSON array
56
+ start = content.find("[")
57
+ end = content.rfind("]") + 1
58
+ return json.loads(content[start:end])
59
+
60
+ def build_dag(self, instruction: str) -> list[DagNode]:
61
+ """Stage 2: validate + repair the raw proposal."""
62
+ raw = self.decompose(instruction)
63
+ validator = DagValidator(raw)
64
+ result = validator.validate()
65
+ if result.is_valid:
66
+ return result.dag
67
+ if result.repairable:
68
+ repaired = validator.auto_repair()
69
+ result2 = DagValidator(repaired).validate()
70
+ if result2.is_valid:
71
+ log.info("DAG auto-repaired: %s", result.errors)
72
+ return result2.dag
73
+ log.warning("DAG invalid after repair, falling back to linear: %s", result.errors)
74
+ linear = validator.fallback_linear()
75
+ return DagValidator(linear).validate().dag
76
+
77
+ def execute_dag(
78
+ self, dag: list[DagNode], base_branch: str = "staging"
79
+ ) -> dict[str, SubTaskResult]:
80
+ """Execute DAG with ThreadPoolExecutor. Write-capable tasks in worktrees."""
81
+ results: dict[str, SubTaskResult] = {}
82
+ pending = {t.id: t for t in dag}
83
+
84
+ with ThreadPoolExecutor(max_workers=3) as pool:
85
+ while pending:
86
+ ready = [
87
+ t
88
+ for t in pending.values()
89
+ if all(
90
+ dep in results and results[dep].status != "failed"
91
+ for dep in t.depends_on
92
+ )
93
+ ]
94
+ if not ready:
95
+ # Mark remaining tasks as skipped due to upstream failures
96
+ for t in pending.values():
97
+ results[t.id] = SubTaskResult(
98
+ task_id=t.id,
99
+ status="skipped",
100
+ error="Upstream dependency failed",
101
+ )
102
+ break
103
+ futures = {
104
+ pool.submit(self._run_subtask, t, base_branch): t for t in ready
105
+ }
106
+ for future in as_completed(futures):
107
+ t = futures[future]
108
+ try:
109
+ results[t.id] = future.result()
110
+ except Exception as e:
111
+ results[t.id] = SubTaskResult(
112
+ task_id=t.id, status="failed", error=str(e)
113
+ )
114
+ del pending[t.id]
115
+
116
+ return results
117
+
118
+ def _run_subtask(self, task: DagNode, base_branch: str) -> SubTaskResult:
119
+ worktree_path = None
120
+ try:
121
+ if task.write_capable and self._wm is not None:
122
+ worktree_path = self._wm.create_worktree(task.id, base_branch)
123
+ log.info("Running subtask %s (write=%s)", task.id, task.write_capable)
124
+ return SubTaskResult(
125
+ task_id=task.id,
126
+ status="ok",
127
+ output=f"Completed: {task.description}",
128
+ )
129
+ except Exception as e:
130
+ return SubTaskResult(task_id=task.id, status="failed", error=str(e))
131
+ finally:
132
+ if worktree_path and self._wm is not None:
133
+ try:
134
+ self._wm.remove_worktree(task.id)
135
+ except Exception:
136
+ pass
@@ -0,0 +1,53 @@
1
+ import subprocess
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+
5
+
6
+ @dataclass
7
+ class ReconcileResult:
8
+ conflicts: list[str] = field(default_factory=list)
9
+ merged: list[str] = field(default_factory=list)
10
+
11
+ @property
12
+ def success(self) -> bool:
13
+ return len(self.conflicts) == 0
14
+
15
+
16
+ class WorktreeManager:
17
+ BASE_DIR = Path(".gdm_worktrees")
18
+
19
+ def create_worktree(self, task_id: str, base_branch: str) -> Path:
20
+ branch = f"gdm/subtask-{task_id}"
21
+ path = self.BASE_DIR / task_id
22
+ path.parent.mkdir(parents=True, exist_ok=True)
23
+ subprocess.run(
24
+ ["git", "worktree", "add", "-b", branch, str(path), base_branch],
25
+ check=True,
26
+ capture_output=True,
27
+ )
28
+ return path
29
+
30
+ def remove_worktree(self, task_id: str) -> None:
31
+ path = self.BASE_DIR / task_id
32
+ subprocess.run(
33
+ ["git", "worktree", "remove", "--force", str(path)],
34
+ capture_output=True,
35
+ )
36
+ subprocess.run(
37
+ ["git", "branch", "-D", f"gdm/subtask-{task_id}"],
38
+ capture_output=True,
39
+ )
40
+
41
+ def reconcile(self, task_branches: list[str], target_branch: str) -> ReconcileResult:
42
+ conflicts, merged = [], []
43
+ for branch in task_branches:
44
+ result = subprocess.run(
45
+ ["git", "merge", "--no-ff", branch],
46
+ capture_output=True,
47
+ )
48
+ if result.returncode != 0:
49
+ conflicts.append(branch)
50
+ subprocess.run(["git", "merge", "--abort"], capture_output=True)
51
+ else:
52
+ merged.append(branch)
53
+ return ReconcileResult(conflicts=conflicts, merged=merged)
@@ -0,0 +1,16 @@
1
+ """Artifact storage and retrieval for gdm code."""
2
+ from src.artifacts.artifact_store import (
3
+ Artifact,
4
+ ArtifactDiff,
5
+ ArtifactNotFoundError,
6
+ ArtifactStore,
7
+ ArtifactVersion,
8
+ )
9
+
10
+ __all__ = [
11
+ "Artifact",
12
+ "ArtifactDiff",
13
+ "ArtifactNotFoundError",
14
+ "ArtifactStore",
15
+ "ArtifactVersion",
16
+ ]