devagent-ai 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {devagent_ai-0.6.0/devagent_ai.egg-info → devagent_ai-0.7.0}/PKG-INFO +1 -1
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/__init__.py +1 -1
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/discovery.py +125 -14
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/orchestrator.py +82 -4
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/workspace.py +51 -6
- {devagent_ai-0.6.0 → devagent_ai-0.7.0/devagent_ai.egg-info}/PKG-INFO +1 -1
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent_ai.egg-info/SOURCES.txt +5 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/pyproject.toml +1 -1
- devagent_ai-0.7.0/tests/test_huge_monorepo_v070.py +44 -0
- devagent_ai-0.7.0/tests/test_migration_e2e_v070.py +160 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_multistack_qualification.py +62 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_production_v040.py +1 -1
- devagent_ai-0.7.0/tests/test_structural_devagent_e2e_v070.py +120 -0
- devagent_ai-0.7.0/tests/test_structural_operations.py +111 -0
- devagent_ai-0.7.0/tests/test_v070_engineering_breadth.py +119 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/LICENSE +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/NOTICE +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/README.md +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/agent/__init__.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/agent/llm.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/agent/loop.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/agent/memory.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/agent/prompts.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/agent/tools.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/__main__.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/artifacts.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/browser.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/cli.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/config.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/evaluation.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/memory.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/models.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/providers.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/qualification.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/realworld.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/report.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/retrieval.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/routing.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/runtime.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/safety.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/source_control.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/state_machine.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/tasking.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/technical_review.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent/worktree.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent_ai.egg-info/dependency_links.txt +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent_ai.egg-info/entry_points.txt +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent_ai.egg-info/requires.txt +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/devagent_ai.egg-info/top_level.txt +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/setup.cfg +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_acceptance_contract.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_benchmark_catalog.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_browser_verification.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_capability_discovery.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_cli.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_developer_review_report.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_discovery_memory.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_e2e_fake_provider.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_evaluation_harness.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_evaluation_matrix.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_evaluation_regression_evidence.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_functional_qualification.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_model_routing.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_multilang_technical_review.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_multistack_devagent_e2e.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_packaging_metadata.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_preservation_contradiction.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_production_hardening.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_realworld_benchmark.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_retrieval.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_runtime_sandbox.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_safety_workspace.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_source_control_publish.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_structured_provider_contract.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_tasking_state.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_workspace_environment.py +0 -0
- {devagent_ai-0.6.0 → devagent_ai-0.7.0}/tests/test_worktree.py +0 -0
|
@@ -32,6 +32,8 @@ _LANGUAGE_EXTENSIONS = {
|
|
|
32
32
|
".cpp": "c++",
|
|
33
33
|
".h": "c/c++",
|
|
34
34
|
".cs": "c#",
|
|
35
|
+
".fs": "f#",
|
|
36
|
+
".vb": "visual-basic",
|
|
35
37
|
".rb": "ruby",
|
|
36
38
|
".php": "php",
|
|
37
39
|
}
|
|
@@ -46,7 +48,11 @@ _MANIFESTS = {
|
|
|
46
48
|
"cargo.toml",
|
|
47
49
|
"go.mod",
|
|
48
50
|
"pom.xml",
|
|
51
|
+
"mvnw",
|
|
49
52
|
"build.gradle",
|
|
53
|
+
"build.gradle.kts",
|
|
54
|
+
"settings.gradle",
|
|
55
|
+
"settings.gradle.kts",
|
|
50
56
|
"gradlew",
|
|
51
57
|
"cmakelists.txt",
|
|
52
58
|
"makefile",
|
|
@@ -56,6 +62,7 @@ _MANIFESTS = {
|
|
|
56
62
|
".gitlab-ci.yml",
|
|
57
63
|
"azure-pipelines.yml",
|
|
58
64
|
}
|
|
65
|
+
_PROJECT_SUFFIXES = {".sln", ".slnx", ".csproj", ".fsproj", ".vbproj"}
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
def _fingerprint(path: Path) -> str:
|
|
@@ -121,6 +128,65 @@ def _is_safe_generated_untracked_path(path: str) -> bool:
|
|
|
121
128
|
)
|
|
122
129
|
|
|
123
130
|
|
|
131
|
+
def _is_manifest(path: Path) -> bool:
|
|
132
|
+
return path.name.lower() in _MANIFESTS or path.suffix.lower() in _PROJECT_SUFFIXES
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_ci_file(path: Path, root: Path) -> bool:
|
|
136
|
+
relative = path.relative_to(root).as_posix()
|
|
137
|
+
return relative.startswith(".github/workflows/") or path.name.lower() in {
|
|
138
|
+
"jenkinsfile",
|
|
139
|
+
".gitlab-ci.yml",
|
|
140
|
+
"azure-pipelines.yml",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _priority_repository_files(root: Path, limit: int = 20_000) -> list[Path]:
|
|
145
|
+
"""Recover high-value tracked manifests/CI files even after the bounded walk is full.
|
|
146
|
+
|
|
147
|
+
A huge monorepo can contain more than the normal 12k inventory budget before a deep
|
|
148
|
+
component is reached. Git's index is a cheap authoritative directory of tracked paths;
|
|
149
|
+
we inspect only manifest/project/CI candidates and keep the result bounded.
|
|
150
|
+
"""
|
|
151
|
+
listed = _git(root, "ls-files")
|
|
152
|
+
if listed is None:
|
|
153
|
+
return []
|
|
154
|
+
priority: list[Path] = []
|
|
155
|
+
for raw in listed.splitlines():
|
|
156
|
+
if len(priority) >= limit:
|
|
157
|
+
break
|
|
158
|
+
if not raw or "\x00" in raw:
|
|
159
|
+
continue
|
|
160
|
+
relative = Path(raw)
|
|
161
|
+
if any(part in SKIP_DIRECTORIES for part in relative.parts) or is_secret_path(relative):
|
|
162
|
+
continue
|
|
163
|
+
candidate = root / relative
|
|
164
|
+
try:
|
|
165
|
+
resolved = candidate.resolve(strict=True)
|
|
166
|
+
resolved_relative = resolved.relative_to(root)
|
|
167
|
+
except (OSError, ValueError):
|
|
168
|
+
continue
|
|
169
|
+
if not resolved.is_file() or is_secret_path(resolved_relative):
|
|
170
|
+
continue
|
|
171
|
+
if _is_manifest(candidate) or _is_ci_file(candidate, root):
|
|
172
|
+
priority.append(candidate)
|
|
173
|
+
return priority
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _manifest_languages(path: Path) -> set[str]:
|
|
177
|
+
name = path.name.lower()
|
|
178
|
+
suffix = path.suffix.lower()
|
|
179
|
+
if name in {"pom.xml", "mvnw", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "gradlew"}:
|
|
180
|
+
return {"java"}
|
|
181
|
+
if suffix == ".csproj":
|
|
182
|
+
return {"c#"}
|
|
183
|
+
if suffix == ".fsproj":
|
|
184
|
+
return {"f#"}
|
|
185
|
+
if suffix == ".vbproj":
|
|
186
|
+
return {"visual-basic"}
|
|
187
|
+
return set()
|
|
188
|
+
|
|
189
|
+
|
|
124
190
|
def _package_capabilities(path: Path, relative: str) -> tuple[list[str], list[Capability]]:
|
|
125
191
|
frameworks: list[str] = []
|
|
126
192
|
capabilities: list[Capability] = []
|
|
@@ -149,9 +215,42 @@ def _package_capabilities(path: Path, relative: str) -> tuple[list[str], list[Ca
|
|
|
149
215
|
return sorted(set(frameworks)), capabilities
|
|
150
216
|
|
|
151
217
|
|
|
218
|
+
def _java_maven_command(path: Path, root: Path, component: str) -> tuple[str, ...]:
|
|
219
|
+
wrapper = path.parent / "mvnw"
|
|
220
|
+
if wrapper.is_file():
|
|
221
|
+
executable = "./mvnw" if component == "." else f"./{component}/mvnw"
|
|
222
|
+
else:
|
|
223
|
+
executable = "mvn"
|
|
224
|
+
return (executable, "test") if component == "." else (executable, "-f", path.relative_to(root).as_posix(), "test")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _dotnet_capabilities(path: Path, root: Path, component: str) -> list[Capability]:
|
|
228
|
+
relative = path.relative_to(root).as_posix()
|
|
229
|
+
suffix = path.suffix.lower()
|
|
230
|
+
capabilities: list[Capability] = []
|
|
231
|
+
if suffix in {".sln", ".slnx"}:
|
|
232
|
+
capabilities.append(Capability("build", ("dotnet", "build", relative), relative, component, True))
|
|
233
|
+
return capabilities
|
|
234
|
+
if suffix not in {".csproj", ".fsproj", ".vbproj"}:
|
|
235
|
+
return capabilities
|
|
236
|
+
|
|
237
|
+
text = path.read_text(encoding="utf-8", errors="ignore").lower()
|
|
238
|
+
assets = path.parent / "obj" / "project.assets.json"
|
|
239
|
+
no_restore = ("--no-restore",) if assets.is_file() else ()
|
|
240
|
+
capabilities.append(
|
|
241
|
+
Capability("build", ("dotnet", "build", relative, *no_restore), relative, component, True)
|
|
242
|
+
)
|
|
243
|
+
if "<istestproject>true" in text or "microsoft.net.test.sdk" in text:
|
|
244
|
+
capabilities.append(
|
|
245
|
+
Capability("test", ("dotnet", "test", relative, *no_restore), relative, component, False)
|
|
246
|
+
)
|
|
247
|
+
return capabilities
|
|
248
|
+
|
|
249
|
+
|
|
152
250
|
def _manifest_capabilities(path: Path, root: Path) -> tuple[list[str], list[Capability]]:
|
|
153
251
|
relative = path.relative_to(root).as_posix()
|
|
154
252
|
name = path.name.lower()
|
|
253
|
+
suffix = path.suffix.lower()
|
|
155
254
|
frameworks: list[str] = []
|
|
156
255
|
capabilities: list[Capability] = []
|
|
157
256
|
component = path.parent.relative_to(root).as_posix() or "."
|
|
@@ -180,12 +279,16 @@ def _manifest_capabilities(path: Path, root: Path) -> tuple[list[str], list[Capa
|
|
|
180
279
|
command = ("go", "test", "./...") if component == "." else ("go", "-C", component, "test", "./...")
|
|
181
280
|
capabilities.append(Capability("test", command, relative, component))
|
|
182
281
|
elif name == "pom.xml":
|
|
183
|
-
|
|
184
|
-
capabilities.append(Capability("test",
|
|
185
|
-
elif name in {"build.gradle", "gradlew"}:
|
|
282
|
+
frameworks.append("maven")
|
|
283
|
+
capabilities.append(Capability("test", _java_maven_command(path, root, component), relative, component))
|
|
284
|
+
elif name in {"build.gradle", "build.gradle.kts", "gradlew"}:
|
|
285
|
+
frameworks.append("gradle")
|
|
186
286
|
executable = "./gradlew" if component == "." and (path.parent / "gradlew").exists() else (f"./{component}/gradlew" if (path.parent / "gradlew").exists() else "gradle")
|
|
187
287
|
command = (executable, "test") if component == "." else (executable, "-p", component, "test")
|
|
188
288
|
capabilities.append(Capability("test", command, relative, component))
|
|
289
|
+
elif suffix in _PROJECT_SUFFIXES:
|
|
290
|
+
frameworks.append("dotnet")
|
|
291
|
+
capabilities.extend(_dotnet_capabilities(path, root, component))
|
|
189
292
|
elif name == "cmakelists.txt":
|
|
190
293
|
build_dir = "build" if component == "." else f"{component}/build"
|
|
191
294
|
capabilities.append(Capability("test", ("ctest", "--test-dir", build_dir), relative, component))
|
|
@@ -201,7 +304,7 @@ def _ci_capabilities(path: Path, root: Path) -> list[Capability]:
|
|
|
201
304
|
"""Extract simple argv commands from CI run/command lines as high-value evidence."""
|
|
202
305
|
relative = path.relative_to(root).as_posix()
|
|
203
306
|
commands: list[Capability] = []
|
|
204
|
-
allowed = {"python", "python3", "pytest", "npm", "pnpm", "yarn", "go", "cargo", "mvn", "gradle", "./gradlew", "make", "ctest", "dotnet", "ruff", "mypy"}
|
|
307
|
+
allowed = {"python", "python3", "pytest", "npm", "pnpm", "yarn", "go", "cargo", "mvn", "gradle", "./gradlew", "./mvnw", "make", "ctest", "dotnet", "ruff", "mypy"}
|
|
205
308
|
for raw_line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
206
309
|
stripped = raw_line.strip()
|
|
207
310
|
value = ""
|
|
@@ -414,14 +517,17 @@ def _probe_python_test_capability(
|
|
|
414
517
|
|
|
415
518
|
def discover_repository(root: Path | str, *, probe_capabilities: bool = True) -> RepositoryModel:
|
|
416
519
|
root_path = Path(root).expanduser().resolve()
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
520
|
+
walked_files = _walk(root_path)
|
|
521
|
+
files = list(walked_files)
|
|
522
|
+
seen = {path.relative_to(root_path).as_posix() for path in files}
|
|
523
|
+
for path in _priority_repository_files(root_path):
|
|
524
|
+
relative = path.relative_to(root_path).as_posix()
|
|
525
|
+
if relative not in seen:
|
|
526
|
+
files.append(path)
|
|
527
|
+
seen.add(relative)
|
|
528
|
+
|
|
529
|
+
ci_files = [path for path in files if _is_ci_file(path, root_path)]
|
|
530
|
+
manifests = [path for path in files if _is_manifest(path)]
|
|
425
531
|
component_paths = {root_path}
|
|
426
532
|
for manifest in manifests:
|
|
427
533
|
if manifest.parent != root_path and manifest not in ci_files:
|
|
@@ -434,6 +540,7 @@ def discover_repository(root: Path | str, *, probe_capabilities: bool = True) ->
|
|
|
434
540
|
owned_files = [path for path in files if path == component_path or component_path in path.parents]
|
|
435
541
|
language_counts = Counter(_LANGUAGE_EXTENSIONS[path.suffix.lower()] for path in owned_files if path.suffix.lower() in _LANGUAGE_EXTENSIONS)
|
|
436
542
|
component_manifests = [path for path in manifests if path.parent == component_path]
|
|
543
|
+
inferred_languages = set().union(*(_manifest_languages(path) for path in component_manifests)) if component_manifests else set()
|
|
437
544
|
frameworks: list[str] = []
|
|
438
545
|
capabilities: list[Capability] = []
|
|
439
546
|
for manifest in component_manifests:
|
|
@@ -456,9 +563,13 @@ def discover_repository(root: Path | str, *, probe_capabilities: bool = True) ->
|
|
|
456
563
|
if probed is not None:
|
|
457
564
|
capabilities.append(probed)
|
|
458
565
|
frameworks.append("pytest" if "pytest" in probed.command else "unittest")
|
|
566
|
+
ordered_languages = [name for name, _ in language_counts.most_common()]
|
|
567
|
+
for language in sorted(inferred_languages):
|
|
568
|
+
if language not in ordered_languages:
|
|
569
|
+
ordered_languages.append(language)
|
|
459
570
|
component = Component(
|
|
460
571
|
path=relative_component,
|
|
461
|
-
languages=
|
|
572
|
+
languages=ordered_languages,
|
|
462
573
|
frameworks=sorted(set(frameworks)),
|
|
463
574
|
manifests=[path.relative_to(root_path).as_posix() for path in component_manifests],
|
|
464
575
|
test_locations=_test_locations(owned_files, root_path, component_path),
|
|
@@ -501,7 +612,7 @@ def discover_repository(root: Path | str, *, probe_capabilities: bool = True) ->
|
|
|
501
612
|
git_branch=_git(root_path, "branch", "--show-current"),
|
|
502
613
|
git_head=_git(root_path, "rev-parse", "HEAD"),
|
|
503
614
|
dirty_files=sorted(tracked_dirty | untracked_dirty),
|
|
504
|
-
inventory_file_count=len(
|
|
615
|
+
inventory_file_count=len(walked_files),
|
|
505
616
|
capability_diagnostics=capability_diagnostics,
|
|
506
617
|
)
|
|
507
618
|
|
|
@@ -99,7 +99,58 @@ _WRITE_ACTION = {
|
|
|
99
99
|
"required": ["tool", "arguments"],
|
|
100
100
|
"additionalProperties": False,
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
_DELETE_ACTION = {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"tool": {"type": "string", "const": "delete_file"},
|
|
106
|
+
"arguments": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {"path": _NON_EMPTY_STRING},
|
|
109
|
+
"required": ["path"],
|
|
110
|
+
"additionalProperties": False,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
"required": ["tool", "arguments"],
|
|
114
|
+
"additionalProperties": False,
|
|
115
|
+
}
|
|
116
|
+
_MOVE_ACTION = {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"tool": {"type": "string", "const": "move_file"},
|
|
120
|
+
"arguments": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {"source": _NON_EMPTY_STRING, "destination": _NON_EMPTY_STRING},
|
|
123
|
+
"required": ["source", "destination"],
|
|
124
|
+
"additionalProperties": False,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
"required": ["tool", "arguments"],
|
|
128
|
+
"additionalProperties": False,
|
|
129
|
+
}
|
|
130
|
+
_RENAME_ACTION = {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"tool": {"type": "string", "const": "rename_file"},
|
|
134
|
+
"arguments": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"properties": {"source": _NON_EMPTY_STRING, "destination": _NON_EMPTY_STRING},
|
|
137
|
+
"required": ["source", "destination"],
|
|
138
|
+
"additionalProperties": False,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
"required": ["tool", "arguments"],
|
|
142
|
+
"additionalProperties": False,
|
|
143
|
+
}
|
|
144
|
+
_ACTION = {
|
|
145
|
+
"anyOf": [
|
|
146
|
+
_REPLACE_ACTION,
|
|
147
|
+
_COUNTED_REPLACE_ACTION,
|
|
148
|
+
_WRITE_ACTION,
|
|
149
|
+
_DELETE_ACTION,
|
|
150
|
+
_MOVE_ACTION,
|
|
151
|
+
_RENAME_ACTION,
|
|
152
|
+
]
|
|
153
|
+
}
|
|
103
154
|
|
|
104
155
|
UNDERSTANDING_SCHEMA = {
|
|
105
156
|
"type": "object",
|
|
@@ -310,12 +361,35 @@ def _execute_actions(workspace: Workspace, response: dict[str, Any], allowed_pat
|
|
|
310
361
|
if not isinstance(actions, list) or not actions:
|
|
311
362
|
raise OrchestrationError("Implementation requires at least one structured action")
|
|
312
363
|
changed: list[str] = []
|
|
364
|
+
allowed_tools = {"replace_text", "write_file", "delete_file", "move_file", "rename_file"}
|
|
313
365
|
for action in actions:
|
|
314
366
|
if not isinstance(action, dict) or set(action) != {"tool", "arguments"} or not isinstance(action["arguments"], dict):
|
|
315
367
|
raise OrchestrationError("Every action must contain only tool and arguments")
|
|
316
368
|
tool, arguments = action["tool"], action["arguments"]
|
|
317
|
-
if tool not in
|
|
369
|
+
if tool not in allowed_tools:
|
|
318
370
|
raise OrchestrationError(f"Implementation tool is not allowed: {tool}")
|
|
371
|
+
|
|
372
|
+
if tool in {"move_file", "rename_file"}:
|
|
373
|
+
if set(arguments) != {"source", "destination"}:
|
|
374
|
+
raise OrchestrationError(f"{tool} requires only string source and destination")
|
|
375
|
+
source = arguments.get("source")
|
|
376
|
+
destination = arguments.get("destination")
|
|
377
|
+
if (
|
|
378
|
+
not isinstance(source, str)
|
|
379
|
+
or not isinstance(destination, str)
|
|
380
|
+
or source not in allowed_paths
|
|
381
|
+
or destination not in allowed_paths
|
|
382
|
+
):
|
|
383
|
+
raise OrchestrationError(
|
|
384
|
+
f"Structural source/destination was not inspected/planned: {source} -> {destination}"
|
|
385
|
+
)
|
|
386
|
+
if tool == "move_file":
|
|
387
|
+
workspace.move_file(source, destination)
|
|
388
|
+
else:
|
|
389
|
+
workspace.rename_file(source, destination)
|
|
390
|
+
changed.extend((source, destination))
|
|
391
|
+
continue
|
|
392
|
+
|
|
319
393
|
path = arguments.get("path")
|
|
320
394
|
if not isinstance(path, str) or path not in allowed_paths:
|
|
321
395
|
raise OrchestrationError(f"Action path was not inspected/planned: {path}")
|
|
@@ -329,12 +403,16 @@ def _execute_actions(workspace: Workspace, response: dict[str, Any], allowed_pat
|
|
|
329
403
|
if isinstance(count, bool) or not isinstance(count, int) or count < 1:
|
|
330
404
|
raise OrchestrationError("replace_text count must be an integer greater than zero")
|
|
331
405
|
workspace.replace_text(path, arguments["old"], arguments["new"], count)
|
|
332
|
-
|
|
406
|
+
elif tool == "write_file":
|
|
333
407
|
if set(arguments) != {"path", "content"} or not isinstance(arguments.get("content"), str):
|
|
334
408
|
raise OrchestrationError("write_file requires only string path and content")
|
|
335
409
|
workspace.write_file(path, arguments["content"])
|
|
410
|
+
else:
|
|
411
|
+
if set(arguments) != {"path"}:
|
|
412
|
+
raise OrchestrationError("delete_file requires only string path")
|
|
413
|
+
workspace.delete_file(path)
|
|
336
414
|
changed.append(path)
|
|
337
|
-
return changed
|
|
415
|
+
return list(dict.fromkeys(changed))
|
|
338
416
|
|
|
339
417
|
|
|
340
418
|
def _diff(root: Path, workspace: Workspace, max_chars: int = 40_000) -> str:
|
|
@@ -146,6 +146,57 @@ class Workspace:
|
|
|
146
146
|
self.modified_paths.add(self._relative(target))
|
|
147
147
|
self.artifacts.record("text_replaced", path=path, count=count, revision=self.revision)
|
|
148
148
|
|
|
149
|
+
def _reject_structural_symlinks(self, path: str) -> None:
|
|
150
|
+
lexical = self.root
|
|
151
|
+
for part in Path(path).parts:
|
|
152
|
+
lexical = lexical / part
|
|
153
|
+
if lexical.is_symlink():
|
|
154
|
+
raise SafetyError(f"Structural file operations do not follow symlinks: {path}")
|
|
155
|
+
|
|
156
|
+
def _structural_file(self, path: str) -> Path:
|
|
157
|
+
self._reject_structural_symlinks(path)
|
|
158
|
+
target = self._writable(path)
|
|
159
|
+
if not target.exists() or not target.is_file():
|
|
160
|
+
raise SafetyError(f"Structural operation requires an existing regular file: {path}")
|
|
161
|
+
return target
|
|
162
|
+
|
|
163
|
+
def delete_file(self, path: str) -> None:
|
|
164
|
+
"""Delete one planned regular file after preserving a run-local backup."""
|
|
165
|
+
target = self._structural_file(path)
|
|
166
|
+
relative = self._relative(target)
|
|
167
|
+
self.artifacts.backup(target, relative_to=self.root)
|
|
168
|
+
target.unlink()
|
|
169
|
+
self.revision += 1
|
|
170
|
+
self.modified_paths.add(relative)
|
|
171
|
+
self.artifacts.record("file_deleted", path=relative, revision=self.revision)
|
|
172
|
+
|
|
173
|
+
def move_file(self, source: str, destination: str, *, operation: str = "move") -> None:
|
|
174
|
+
"""Move/rename one planned regular file without overwriting existing content."""
|
|
175
|
+
source_target = self._structural_file(source)
|
|
176
|
+
self._reject_structural_symlinks(destination)
|
|
177
|
+
destination_target = self._writable(destination)
|
|
178
|
+
source_relative = self._relative(source_target)
|
|
179
|
+
destination_relative = self._relative(destination_target)
|
|
180
|
+
if source_relative == destination_relative:
|
|
181
|
+
raise SafetyError("Structural source and destination must be different")
|
|
182
|
+
if destination_target.exists():
|
|
183
|
+
raise SafetyError(f"Refusing to overwrite structural destination: {destination_relative}")
|
|
184
|
+
self.artifacts.backup(source_target, relative_to=self.root)
|
|
185
|
+
destination_target.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
source_target.replace(destination_target)
|
|
187
|
+
self.revision += 1
|
|
188
|
+
self.modified_paths.update({source_relative, destination_relative})
|
|
189
|
+
self.artifacts.record(
|
|
190
|
+
"file_moved",
|
|
191
|
+
source=source_relative,
|
|
192
|
+
destination=destination_relative,
|
|
193
|
+
operation=operation,
|
|
194
|
+
revision=self.revision,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def rename_file(self, source: str, destination: str) -> None:
|
|
198
|
+
self.move_file(source, destination, operation="rename")
|
|
199
|
+
|
|
149
200
|
def _validate_python_requirement_file(self, target: Path) -> None:
|
|
150
201
|
if target.stat().st_size > 1_000_000:
|
|
151
202
|
raise SafetyError(f"Dependency requirement file is too large: {self._relative(target)}")
|
|
@@ -258,9 +309,6 @@ class Workspace:
|
|
|
258
309
|
for key, value in os.environ.items()
|
|
259
310
|
if key in {"PATH", "LANG", "LC_ALL", "TERM", "VIRTUAL_ENV", "PYTHONPATH", "SYSTEMROOT", "WINDIR"}
|
|
260
311
|
}
|
|
261
|
-
# Keep HOME sandboxed so package-manager and cloud credentials are not inherited.
|
|
262
|
-
# Rustup-managed cargo/rustc binaries still need the installed toolchain directory,
|
|
263
|
-
# which is safe to expose separately from CARGO_HOME and registry credentials.
|
|
264
312
|
rustup_home = os.environ.get("RUSTUP_HOME")
|
|
265
313
|
if rustup_home:
|
|
266
314
|
environment["RUSTUP_HOME"] = rustup_home
|
|
@@ -284,9 +332,6 @@ class Workspace:
|
|
|
284
332
|
}
|
|
285
333
|
)
|
|
286
334
|
if dependency_install and Path(argv[0]).name.lower() == "yarn":
|
|
287
|
-
# Yarn Classic understands ignore-scripts; Yarn Modern uses enableScripts
|
|
288
|
-
# and rejects the old --ignore-scripts CLI flag. Supplying both environment
|
|
289
|
-
# settings is safe: each generation consumes the setting it understands.
|
|
290
335
|
environment["YARN_IGNORE_SCRIPTS"] = "true"
|
|
291
336
|
environment["YARN_ENABLE_SCRIPTS"] = "false"
|
|
292
337
|
try:
|
|
@@ -51,6 +51,8 @@ tests/test_evaluation_harness.py
|
|
|
51
51
|
tests/test_evaluation_matrix.py
|
|
52
52
|
tests/test_evaluation_regression_evidence.py
|
|
53
53
|
tests/test_functional_qualification.py
|
|
54
|
+
tests/test_huge_monorepo_v070.py
|
|
55
|
+
tests/test_migration_e2e_v070.py
|
|
54
56
|
tests/test_model_routing.py
|
|
55
57
|
tests/test_multilang_technical_review.py
|
|
56
58
|
tests/test_multistack_devagent_e2e.py
|
|
@@ -64,7 +66,10 @@ tests/test_retrieval.py
|
|
|
64
66
|
tests/test_runtime_sandbox.py
|
|
65
67
|
tests/test_safety_workspace.py
|
|
66
68
|
tests/test_source_control_publish.py
|
|
69
|
+
tests/test_structural_devagent_e2e_v070.py
|
|
70
|
+
tests/test_structural_operations.py
|
|
67
71
|
tests/test_structured_provider_contract.py
|
|
68
72
|
tests/test_tasking_state.py
|
|
73
|
+
tests/test_v070_engineering_breadth.py
|
|
69
74
|
tests/test_workspace_environment.py
|
|
70
75
|
tests/test_worktree.py
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from devagent.discovery import discover_repository
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _production_only() -> None:
|
|
13
|
+
if os.getenv("DEVAGENT_PRODUCTION_QUALIFICATION") != "1":
|
|
14
|
+
pytest.skip("huge-monorepo stress runs only through the production qualification gate")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_deep_dotnet_manifest_is_recovered_beyond_12000_file_walk_frontier(tmp_path: Path) -> None:
|
|
18
|
+
_production_only()
|
|
19
|
+
filler = tmp_path / "aaa"
|
|
20
|
+
filler.mkdir()
|
|
21
|
+
for index in range(12_025):
|
|
22
|
+
(filler / f"f{index:05d}.txt").write_text("", encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
deep = tmp_path / "zzz" / "service"
|
|
25
|
+
deep.mkdir(parents=True)
|
|
26
|
+
(deep / "Service.csproj").write_text(
|
|
27
|
+
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework>'
|
|
28
|
+
'</PropertyGroup></Project>\n',
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
31
|
+
(deep / "Service.cs").write_text(
|
|
32
|
+
"namespace Deep; public static class Service { public static int Value => 1; }\n",
|
|
33
|
+
encoding="utf-8",
|
|
34
|
+
)
|
|
35
|
+
subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
|
|
36
|
+
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
|
|
37
|
+
|
|
38
|
+
repository = discover_repository(tmp_path, probe_capabilities=False)
|
|
39
|
+
|
|
40
|
+
assert repository.inventory_file_count == 12_000
|
|
41
|
+
component = next(item for item in repository.components if item.path == "zzz/service")
|
|
42
|
+
assert "zzz/service/Service.csproj" in component.manifests
|
|
43
|
+
assert "c#" in component.languages
|
|
44
|
+
assert any(item.command == ("dotnet", "build", "zzz/service/Service.csproj") for item in component.capabilities)
|