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,157 @@
1
+ """LRU+TTL cache for tool results — tools-004.
2
+
3
+ Thread-safe, bounded cache that avoids redundant tool calls.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import json
9
+ import threading
10
+ import time
11
+ from collections import OrderedDict
12
+ from dataclasses import dataclass, field
13
+ from functools import wraps
14
+ from typing import Any, Callable
15
+
16
+
17
+ __all__ = ["CacheEntry", "ToolResultCache", "cached_tool"]
18
+
19
+
20
+ @dataclass
21
+ class CacheEntry:
22
+ result: Any
23
+ created_at: float = field(default_factory=time.monotonic)
24
+ hits: int = 0
25
+
26
+
27
+ class ToolResultCache:
28
+ """Bounded LRU cache with TTL for tool call results.
29
+
30
+ Args:
31
+ max_size: Maximum number of entries before LRU eviction.
32
+ ttl: Time-to-live in seconds (uses ``time.monotonic``).
33
+ """
34
+
35
+ def __init__(self, max_size: int = 256, ttl: float = 300.0) -> None:
36
+ self._max_size = max_size
37
+ self._ttl = ttl
38
+ self._store: OrderedDict[str, CacheEntry] = OrderedDict()
39
+ self._lock = threading.Lock()
40
+ self._hits = 0
41
+ self._misses = 0
42
+ self._evictions = 0
43
+
44
+ # ------------------------------------------------------------------
45
+ # Public API
46
+ # ------------------------------------------------------------------
47
+
48
+ def get(self, tool_name: str, args: dict[str, Any]) -> Any | None:
49
+ """Return cached result or None if missing / expired."""
50
+ key = self._make_key(tool_name, args)
51
+ with self._lock:
52
+ entry = self._store.get(key)
53
+ if entry is None:
54
+ self._misses += 1
55
+ return None
56
+ if time.monotonic() - entry.created_at > self._ttl:
57
+ del self._store[key]
58
+ self._misses += 1
59
+ return None
60
+ # Move to end (most-recently-used)
61
+ self._store.move_to_end(key)
62
+ entry.hits += 1
63
+ self._hits += 1
64
+ return entry.result
65
+
66
+ def put(self, tool_name: str, args: dict[str, Any], result: Any) -> None:
67
+ """Store a result, evicting the LRU entry if at capacity."""
68
+ key = self._make_key(tool_name, args)
69
+ with self._lock:
70
+ if key in self._store:
71
+ self._store.move_to_end(key)
72
+ self._store[key] = CacheEntry(result=result)
73
+ return
74
+ if len(self._store) >= self._max_size:
75
+ self._store.popitem(last=False) # evict LRU (oldest)
76
+ self._evictions += 1
77
+ self._store[key] = CacheEntry(result=result)
78
+
79
+ def invalidate(self, tool_name: str | None = None) -> int:
80
+ """Remove entries.
81
+
82
+ Args:
83
+ tool_name: If given, remove only entries for that tool.
84
+ If None, clear everything.
85
+
86
+ Returns:
87
+ Number of entries removed.
88
+ """
89
+ with self._lock:
90
+ if tool_name is None:
91
+ count = len(self._store)
92
+ self._store.clear()
93
+ return count
94
+ prefix = tool_name + ":"
95
+ keys = [k for k in self._store if k.startswith(prefix)]
96
+ for k in keys:
97
+ del self._store[k]
98
+ return len(keys)
99
+
100
+ def stats(self) -> dict[str, int]:
101
+ """Return cache statistics."""
102
+ with self._lock:
103
+ return {
104
+ "size": len(self._store),
105
+ "hits": self._hits,
106
+ "misses": self._misses,
107
+ "evictions": self._evictions,
108
+ }
109
+
110
+ # ------------------------------------------------------------------
111
+ # Internal helpers
112
+ # ------------------------------------------------------------------
113
+
114
+ @staticmethod
115
+ def _make_key(tool_name: str, args: dict[str, Any]) -> str:
116
+ """Deterministic SHA-256 key; arg order does not affect the result."""
117
+ canonical = json.dumps(args, sort_keys=True, separators=(",", ":"), default=str)
118
+ payload = f"{tool_name}:{canonical}"
119
+ return tool_name + ":" + hashlib.sha256(payload.encode()).hexdigest()
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Decorator
124
+ # ---------------------------------------------------------------------------
125
+
126
+
127
+ def cached_tool(
128
+ cache: ToolResultCache,
129
+ tool_name: str | None = None,
130
+ ) -> Callable:
131
+ """Decorator that caches a tool function''s return value.
132
+
133
+ Args:
134
+ cache: ``ToolResultCache`` instance to use.
135
+ tool_name: Cache key prefix. Defaults to the wrapped function''s name.
136
+ """
137
+
138
+ def decorator(fn: Callable) -> Callable:
139
+ name = tool_name or fn.__name__
140
+
141
+ @wraps(fn)
142
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
143
+ # Build a stable args dict from positional + keyword arguments
144
+ call_args: dict[str, Any] = {f"_arg{i}": v for i, v in enumerate(args)}
145
+ call_args.update(kwargs)
146
+
147
+ cached = cache.get(name, call_args)
148
+ if cached is not None:
149
+ return cached
150
+
151
+ result = fn(*args, **kwargs)
152
+ cache.put(name, call_args, result)
153
+ return result
154
+
155
+ return wrapper
156
+
157
+ return decorator
@@ -0,0 +1,310 @@
1
+ """GlobTool and GrepTool — fast file pattern and content search.
2
+
3
+ GlobTool: pathlib-based file pattern matching with .gdmignore support.
4
+ GrepTool: ripgrep-based content search (falls back to Python re if rg not found).
5
+
6
+ Both tools tag their output as untrusted before returning, since file paths and
7
+ content could contain adversarial data.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import fnmatch
12
+ import logging
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any, ClassVar
18
+
19
+ from src.tools import REGISTRY, ToolBase, ToolResult
20
+
21
+ __all__ = ["GlobTool", "GrepTool"]
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+ _MAX_RESULTS: int = 500 # max file paths to return
26
+ _MAX_GREP_OUTPUT_BYTES: int = 30_000
27
+ _GDMIGNORE_FILENAME = ".gdmignore"
28
+
29
+ # Directories always excluded from search.
30
+ _DEFAULT_EXCLUDE_DIRS: frozenset[str] = frozenset({
31
+ ".git", "__pycache__", ".venv", "venv", "env",
32
+ "node_modules", ".next", ".nuxt", "dist", "build",
33
+ ".mypy_cache", ".pytest_cache", ".ruff_cache",
34
+ ".context-memory",
35
+ })
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # .gdmignore support
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def _load_ignore_patterns(workspace: Path) -> list[str]:
43
+ """Read .gdmignore from workspace root; return list of fnmatch patterns."""
44
+ ignore_file = workspace / _GDMIGNORE_FILENAME
45
+ if not ignore_file.exists():
46
+ return []
47
+ try:
48
+ lines = ignore_file.read_text(encoding="utf-8").splitlines()
49
+ return [ln.strip() for ln in lines if ln.strip() and not ln.startswith("#")]
50
+ except OSError:
51
+ return []
52
+
53
+
54
+ def _is_ignored(path: Path, patterns: list[str]) -> bool:
55
+ """Return True if any glob pattern matches the path."""
56
+ name = path.name
57
+ return any(fnmatch.fnmatch(name, pat) for pat in patterns)
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # GlobTool
62
+ # ---------------------------------------------------------------------------
63
+
64
+ class GlobTool(ToolBase):
65
+ """Find files matching a glob pattern within the project."""
66
+
67
+ name: ClassVar[str] = "glob"
68
+ description: ClassVar[str] = (
69
+ "Find files matching a glob pattern (e.g. '**/*.py', 'src/**/*.ts'). "
70
+ "Returns up to 500 matching paths relative to the project root. "
71
+ "Respects .gdmignore and always excludes .git, node_modules, __pycache__."
72
+ )
73
+ input_schema: ClassVar[dict[str, Any]] = {
74
+ "type": "object",
75
+ "required": ["pattern"],
76
+ "properties": {
77
+ "pattern": {
78
+ "type": "string",
79
+ "description": "Glob pattern, e.g. '**/*.py' or 'src/**/*.ts'.",
80
+ },
81
+ "cwd": {
82
+ "type": "string",
83
+ "description": "Search root. Defaults to project workspace.",
84
+ },
85
+ },
86
+ "additionalProperties": False,
87
+ }
88
+
89
+ def __init__(self, workspace: Path | None = None) -> None:
90
+ self._workspace = workspace
91
+
92
+ def execute(self, params: dict[str, Any]) -> ToolResult: # noqa: D102
93
+ pattern: str = params["pattern"]
94
+ cwd_str: str | None = params.get("cwd")
95
+
96
+ root = Path(cwd_str).resolve() if cwd_str else (self._workspace or Path.cwd())
97
+ if not root.is_dir():
98
+ return ToolResult(output="", error=f"Directory not found: {root}")
99
+
100
+ ignore_patterns = _load_ignore_patterns(root)
101
+
102
+ try:
103
+ matches: list[Path] = []
104
+ for p in root.glob(pattern):
105
+ if not p.is_file():
106
+ continue
107
+ # Skip excluded dirs.
108
+ if any(part in _DEFAULT_EXCLUDE_DIRS for part in p.parts):
109
+ continue
110
+ if _is_ignored(p, ignore_patterns):
111
+ continue
112
+ matches.append(p)
113
+ if len(matches) >= _MAX_RESULTS:
114
+ break
115
+ except Exception as exc: # noqa: BLE001
116
+ return ToolResult(output="", error=f"Glob failed: {exc}")
117
+
118
+ if not matches:
119
+ return ToolResult(output="No files matched.")
120
+
121
+ lines = [str(p.relative_to(root)) for p in sorted(matches)]
122
+ output = "\n".join(lines)
123
+ truncated = len(matches) >= _MAX_RESULTS
124
+
125
+ return ToolResult(
126
+ output=output,
127
+ truncated=truncated,
128
+ metadata={"count": len(matches), "root": str(root)},
129
+ )
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # GrepTool
134
+ # ---------------------------------------------------------------------------
135
+
136
+ class GrepTool(ToolBase):
137
+ """Search file contents using ripgrep (or Python regex fallback)."""
138
+
139
+ name: ClassVar[str] = "grep"
140
+ description: ClassVar[str] = (
141
+ "Search for a regex pattern in file contents. "
142
+ "Uses ripgrep if available for speed; otherwise falls back to Python re. "
143
+ "Returns matching lines with file:line format. Output capped at 30 KB."
144
+ )
145
+ input_schema: ClassVar[dict[str, Any]] = {
146
+ "type": "object",
147
+ "required": ["pattern"],
148
+ "properties": {
149
+ "pattern": {
150
+ "type": "string",
151
+ "description": "Regular expression to search for.",
152
+ },
153
+ "path": {
154
+ "type": "string",
155
+ "description": "File or directory to search. Defaults to project root.",
156
+ },
157
+ "glob": {
158
+ "type": "string",
159
+ "description": "File glob filter, e.g. '*.py'.",
160
+ },
161
+ "case_insensitive": {
162
+ "type": "boolean",
163
+ "description": "Case-insensitive search.",
164
+ },
165
+ "context_lines": {
166
+ "type": "integer",
167
+ "description": "Lines of context before and after each match (0–10).",
168
+ },
169
+ },
170
+ "additionalProperties": False,
171
+ }
172
+
173
+ def __init__(self, workspace: Path | None = None) -> None:
174
+ self._workspace = workspace
175
+
176
+ def execute(self, params: dict[str, Any]) -> ToolResult: # noqa: D102
177
+ pattern: str = params["pattern"]
178
+ path_str: str | None = params.get("path")
179
+ glob_filter: str | None = params.get("glob")
180
+ case_insensitive: bool = bool(params.get("case_insensitive", False))
181
+ context_lines: int = min(int(params.get("context_lines", 0)), 10)
182
+
183
+ search_root = (
184
+ Path(path_str).resolve()
185
+ if path_str
186
+ else (self._workspace or Path.cwd())
187
+ )
188
+
189
+ if _rg_available():
190
+ output, err = _rg_search(
191
+ pattern,
192
+ search_root,
193
+ glob_filter=glob_filter,
194
+ case_insensitive=case_insensitive,
195
+ context_lines=context_lines,
196
+ )
197
+ else:
198
+ output, err = _python_search(
199
+ pattern,
200
+ search_root,
201
+ glob_filter=glob_filter,
202
+ case_insensitive=case_insensitive,
203
+ context_lines=context_lines,
204
+ )
205
+
206
+ if err:
207
+ return ToolResult(output="", error=err)
208
+ if not output.strip():
209
+ return ToolResult(output="No matches found.")
210
+
211
+ truncated = len(output.encode("utf-8")) >= _MAX_GREP_OUTPUT_BYTES
212
+ return ToolResult(output=output, truncated=truncated)
213
+
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # ripgrep implementation
217
+ # ---------------------------------------------------------------------------
218
+
219
+ def _rg_available() -> bool:
220
+ import shutil
221
+ return shutil.which("rg") is not None
222
+
223
+
224
+ def _rg_search(
225
+ pattern: str,
226
+ root: Path,
227
+ *,
228
+ glob_filter: str | None,
229
+ case_insensitive: bool,
230
+ context_lines: int,
231
+ ) -> tuple[str, str | None]:
232
+ """Run ripgrep; return (output, error)."""
233
+ args = ["rg", "--line-number", "--no-heading", "--color=never"]
234
+ if case_insensitive:
235
+ args.append("--ignore-case")
236
+ if context_lines:
237
+ args.extend(["-C", str(context_lines)])
238
+ if glob_filter:
239
+ args.extend(["--glob", glob_filter])
240
+ # Exclude default dirs.
241
+ for d in _DEFAULT_EXCLUDE_DIRS:
242
+ args.extend(["--glob", f"!{d}/**"])
243
+ args.extend(["--", pattern, str(root)])
244
+
245
+ try:
246
+ result = subprocess.run(args, capture_output=True, text=True, timeout=30)
247
+ output = result.stdout
248
+ if len(output.encode("utf-8")) > _MAX_GREP_OUTPUT_BYTES:
249
+ output = output.encode("utf-8")[:_MAX_GREP_OUTPUT_BYTES].decode("utf-8", errors="replace")
250
+ return output, None
251
+ except subprocess.TimeoutExpired:
252
+ return "", "Grep timed out after 30s"
253
+ except Exception as exc: # noqa: BLE001
254
+ return "", str(exc)
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # Pure-Python fallback
259
+ # ---------------------------------------------------------------------------
260
+
261
+ def _python_search(
262
+ pattern: str,
263
+ root: Path,
264
+ *,
265
+ glob_filter: str | None,
266
+ case_insensitive: bool,
267
+ context_lines: int,
268
+ ) -> tuple[str, str | None]:
269
+ """Walk root and search files with re; return (output, error)."""
270
+ flags = re.IGNORECASE if case_insensitive else 0
271
+ try:
272
+ compiled = re.compile(pattern, flags)
273
+ except re.error as exc:
274
+ return "", f"Invalid regex: {exc}"
275
+
276
+ file_glob = glob_filter or "*"
277
+ results: list[str] = []
278
+ total_bytes = 0
279
+
280
+ for filepath in root.rglob(file_glob):
281
+ if not filepath.is_file():
282
+ continue
283
+ if any(part in _DEFAULT_EXCLUDE_DIRS for part in filepath.parts):
284
+ continue
285
+ try:
286
+ lines = filepath.read_text(encoding="utf-8", errors="replace").splitlines()
287
+ except OSError:
288
+ continue
289
+
290
+ for i, line in enumerate(lines):
291
+ if compiled.search(line):
292
+ ctx_start = max(0, i - context_lines)
293
+ ctx_end = min(len(lines), i + context_lines + 1)
294
+ for j in range(ctx_start, ctx_end):
295
+ prefix = ">" if j == i else " "
296
+ entry = f"{filepath}:{j + 1}{prefix} {lines[j]}"
297
+ results.append(entry)
298
+ total_bytes += len(entry.encode("utf-8"))
299
+ if total_bytes >= _MAX_GREP_OUTPUT_BYTES:
300
+ return "\n".join(results), None
301
+
302
+ return "\n".join(results), None
303
+
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # Auto-register
307
+ # ---------------------------------------------------------------------------
308
+
309
+ REGISTRY.register(GlobTool())
310
+ REGISTRY.register(GrepTool())