agentic-devtools 0.2.596__py3-none-any.whl → 0.2.598__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.
- agentic_devtools/_version.py +2 -2
- agentic_devtools/cli/ci/windows_specific.py +32 -34
- agentic_devtools/cli/ci/windows_specific_cli.py +362 -0
- agentic_devtools/cli/setup/dependency_checker.py +70 -71
- agentic_devtools/cli/setup/doctor.py +2 -2
- {agentic_devtools-0.2.596.dist-info → agentic_devtools-0.2.598.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.596.dist-info → agentic_devtools-0.2.598.dist-info}/RECORD +10 -9
- {agentic_devtools-0.2.596.dist-info → agentic_devtools-0.2.598.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.596.dist-info → agentic_devtools-0.2.598.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.596.dist-info → agentic_devtools-0.2.598.dist-info}/licenses/LICENSE +0 -0
agentic_devtools/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.598'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 598)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -13,6 +13,9 @@ from collections.abc import Collection, Mapping, Sequence
|
|
|
13
13
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
14
14
|
from typing import Literal, NamedTuple, cast
|
|
15
15
|
|
|
16
|
+
from agentic_devtools.cli.ci import windows_specific_cli
|
|
17
|
+
from agentic_devtools.cli.ci.windows_specific_cli import _check_bindings, _run_cli, _unique_object
|
|
18
|
+
|
|
16
19
|
__all__ = [
|
|
17
20
|
"ProductGroup",
|
|
18
21
|
"SelectorPolicy",
|
|
@@ -23,6 +26,7 @@ __all__ = [
|
|
|
23
26
|
"run_selected_tests",
|
|
24
27
|
"audit_test_results",
|
|
25
28
|
"evaluate_gate",
|
|
29
|
+
"main",
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
ProductGroup = Literal["setup-check", "portability", "benchmark"]
|
|
@@ -107,9 +111,14 @@ _CONTRACT_PATHS = {
|
|
|
107
111
|
".github/workflows/pr-smart-module-tests.yml",
|
|
108
112
|
"tests/workflows/test_windows_specific_ci.py",
|
|
109
113
|
"agentic_devtools/cli/ci/windows_specific.py",
|
|
114
|
+
"agentic_devtools/cli/ci/windows_specific_cli.py",
|
|
110
115
|
"agentic_devtools/cli/ci/windows_fixture_bytes.py",
|
|
111
116
|
}
|
|
112
|
-
_CONTRACT_PREFIXES = (
|
|
117
|
+
_CONTRACT_PREFIXES = (
|
|
118
|
+
"tests/unit/cli/ci/windows_specific/",
|
|
119
|
+
"tests/unit/cli/ci/windows_specific_cli/",
|
|
120
|
+
"tests/unit/cli/ci/windows_fixture_bytes/",
|
|
121
|
+
)
|
|
113
122
|
_EXPECTATION_NAMES = {"EXPECTED_SELECTORS", "EXPECTED_EXPANSIONS"}
|
|
114
123
|
_NODE_ID_PATTERN = re.compile(
|
|
115
124
|
r"(?:(?!\.{1,2}(?:/|$))[^/\\:\s]+/)*(?!\.{1,2}(?:/|$))[^/\\:\s]+\.py"
|
|
@@ -153,7 +162,7 @@ def _parse_source(source: str) -> SelectorPolicy:
|
|
|
153
162
|
if assignments.keys() != _EXPECTATION_NAMES:
|
|
154
163
|
raise ValueError("Missing simple top-level expectation assignments")
|
|
155
164
|
|
|
156
|
-
_check_bindings(tree, {id(statement.targets[0]) for statement in assignments.values()})
|
|
165
|
+
_check_bindings(tree, {id(statement.targets[0]) for statement in assignments.values()}, _EXPECTATION_NAMES)
|
|
157
166
|
|
|
158
167
|
values = {}
|
|
159
168
|
for name, statement in assignments.items():
|
|
@@ -171,38 +180,6 @@ def _parse_source(source: str) -> SelectorPolicy:
|
|
|
171
180
|
return _validate_policy(values["EXPECTED_SELECTORS"], values["EXPECTED_EXPANSIONS"])
|
|
172
181
|
|
|
173
182
|
|
|
174
|
-
def _check_bindings(tree: ast.Module, allowed_targets: set[int]) -> None:
|
|
175
|
-
"""Reject expectation bindings other than the two approved literal targets."""
|
|
176
|
-
for node in ast.walk(tree):
|
|
177
|
-
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
178
|
-
if node.id in _EXPECTATION_NAMES and id(node) not in allowed_targets:
|
|
179
|
-
raise ValueError("Additional expectation binding or deletion")
|
|
180
|
-
# Some Python bindings are string fields rather than Name(Store) nodes.
|
|
181
|
-
names: Sequence[str | None]
|
|
182
|
-
if isinstance(node, ast.alias):
|
|
183
|
-
if node.name == "*":
|
|
184
|
-
raise ValueError("Wildcard imports may rebind policy expectations")
|
|
185
|
-
names = [node.asname or node.name.split(".")[0]]
|
|
186
|
-
elif isinstance(node, ast.arg):
|
|
187
|
-
names = [node.arg]
|
|
188
|
-
elif isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
189
|
-
names = node.names
|
|
190
|
-
else:
|
|
191
|
-
names = [getattr(node, "name", None), getattr(node, "rest", None)]
|
|
192
|
-
if any(name in _EXPECTATION_NAMES for name in names):
|
|
193
|
-
raise ValueError("Additional expectation binding")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
197
|
-
"""Reject duplicate JSON keys at every nesting level."""
|
|
198
|
-
result: dict[str, object] = {}
|
|
199
|
-
for key, value in pairs:
|
|
200
|
-
if key in result:
|
|
201
|
-
raise ValueError(f"Duplicate JSON object key: {key}")
|
|
202
|
-
result[key] = value
|
|
203
|
-
return result
|
|
204
|
-
|
|
205
|
-
|
|
206
183
|
def load_policy(
|
|
207
184
|
manifest_path: str | Path,
|
|
208
185
|
base_policy_path: str | Path,
|
|
@@ -498,3 +475,24 @@ def evaluate_gate(
|
|
|
498
475
|
):
|
|
499
476
|
return {"ok": False, "error_code": "required_evidence_missing"}
|
|
500
477
|
return {"ok": True, "classification": "native-evidence" if groups else "not-applicable"}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
481
|
+
"""Run the focused Windows CI command dispatcher with trusted APIs."""
|
|
482
|
+
policy_loader: windows_specific_cli._LoadPolicy[SelectorPolicy, ProductGroup] = load_policy
|
|
483
|
+
group_selector: windows_specific_cli._SelectGroups[ProductGroup] = select_groups
|
|
484
|
+
test_runner: windows_specific_cli._RunSelectedTests[SelectorPolicy, ProductGroup] = run_selected_tests
|
|
485
|
+
gate_evaluator: windows_specific_cli._EvaluateGate[Literal["success", "failure", "cancelled", "skipped"]] = (
|
|
486
|
+
evaluate_gate
|
|
487
|
+
)
|
|
488
|
+
return _run_cli(
|
|
489
|
+
argv,
|
|
490
|
+
load_policy=policy_loader,
|
|
491
|
+
select_groups=group_selector,
|
|
492
|
+
run_selected_tests=test_runner,
|
|
493
|
+
evaluate_gate=gate_evaluator,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
if __name__ == "__main__":
|
|
498
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Private command dispatch for trusted Windows-specific CI orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ast
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Collection, Mapping, Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal, Never, Protocol, TypeVar, cast
|
|
14
|
+
|
|
15
|
+
from agentic_devtools.cli.ci.windows_fixture_bytes import verify_fixture_bytes
|
|
16
|
+
|
|
17
|
+
POut = TypeVar("POut", covariant=True)
|
|
18
|
+
PIn = TypeVar("PIn", contravariant=True)
|
|
19
|
+
GOut = TypeVar("GOut", bound=Literal["setup-check", "portability", "benchmark"], covariant=True)
|
|
20
|
+
GIn = TypeVar("GIn", bound=Literal["setup-check", "portability", "benchmark"], contravariant=True)
|
|
21
|
+
RIn = TypeVar("RIn", bound=Literal["success", "failure", "cancelled", "skipped"], contravariant=True)
|
|
22
|
+
_P = str | Path
|
|
23
|
+
type _G[T] = Collection[T]
|
|
24
|
+
_R = dict[str, bool | list[str] | int]
|
|
25
|
+
_S = Mapping[str, object]
|
|
26
|
+
_E = dict[str, bool | str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _LoadPolicy(Protocol[POut, GIn]):
|
|
30
|
+
def __call__(self, m: _P, b: _P, h: _P, /, *, applicable_groups: _G[GIn], bootstrap: bool) -> POut: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _SelectGroups(Protocol[GOut]):
|
|
34
|
+
def __call__(self, changed_paths: Sequence[str]) -> tuple[frozenset[GOut], bool]: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _RunSelectedTests(Protocol[PIn, GIn]):
|
|
38
|
+
def __call__(self, root: _P, policy: PIn, groups: _G[GIn], /, *, junit_path: _P) -> _R: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _EvaluateGate(Protocol[RIn]):
|
|
42
|
+
def __call__(self, selection: _S, linux: RIn | None, windows: RIn | None, /) -> _E: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check_bindings(tree: ast.Module, allowed_targets: set[int], expectation_names: set[str]) -> None:
|
|
46
|
+
"""Reject expectation bindings other than the approved literal targets."""
|
|
47
|
+
for node in ast.walk(tree):
|
|
48
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
49
|
+
if node.id in expectation_names and id(node) not in allowed_targets:
|
|
50
|
+
raise ValueError("Additional expectation binding or deletion")
|
|
51
|
+
# Some Python bindings are string fields rather than Name(Store) nodes.
|
|
52
|
+
names: Sequence[str | None]
|
|
53
|
+
if isinstance(node, ast.alias):
|
|
54
|
+
if node.name == "*":
|
|
55
|
+
raise ValueError("Wildcard imports may rebind policy expectations")
|
|
56
|
+
names = [node.asname or node.name.split(".")[0]]
|
|
57
|
+
elif isinstance(node, ast.arg):
|
|
58
|
+
names = [node.arg]
|
|
59
|
+
elif isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
60
|
+
names = node.names
|
|
61
|
+
else:
|
|
62
|
+
names = [getattr(node, "name", None), getattr(node, "rest", None)]
|
|
63
|
+
if any(name in expectation_names for name in names):
|
|
64
|
+
raise ValueError("Additional expectation binding")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
|
68
|
+
"""Reject duplicate JSON keys at every nesting level."""
|
|
69
|
+
result: dict[str, object] = {}
|
|
70
|
+
for key, value in pairs:
|
|
71
|
+
if key in result:
|
|
72
|
+
raise ValueError(f"Duplicate JSON object key: {key}")
|
|
73
|
+
result[key] = value
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _Dispatcher[
|
|
78
|
+
PolicyT,
|
|
79
|
+
GroupT: Literal["setup-check", "portability", "benchmark"],
|
|
80
|
+
ResultT: Literal["success", "failure", "cancelled", "skipped"],
|
|
81
|
+
]:
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
load_policy: _LoadPolicy[PolicyT, GroupT],
|
|
85
|
+
select_groups: _SelectGroups[GroupT],
|
|
86
|
+
run_selected_tests: _RunSelectedTests[PolicyT, GroupT],
|
|
87
|
+
evaluate_gate: _EvaluateGate[ResultT],
|
|
88
|
+
) -> None:
|
|
89
|
+
self._load_policy = load_policy
|
|
90
|
+
self._select_groups = select_groups
|
|
91
|
+
self._run_selected_tests = run_selected_tests
|
|
92
|
+
self._evaluate_gate = evaluate_gate
|
|
93
|
+
self._product_group_order = ("setup-check", "portability", "benchmark")
|
|
94
|
+
|
|
95
|
+
def _emit(self, payload: Mapping[str, object], status: int = 0) -> int:
|
|
96
|
+
print(json.dumps(payload, separators=(",", ":")))
|
|
97
|
+
return status
|
|
98
|
+
|
|
99
|
+
def _fail(self, error_code: str, detail: object, status: int = 1) -> int:
|
|
100
|
+
print(str(detail), file=sys.stderr)
|
|
101
|
+
return self._emit({"ok": False, "error_code": error_code}, status)
|
|
102
|
+
|
|
103
|
+
def _missing_file_code(self, exc: FileNotFoundError, base_policy: str, manifest: str, head_policy: str) -> str:
|
|
104
|
+
if not isinstance(exc.filename, str):
|
|
105
|
+
return "policy_io_failed"
|
|
106
|
+
path = Path(exc.filename)
|
|
107
|
+
if path == Path(base_policy):
|
|
108
|
+
return "base_policy_missing"
|
|
109
|
+
if path == Path(manifest):
|
|
110
|
+
return "manifest_missing"
|
|
111
|
+
if path == Path(head_policy):
|
|
112
|
+
return "head_policy_missing"
|
|
113
|
+
return "policy_io_failed"
|
|
114
|
+
|
|
115
|
+
def _decode_json(self, raw: str, error_code: str) -> tuple[object | None, int | None]:
|
|
116
|
+
try:
|
|
117
|
+
return json.loads(raw, object_pairs_hook=_unique_object), None
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
return None, self._fail(error_code, exc)
|
|
120
|
+
|
|
121
|
+
def _retry_select_policy(
|
|
122
|
+
self, manifest: str, base_policy: str, head_policy: str, groups: frozenset[GroupT]
|
|
123
|
+
) -> tuple[int, bool]:
|
|
124
|
+
try:
|
|
125
|
+
self._load_policy(
|
|
126
|
+
manifest,
|
|
127
|
+
base_policy,
|
|
128
|
+
head_policy,
|
|
129
|
+
applicable_groups=groups,
|
|
130
|
+
bootstrap=True,
|
|
131
|
+
)
|
|
132
|
+
except FileNotFoundError as exc:
|
|
133
|
+
return self._fail(self._missing_file_code(exc, base_policy, manifest, head_policy), exc), False
|
|
134
|
+
except ValueError as exc:
|
|
135
|
+
return self._fail("bootstrap_not_allowed", exc), False
|
|
136
|
+
except OSError as exc:
|
|
137
|
+
return self._fail("policy_io_failed", exc), False
|
|
138
|
+
return 0, True
|
|
139
|
+
|
|
140
|
+
def _select_policy(
|
|
141
|
+
self, manifest: str, base_policy: str, head_policy: str, groups: frozenset[GroupT], bootstrap: bool
|
|
142
|
+
) -> tuple[int, bool]:
|
|
143
|
+
try:
|
|
144
|
+
self._load_policy(
|
|
145
|
+
manifest,
|
|
146
|
+
base_policy,
|
|
147
|
+
head_policy,
|
|
148
|
+
applicable_groups=groups,
|
|
149
|
+
bootstrap=False,
|
|
150
|
+
)
|
|
151
|
+
except FileNotFoundError as exc:
|
|
152
|
+
code = self._missing_file_code(exc, base_policy, manifest, head_policy)
|
|
153
|
+
if code == "base_policy_missing" and bootstrap and not groups:
|
|
154
|
+
return self._retry_select_policy(manifest, base_policy, head_policy, groups)
|
|
155
|
+
return self._fail(code, exc), False
|
|
156
|
+
except ValueError as exc:
|
|
157
|
+
return self._fail("policy_invalid", exc), False
|
|
158
|
+
except OSError as exc:
|
|
159
|
+
return self._fail("policy_io_failed", exc), False
|
|
160
|
+
if bootstrap:
|
|
161
|
+
return self._fail(
|
|
162
|
+
"bootstrap_not_allowed", "--bootstrap is not allowed when the trusted base policy exists"
|
|
163
|
+
), False
|
|
164
|
+
return 0, False
|
|
165
|
+
|
|
166
|
+
def _select(self, args: argparse.Namespace) -> int:
|
|
167
|
+
changed_paths, status = self._decode_json(args.changed_paths_json, "changed_paths_failed")
|
|
168
|
+
if status is not None:
|
|
169
|
+
return status
|
|
170
|
+
if not isinstance(changed_paths, list) or any(not isinstance(path, str) for path in changed_paths):
|
|
171
|
+
return self._fail("changed_paths_failed", "Changed paths must be a JSON array of strings")
|
|
172
|
+
try:
|
|
173
|
+
applicable_groups, contract_required = self._select_groups(changed_paths)
|
|
174
|
+
except ValueError as exc:
|
|
175
|
+
return self._fail("changed_paths_failed", exc)
|
|
176
|
+
base_policy = os.environ.get("WINDOWS_CI_BASE_POLICY")
|
|
177
|
+
if not base_policy:
|
|
178
|
+
return self._fail("base_policy_unset", "WINDOWS_CI_BASE_POLICY must name the trusted base policy")
|
|
179
|
+
status, bootstrap_allowed = self._select_policy(
|
|
180
|
+
args.manifest, base_policy, args.head_policy, applicable_groups, args.bootstrap
|
|
181
|
+
)
|
|
182
|
+
if status:
|
|
183
|
+
return status
|
|
184
|
+
return self._emit(
|
|
185
|
+
{
|
|
186
|
+
"ok": True,
|
|
187
|
+
"product_groups": [group for group in self._product_group_order if group in applicable_groups],
|
|
188
|
+
"contract_required": contract_required,
|
|
189
|
+
"windows_required": bool(applicable_groups),
|
|
190
|
+
"bootstrap_allowed": bootstrap_allowed,
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _validate_groups(self, groups: object) -> list[GroupT] | None:
|
|
195
|
+
if not isinstance(groups, list):
|
|
196
|
+
return None
|
|
197
|
+
for group in groups:
|
|
198
|
+
if not isinstance(group, str) or group not in self._product_group_order:
|
|
199
|
+
return None
|
|
200
|
+
if len(set(groups)) != len(groups):
|
|
201
|
+
return None
|
|
202
|
+
if groups != [group for group in self._product_group_order if group in groups]:
|
|
203
|
+
return None
|
|
204
|
+
return cast(list[GroupT], groups)
|
|
205
|
+
|
|
206
|
+
def _validate_run_selection(self, selection: object) -> list[GroupT] | None:
|
|
207
|
+
keys = {"ok", "product_groups", "contract_required", "windows_required", "bootstrap_allowed"}
|
|
208
|
+
if not isinstance(selection, dict) or set(selection) != keys or selection.get("ok") is not True:
|
|
209
|
+
return None
|
|
210
|
+
groups = self._validate_groups(selection.get("product_groups"))
|
|
211
|
+
if groups is None:
|
|
212
|
+
return None
|
|
213
|
+
if any(
|
|
214
|
+
not isinstance(selection.get(key), bool)
|
|
215
|
+
for key in ("contract_required", "windows_required", "bootstrap_allowed")
|
|
216
|
+
):
|
|
217
|
+
return None
|
|
218
|
+
if selection["windows_required"] != bool(groups):
|
|
219
|
+
return None
|
|
220
|
+
if groups and selection["bootstrap_allowed"]:
|
|
221
|
+
return None
|
|
222
|
+
return groups
|
|
223
|
+
|
|
224
|
+
def _load_run_policy(
|
|
225
|
+
self, manifest: str, base_policy: str, head_policy: str, groups: list[GroupT]
|
|
226
|
+
) -> tuple[int, PolicyT | None]:
|
|
227
|
+
try:
|
|
228
|
+
policy = self._load_policy(
|
|
229
|
+
manifest,
|
|
230
|
+
base_policy,
|
|
231
|
+
head_policy,
|
|
232
|
+
applicable_groups=groups,
|
|
233
|
+
bootstrap=False,
|
|
234
|
+
)
|
|
235
|
+
except FileNotFoundError as exc:
|
|
236
|
+
return self._fail(self._missing_file_code(exc, base_policy, manifest, head_policy), exc), None
|
|
237
|
+
except ValueError as exc:
|
|
238
|
+
return self._fail("policy_invalid", exc), None
|
|
239
|
+
except OSError as exc:
|
|
240
|
+
return self._fail("policy_io_failed", exc), None
|
|
241
|
+
return 0, policy
|
|
242
|
+
|
|
243
|
+
def _execute_tests(self, args: argparse.Namespace, policy: PolicyT, groups: list[GroupT]) -> int:
|
|
244
|
+
try:
|
|
245
|
+
result = self._run_selected_tests(args.root, policy, groups, junit_path=args.junit)
|
|
246
|
+
except subprocess.CalledProcessError as exc:
|
|
247
|
+
detail = f"Test subprocess failed with exit status {exc.returncode}: {exc}"
|
|
248
|
+
if exc.output:
|
|
249
|
+
detail += f"\nstdout:\n{exc.output}"
|
|
250
|
+
if exc.stderr:
|
|
251
|
+
detail += f"\nstderr:\n{exc.stderr}"
|
|
252
|
+
return self._fail("test_process_failed", detail, exc.returncode)
|
|
253
|
+
except ValueError as exc:
|
|
254
|
+
return self._fail("test_validation_failed", exc)
|
|
255
|
+
except OSError as exc:
|
|
256
|
+
return self._fail("test_io_failed", exc)
|
|
257
|
+
return self._emit(
|
|
258
|
+
{
|
|
259
|
+
"ok": True,
|
|
260
|
+
"collected_ids": result["collected_ids"],
|
|
261
|
+
"executed_ids": result["executed_ids"],
|
|
262
|
+
"skipped": result["skipped"],
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _run(self, args: argparse.Namespace) -> int:
|
|
267
|
+
selection, status = self._decode_json(args.selection_json, "selection_failed")
|
|
268
|
+
if status is not None:
|
|
269
|
+
return status
|
|
270
|
+
groups = self._validate_run_selection(selection)
|
|
271
|
+
if groups is None:
|
|
272
|
+
return self._fail("selection_failed", "Run requires a successful select result")
|
|
273
|
+
if not groups:
|
|
274
|
+
return self._fail("no_product_groups", "No Windows product groups apply")
|
|
275
|
+
base_policy = os.environ.get("WINDOWS_CI_BASE_POLICY")
|
|
276
|
+
if not base_policy:
|
|
277
|
+
return self._fail("base_policy_unset", "WINDOWS_CI_BASE_POLICY must name the trusted base policy")
|
|
278
|
+
status, policy = self._load_run_policy(args.manifest, base_policy, args.head_policy, groups)
|
|
279
|
+
if status:
|
|
280
|
+
return status
|
|
281
|
+
return self._execute_tests(args, cast(PolicyT, policy), groups)
|
|
282
|
+
|
|
283
|
+
def _gate(self, args: argparse.Namespace) -> int:
|
|
284
|
+
selection, status = self._decode_json(args.selection_json, "selection_failed")
|
|
285
|
+
if status is not None:
|
|
286
|
+
return status
|
|
287
|
+
result = self._evaluate_gate(
|
|
288
|
+
cast(_S, selection),
|
|
289
|
+
cast(ResultT | None, args.linux_result),
|
|
290
|
+
cast(ResultT | None, args.windows_result),
|
|
291
|
+
)
|
|
292
|
+
return self._emit(result, 0 if result.get("ok") is True else 1)
|
|
293
|
+
|
|
294
|
+
def _verify_fixtures(self, args: argparse.Namespace) -> int:
|
|
295
|
+
try:
|
|
296
|
+
files_checked = verify_fixture_bytes(args.commit, Path(args.root))
|
|
297
|
+
except subprocess.CalledProcessError as exc:
|
|
298
|
+
detail = str(exc)
|
|
299
|
+
for label, output in (("stdout", exc.stdout), ("stderr", exc.stderr)):
|
|
300
|
+
if output:
|
|
301
|
+
detail += f"\n{label}:\n{os.fsdecode(output)}"
|
|
302
|
+
return self._fail("fixture_audit_failed", detail)
|
|
303
|
+
except (ValueError, FileNotFoundError, NotADirectoryError, OSError) as exc:
|
|
304
|
+
return self._fail("fixture_audit_failed", exc)
|
|
305
|
+
return self._emit({"ok": True, "files_checked": files_checked})
|
|
306
|
+
|
|
307
|
+
def run(self, argv: Sequence[str] | None) -> int:
|
|
308
|
+
class _JsonArgumentParser(argparse.ArgumentParser):
|
|
309
|
+
def error(self, message: str) -> Never:
|
|
310
|
+
self.print_usage(sys.stderr)
|
|
311
|
+
print(f"{self.prog}: error: {message}", file=sys.stderr)
|
|
312
|
+
print(json.dumps({"ok": False, "error_code": "invalid_arguments"}, separators=(",", ":")))
|
|
313
|
+
self.exit(2)
|
|
314
|
+
|
|
315
|
+
parser = _JsonArgumentParser(description="Run focused trusted Windows CI commands")
|
|
316
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
317
|
+
|
|
318
|
+
select_parser = commands.add_parser("select")
|
|
319
|
+
select_parser.add_argument("--changed-paths-json", required=True)
|
|
320
|
+
select_parser.add_argument("--manifest", required=True)
|
|
321
|
+
select_parser.add_argument("--head-policy", required=True)
|
|
322
|
+
select_parser.add_argument("--bootstrap", action="store_true")
|
|
323
|
+
|
|
324
|
+
run_parser = commands.add_parser("run")
|
|
325
|
+
run_parser.add_argument("--selection-json", required=True)
|
|
326
|
+
run_parser.add_argument("--manifest", required=True)
|
|
327
|
+
run_parser.add_argument("--head-policy", required=True)
|
|
328
|
+
run_parser.add_argument("--root", required=True)
|
|
329
|
+
run_parser.add_argument("--junit", required=True)
|
|
330
|
+
|
|
331
|
+
gate_parser = commands.add_parser("gate")
|
|
332
|
+
gate_parser.add_argument("--selection-json", required=True)
|
|
333
|
+
gate_parser.add_argument("--linux-result")
|
|
334
|
+
gate_parser.add_argument("--windows-result")
|
|
335
|
+
|
|
336
|
+
fixture_parser = commands.add_parser("verify-fixtures")
|
|
337
|
+
fixture_parser.add_argument("--commit", required=True)
|
|
338
|
+
fixture_parser.add_argument("--root", required=True)
|
|
339
|
+
|
|
340
|
+
args = parser.parse_args(argv)
|
|
341
|
+
return {
|
|
342
|
+
"select": self._select,
|
|
343
|
+
"run": self._run,
|
|
344
|
+
"gate": self._gate,
|
|
345
|
+
"verify-fixtures": self._verify_fixtures,
|
|
346
|
+
}[args.command](args)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _run_cli[
|
|
350
|
+
PolicyT,
|
|
351
|
+
GroupT: Literal["setup-check", "portability", "benchmark"],
|
|
352
|
+
ResultT: Literal["success", "failure", "cancelled", "skipped"],
|
|
353
|
+
](
|
|
354
|
+
argv: Sequence[str] | None,
|
|
355
|
+
*,
|
|
356
|
+
load_policy: _LoadPolicy[PolicyT, GroupT],
|
|
357
|
+
select_groups: _SelectGroups[GroupT],
|
|
358
|
+
run_selected_tests: _RunSelectedTests[PolicyT, GroupT],
|
|
359
|
+
evaluate_gate: _EvaluateGate[ResultT],
|
|
360
|
+
) -> int:
|
|
361
|
+
"""Dispatch through the private typed command handler."""
|
|
362
|
+
return _Dispatcher(load_policy, select_groups, run_selected_tests, evaluate_gate).run(argv)
|
|
@@ -5,13 +5,19 @@ Checks all external CLI tools that ``agentic-devtools`` depends on and
|
|
|
5
5
|
reports their availability, version, and install path in a formatted table.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
8
10
|
import re
|
|
11
|
+
import shlex
|
|
9
12
|
import shutil
|
|
10
13
|
import subprocess
|
|
11
14
|
import sys
|
|
12
15
|
from dataclasses import dataclass, field
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
13
17
|
from pathlib import Path
|
|
14
18
|
from typing import Any
|
|
19
|
+
from urllib.parse import urlsplit
|
|
20
|
+
from urllib.request import url2pathname
|
|
15
21
|
|
|
16
22
|
from ..subprocess_utils import run_safe
|
|
17
23
|
from .git_hooks_policy import is_git_hooks_management_enabled
|
|
@@ -128,23 +134,11 @@ def _find_binary(name: str) -> str | None:
|
|
|
128
134
|
return shutil.which(name)
|
|
129
135
|
|
|
130
136
|
|
|
131
|
-
def _check_dependency(
|
|
132
|
-
name: str,
|
|
133
|
-
*,
|
|
134
|
-
required: bool,
|
|
135
|
-
install_hint: str,
|
|
136
|
-
category: str,
|
|
137
|
-
) -> DependencyStatus:
|
|
137
|
+
def _check_dependency(name: str, *, required: bool, install_hint: str, category: str) -> DependencyStatus:
|
|
138
138
|
"""Check a single dependency and return its :class:`DependencyStatus`."""
|
|
139
139
|
path = _find_binary(name)
|
|
140
140
|
if not path:
|
|
141
|
-
return DependencyStatus(
|
|
142
|
-
name=name,
|
|
143
|
-
found=False,
|
|
144
|
-
required=required,
|
|
145
|
-
install_hint=install_hint,
|
|
146
|
-
category=category,
|
|
147
|
-
)
|
|
141
|
+
return DependencyStatus(name=name, found=False, required=required, install_hint=install_hint, category=category)
|
|
148
142
|
version = _get_version(name, path)
|
|
149
143
|
return DependencyStatus(
|
|
150
144
|
name=name,
|
|
@@ -162,6 +156,50 @@ def _check_dependency(
|
|
|
162
156
|
# ---------------------------------------------------------------------------
|
|
163
157
|
|
|
164
158
|
|
|
159
|
+
def _check_editable_install() -> DependencyStatus:
|
|
160
|
+
"""Report stale editable sources for this interpreter without repairing them."""
|
|
161
|
+
status = DependencyStatus(name="editable-install", found=True, category="Warning")
|
|
162
|
+
status.install_hint = "pip install -e <current-worktree-root>"
|
|
163
|
+
try:
|
|
164
|
+
cwd = Path.cwd().resolve()
|
|
165
|
+
root = next((p for p in (cwd, *cwd.parents) if (p / "pyproject.toml").is_file()), None)
|
|
166
|
+
root_path = ("<current-worktree-root>", str(root))[root is not None]
|
|
167
|
+
powershell_path = "'" + root_path.replace("'", "''") + "'"
|
|
168
|
+
quote = (shlex.quote(root_path), powershell_path)[sys.platform == "win32"]
|
|
169
|
+
status.install_hint = (f"pip install -e {root_path}", f"pip install -e {quote}")[root is not None]
|
|
170
|
+
raw = (installed_distribution := distribution("agentic-devtools")).read_text("direct_url.json")
|
|
171
|
+
if raw is None: # PathDistribution.read_text suppresses PermissionError by returning None.
|
|
172
|
+
try:
|
|
173
|
+
raw = getattr(installed_distribution, "_path").joinpath("direct_url.json").read_text(encoding="utf-8")
|
|
174
|
+
except AttributeError, FileNotFoundError, KeyError, NotADirectoryError:
|
|
175
|
+
return status
|
|
176
|
+
data = json.loads(raw)
|
|
177
|
+
if not isinstance(data, dict):
|
|
178
|
+
raise ValueError("Invalid direct_url.json object")
|
|
179
|
+
info = data.get("dir_info", {})
|
|
180
|
+
if not isinstance(info, dict) or not isinstance(info.get("editable", False), bool):
|
|
181
|
+
raise ValueError("Invalid editable metadata")
|
|
182
|
+
if not info.get("editable", False):
|
|
183
|
+
return status
|
|
184
|
+
url = data.get("url")
|
|
185
|
+
if not isinstance(url, str):
|
|
186
|
+
raise ValueError("Missing editable source URL")
|
|
187
|
+
parsed = urlsplit(url)
|
|
188
|
+
source = Path(url2pathname(url, require_scheme=True))
|
|
189
|
+
if parsed.query or parsed.fragment or not source.is_absolute():
|
|
190
|
+
raise ValueError("Invalid editable source URL")
|
|
191
|
+
source = source.resolve()
|
|
192
|
+
status.path = str(source)
|
|
193
|
+
status.found = source.is_dir() and (
|
|
194
|
+
root is None or os.path.normcase(str(source)) == os.path.normcase(str(root))
|
|
195
|
+
)
|
|
196
|
+
except PackageNotFoundError:
|
|
197
|
+
return status
|
|
198
|
+
except OSError, TypeError, ValueError, UnicodeError, RuntimeError:
|
|
199
|
+
status.found = False
|
|
200
|
+
return status
|
|
201
|
+
|
|
202
|
+
|
|
165
203
|
def _check_path_profile() -> DependencyStatus:
|
|
166
204
|
"""Check whether ``~/.agdt/bin`` is present as a PATH component.
|
|
167
205
|
|
|
@@ -281,12 +319,7 @@ def _check_git_hooks_config() -> DependencyStatus:
|
|
|
281
319
|
category=_GIT_HOOKS_OPTIONAL_CATEGORY,
|
|
282
320
|
)
|
|
283
321
|
except subprocess.TimeoutExpired:
|
|
284
|
-
return DependencyStatus(
|
|
285
|
-
name="git-hooks",
|
|
286
|
-
found=False,
|
|
287
|
-
required=False,
|
|
288
|
-
category=_GIT_HOOKS_OPTIONAL_CATEGORY,
|
|
289
|
-
)
|
|
322
|
+
return DependencyStatus(name="git-hooks", found=False, required=False, category=_GIT_HOOKS_OPTIONAL_CATEGORY)
|
|
290
323
|
|
|
291
324
|
if in_repo.returncode != 0:
|
|
292
325
|
# Not in a git repo — non-blocking
|
|
@@ -308,22 +341,12 @@ def _check_git_hooks_config() -> DependencyStatus:
|
|
|
308
341
|
)
|
|
309
342
|
except FileNotFoundError, subprocess.TimeoutExpired:
|
|
310
343
|
# git disappeared or timed out — treat as non-blocking (same as git-missing)
|
|
311
|
-
return DependencyStatus(
|
|
312
|
-
name="git-hooks",
|
|
313
|
-
found=False,
|
|
314
|
-
required=False,
|
|
315
|
-
category=_GIT_HOOKS_OPTIONAL_CATEGORY,
|
|
316
|
-
)
|
|
344
|
+
return DependencyStatus(name="git-hooks", found=False, required=False, category=_GIT_HOOKS_OPTIONAL_CATEGORY)
|
|
317
345
|
|
|
318
346
|
if toplevel.returncode != 0:
|
|
319
347
|
# show-toplevel failed (e.g. detached HEAD, bare repo, or unexpected error)
|
|
320
348
|
# — cannot validate .githooks dir; treat as non-blocking
|
|
321
|
-
return DependencyStatus(
|
|
322
|
-
name="git-hooks",
|
|
323
|
-
found=False,
|
|
324
|
-
required=False,
|
|
325
|
-
category=_GIT_HOOKS_OPTIONAL_CATEGORY,
|
|
326
|
-
)
|
|
349
|
+
return DependencyStatus(name="git-hooks", found=False, required=False, category=_GIT_HOOKS_OPTIONAL_CATEGORY)
|
|
327
350
|
|
|
328
351
|
repo_root = Path(toplevel.stdout.strip())
|
|
329
352
|
|
|
@@ -337,12 +360,7 @@ def _check_git_hooks_config() -> DependencyStatus:
|
|
|
337
360
|
)
|
|
338
361
|
except FileNotFoundError, subprocess.TimeoutExpired:
|
|
339
362
|
# git disappeared or timed out — treat as non-blocking
|
|
340
|
-
return DependencyStatus(
|
|
341
|
-
name="git-hooks",
|
|
342
|
-
found=False,
|
|
343
|
-
required=False,
|
|
344
|
-
category=_GIT_HOOKS_OPTIONAL_CATEGORY,
|
|
345
|
-
)
|
|
363
|
+
return DependencyStatus(name="git-hooks", found=False, required=False, category=_GIT_HOOKS_OPTIONAL_CATEGORY)
|
|
346
364
|
|
|
347
365
|
hooks_path_set = config_result.returncode == 0
|
|
348
366
|
current = config_result.stdout.strip() if hooks_path_set else ""
|
|
@@ -395,40 +413,21 @@ def check_all_dependencies() -> list[DependencyStatus]:
|
|
|
395
413
|
A list of :class:`DependencyStatus` objects, one per tool.
|
|
396
414
|
"""
|
|
397
415
|
results = [
|
|
398
|
-
_check_dependency(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
"git",
|
|
412
|
-
required=True,
|
|
413
|
-
install_hint="https://git-scm.com/downloads",
|
|
414
|
-
category="Required",
|
|
415
|
-
),
|
|
416
|
-
_check_dependency(
|
|
417
|
-
"az",
|
|
418
|
-
required=False,
|
|
419
|
-
install_hint="https://docs.microsoft.com/cli/azure/install-azure-cli",
|
|
420
|
-
category="Optional — needed for Azure DevOps",
|
|
421
|
-
),
|
|
422
|
-
_check_dependency(
|
|
423
|
-
"code",
|
|
424
|
-
required=False,
|
|
425
|
-
install_hint="https://code.visualstudio.com/",
|
|
426
|
-
category="Optional — needed for VS Code integration",
|
|
427
|
-
),
|
|
416
|
+
_check_dependency(name, required=required, install_hint=hint, category=category)
|
|
417
|
+
for name, required, hint, category in (
|
|
418
|
+
("copilot", False, "run: agdt-setup-copilot-cli", "Recommended"),
|
|
419
|
+
("gh", False, "run: agdt-setup-gh-cli (or https://cli.github.com/)", "Recommended"),
|
|
420
|
+
("git", True, "https://git-scm.com/downloads", "Required"),
|
|
421
|
+
(
|
|
422
|
+
"az",
|
|
423
|
+
False,
|
|
424
|
+
"https://docs.microsoft.com/cli/azure/install-azure-cli",
|
|
425
|
+
"Optional — needed for Azure DevOps",
|
|
426
|
+
),
|
|
427
|
+
("code", False, "https://code.visualstudio.com/", "Optional — needed for VS Code integration"),
|
|
428
|
+
)
|
|
428
429
|
]
|
|
429
|
-
results
|
|
430
|
-
results.append(_check_git_hooks_config())
|
|
431
|
-
return results
|
|
430
|
+
return [*results, _check_path_profile(), _check_git_hooks_config(), _check_editable_install()]
|
|
432
431
|
|
|
433
432
|
|
|
434
433
|
def print_dependency_report(statuses: list[DependencyStatus]) -> None:
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import time
|
|
15
15
|
from collections.abc import Callable
|
|
16
|
-
from dataclasses import dataclass, field
|
|
16
|
+
from dataclasses import asdict, dataclass, field
|
|
17
17
|
from dataclasses import replace as _replace
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
@@ -250,7 +250,7 @@ def run_doctor(
|
|
|
250
250
|
classified_problems: list[tuple[ErrorClass, DependencyStatus]] = []
|
|
251
251
|
repair_outcomes: list[RepairOutcome] = []
|
|
252
252
|
phases: list[PhaseResult] = []
|
|
253
|
-
extra_details: dict[str, Any] = {}
|
|
253
|
+
extra_details: dict[str, Any] = {"dependencies": [asdict(status) for status in statuses]}
|
|
254
254
|
|
|
255
255
|
# ── Phase 1: check ────────────────────────────────────────────────────────
|
|
256
256
|
check_start = time.monotonic()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
agentic_devtools/__init__.py,sha256=gvLzsZ6L3Abr4sPHr4OJ_rdZDD8cFzXacaV0LGs8dVc,539
|
|
2
|
-
agentic_devtools/_version.py,sha256=
|
|
2
|
+
agentic_devtools/_version.py,sha256=J38w9gRM5zrffQfl71XdldHwa7TCV6jmj0Fcjq-GZc8,524
|
|
3
3
|
agentic_devtools/agdt_gitignore.py,sha256=vVVqTjBZ_ijn6U3Lnh_JGrARLschYVN-fFal9jDk0Ak,2694
|
|
4
4
|
agentic_devtools/background_tasks.py,sha256=sFgmqOnCdCjclABJ8V__yDwBXDyi45b1Z-MVv3G2zB8,17664
|
|
5
5
|
agentic_devtools/config.py,sha256=DEVxTVZhsQbVQwO9qdB_1h19aBpjMzF6xJY_ePYLGLs,15629
|
|
@@ -290,7 +290,8 @@ agentic_devtools/cli/ci/suppressed_reaper.py,sha256=hitRKUr3BhxB5fCJvEDvxMU0_9c6
|
|
|
290
290
|
agentic_devtools/cli/ci/suppressed_reaper_command.py,sha256=Dl-T5haMCyaAo1fnCnLoKsJw4aRwqwLiB2v1o8XWx_8,2247
|
|
291
291
|
agentic_devtools/cli/ci/watchdog_command.py,sha256=8I_emQPPU1wx37DcJUgNz-PRl1zsKBv88pWHIFGXw3s,54494
|
|
292
292
|
agentic_devtools/cli/ci/windows_fixture_bytes.py,sha256=19rXUSdokwWel5N3ngOkdelQVv-dGjAWyZCaVHQNpWI,6188
|
|
293
|
-
agentic_devtools/cli/ci/windows_specific.py,sha256=
|
|
293
|
+
agentic_devtools/cli/ci/windows_specific.py,sha256=7xzmQp2A0wAGe3H-qmfWwqESt9E-GKA4Fu_6rAIIjms,23159
|
|
294
|
+
agentic_devtools/cli/ci/windows_specific_cli.py,sha256=g0CtGsvdN-RW4FdHO_l86KPosC532npXG3nrBy38CRM,15653
|
|
294
295
|
agentic_devtools/cli/ci/evaluator/__init__.py,sha256=59IKUIYDwZQCbLwBoMgwlr_kHYkdWOXIONnRlICnySk,1079
|
|
295
296
|
agentic_devtools/cli/ci/evaluator/actions.py,sha256=ofZpNDtBtLMH_Ka4h3JKEnhxaR1nrDGIrpzhHB1vW34,30274
|
|
296
297
|
agentic_devtools/cli/ci/evaluator/classifier.py,sha256=5o_LguIbS8jMScsIs-HPNObzeMtHdcd4X1IPOP8rrSM,5867
|
|
@@ -555,8 +556,8 @@ agentic_devtools/cli/setup/commit_template_setup.py,sha256=3xojqN7o8uF12PfnSdtaG
|
|
|
555
556
|
agentic_devtools/cli/setup/copilot_cli_installer.py,sha256=LoBWSfuHlyzrDMyUb8h5gOe77JLW5XSyaif_XE7PLXY,10040
|
|
556
557
|
agentic_devtools/cli/setup/copilot_settings.py,sha256=hLpU4MA3dHVa0Pq19bzoSsVbF8zPaTs-He4T_uDgYpM,9854
|
|
557
558
|
agentic_devtools/cli/setup/decision_log.py,sha256=KAqxWyPJLcRg2bFT2-pbx6faobo9dZktMlyuE9Qxaz4,12396
|
|
558
|
-
agentic_devtools/cli/setup/dependency_checker.py,sha256=
|
|
559
|
-
agentic_devtools/cli/setup/doctor.py,sha256=
|
|
559
|
+
agentic_devtools/cli/setup/dependency_checker.py,sha256=ytDKcq6QepYAhGt2obHriOuOrCDgO8YzsGqt2kDJj8E,17272
|
|
560
|
+
agentic_devtools/cli/setup/doctor.py,sha256=z1vEDQKI4jSMu2pYLObAxiZv4c5-5SnTNWCf4ipsVJc,18454
|
|
560
561
|
agentic_devtools/cli/setup/doctor_repair.py,sha256=JmlQZi6dIqke7Rzq19EfIgkbtYbE-a0DdqLYg1iyP4A,6230
|
|
561
562
|
agentic_devtools/cli/setup/doctor_repairs.py,sha256=_iuruXUB9vrrhP2YcRz-dZfpIKEqWoyQ-WxtQ11YD9M,9809
|
|
562
563
|
agentic_devtools/cli/setup/exit_codes.py,sha256=56ej8g1n8K5cmsuyE0eEwoITqkJLWONRsNI6DfunuVQ,2553
|
|
@@ -1305,8 +1306,8 @@ agentic_devtools/_bundled_skills/skills/speckit-plan/SKILL.md,sha256=qYLpzgLCIZ6
|
|
|
1305
1306
|
agentic_devtools/_bundled_skills/skills/speckit-specify/SKILL.md,sha256=HbHRbdphMqbtNw6fL6NcYCj6sSFmUyk2S4U0bZ9p_l4,13681
|
|
1306
1307
|
agentic_devtools/_bundled_skills/skills/write-github-commit-message/SKILL.md,sha256=Ep1lR8NDN117Iytj172VZgqazFiLpJ56Z7RXgaTnxR8,2626
|
|
1307
1308
|
agentic_devtools/_bundled_skills/skills/write-github-commit-message/commit-types.md,sha256=bTy_t68ZD6R87rFIzq9wybTu8Wudoahb7nB9IWC4bFo,1839
|
|
1308
|
-
agentic_devtools-0.2.
|
|
1309
|
-
agentic_devtools-0.2.
|
|
1310
|
-
agentic_devtools-0.2.
|
|
1311
|
-
agentic_devtools-0.2.
|
|
1312
|
-
agentic_devtools-0.2.
|
|
1309
|
+
agentic_devtools-0.2.598.dist-info/METADATA,sha256=NQlxxZMvekaRdXpTDU1tKYAqPvYURKqbkk3qUDhZDXE,35589
|
|
1310
|
+
agentic_devtools-0.2.598.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
1311
|
+
agentic_devtools-0.2.598.dist-info/entry_points.txt,sha256=kL35_Y5OHI6XpPneeBTcX82ORjjfx2VIK6XfmKHgNm4,13630
|
|
1312
|
+
agentic_devtools-0.2.598.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
|
|
1313
|
+
agentic_devtools-0.2.598.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|