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,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
|
+
]
|