agentic-devtools 0.2.575__py3-none-any.whl → 0.2.577__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.
@@ -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.575'
22
- __version_tuple__ = version_tuple = (0, 2, 575)
21
+ __version__ = version = '0.2.577'
22
+ __version_tuple__ = version_tuple = (0, 2, 577)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -44,6 +44,17 @@ _MODULE_SPECS = {
44
44
  "tasks": _ModuleTestSpec(("tests/unit/cli/tasks",), (), ("agentic_devtools/cli/tasks",)),
45
45
  "github_module": _ModuleTestSpec(("tests/unit/cli/github",), (), ("agentic_devtools/cli/github",)),
46
46
  "copilot": _ModuleTestSpec(("tests/unit/cli/copilot",), (), ("agentic_devtools/cli/copilot",)),
47
+ "ci": _ModuleTestSpec(
48
+ (
49
+ "tests/unit/cli/ci/critique_adjudicator/test_critique_adjudicator.py",
50
+ "tests/unit/cli/ci/pipeline/critique_gate/test_critique_gate_exception_passed.py",
51
+ ),
52
+ (),
53
+ (
54
+ "agentic_devtools.cli.ci.critique_adjudicator",
55
+ "agentic_devtools.cli.ci.pipeline.critique_gate",
56
+ ),
57
+ ),
47
58
  "speckit": _ModuleTestSpec(("tests/unit/cli/speckit",), (), ("agentic_devtools/cli/speckit",)),
48
59
  "azure": _ModuleTestSpec(("tests/unit/cli/azure",), (), ("agentic_devtools/cli/azure",)),
49
60
  "issue_template": _ModuleTestSpec(("tests/unit/cli/issue_template",), (), ("agentic_devtools/cli/issue_template",)),
@@ -0,0 +1,308 @@
1
+ """Versioned reconciliation claims, not authentication or a merge authorization.
2
+
3
+ All identities, digests, inventories, and verdicts here are unverified payload
4
+ data. Provider verification, original-criteria digest verification, live-head
5
+ binding, and every independent pipeline gate remain the caller's responsibility.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import binascii
12
+ import hashlib
13
+ import json
14
+ import re
15
+ from dataclasses import asdict, dataclass, fields
16
+ from typing import Any, Literal
17
+
18
+ __all__ = [
19
+ "ReconciliationAttestation",
20
+ "ReconciliationCriterionVerdict",
21
+ "ReconciliationFileVerdict",
22
+ "ReconciliationValidationError",
23
+ "validate_reconciliation_attestation",
24
+ "encode_reconciliation_review_body",
25
+ "decode_reconciliation_review_body",
26
+ ]
27
+
28
+ _LOGIN = r"[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}"
29
+ _CRITERIA_SOURCE = "https://github.com/swai-factory/agentic-devtools/issues/4299#acceptance-and-safety-tests"
30
+ _CRITERION_IDS = ("AC1", "AC2", "AC3", "AC4", "AC5", "AC6", "AC7")
31
+ _MAX_BODY_LENGTH = 58_000
32
+ _FOOTER = "<!-- /agdt-reconciliation -->"
33
+ _Verdict = Literal["accepted", "rejected", "unresolved"]
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class ReconciliationFileVerdict:
38
+ """Claim about raw file bytes and intended behavior at the reviewed head."""
39
+
40
+ path: str
41
+ content_sha256: str
42
+ verdict: _Verdict
43
+ behavior: str
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ReconciliationCriterionVerdict:
48
+ """Claim about one original parent acceptance criterion."""
49
+
50
+ criterion_id: str
51
+ verdict: _Verdict
52
+ rationale: str
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class ReconciliationAttestation:
57
+ """Immutable schema-v1 claims with separate repair-output and reviewed heads."""
58
+
59
+ schema_version: int
60
+ repository: str
61
+ pr_number: int
62
+ pre_repair_sha: str
63
+ pinned_base_sha: str
64
+ repair_output_sha: str
65
+ reviewed_head_sha: str
66
+ changed_files: tuple[str, ...]
67
+ file_verdicts: tuple[ReconciliationFileVerdict, ...]
68
+ criteria_source: str
69
+ criteria_source_sha256: str
70
+ criterion_verdicts: tuple[ReconciliationCriterionVerdict, ...]
71
+ provider_evidence_ref: str
72
+ configured_approver_login: str
73
+ reviewer_login: str
74
+ repair_writer_login: str
75
+
76
+
77
+ class ReconciliationValidationError(ValueError):
78
+ """Contract failure with a stable machine-readable reason."""
79
+
80
+ def __init__(self, reason: str) -> None:
81
+ self.reason = reason
82
+ super().__init__(reason)
83
+
84
+
85
+ def _require_pattern(value: str, pattern: str, reason: str) -> None:
86
+ """Require a string matching the entire ASCII contract pattern."""
87
+ if not isinstance(value, str) or re.fullmatch(pattern, value) is None:
88
+ raise ReconciliationValidationError(reason)
89
+
90
+
91
+ def _validate_path(path: str) -> None:
92
+ """Validate a repository-relative Git path without normalization."""
93
+ if (
94
+ not isinstance(path, str)
95
+ or not path
96
+ or path.startswith("/")
97
+ or re.match(r"[A-Za-z]:/", path) is not None
98
+ or "\\" in path
99
+ or "\0" in path
100
+ or any(segment in ("", ".", "..") for segment in path.split("/"))
101
+ ):
102
+ raise ReconciliationValidationError("invalid_file_path")
103
+
104
+
105
+ def validate_reconciliation_attestation(attestation: ReconciliationAttestation) -> None:
106
+ """Validate payload structure and consistency only; never authorize a repair."""
107
+ if type(attestation) is not ReconciliationAttestation:
108
+ raise ReconciliationValidationError("invalid_record_type")
109
+ if type(attestation.schema_version) is not int or attestation.schema_version != 1:
110
+ raise ReconciliationValidationError("unsupported_schema_version")
111
+ _require_pattern(attestation.repository, rf"{_LOGIN}/[a-z0-9._-]{{1,100}}", "invalid_repository")
112
+ if type(attestation.pr_number) is not int or attestation.pr_number <= 0:
113
+ raise ReconciliationValidationError("invalid_pr_number")
114
+ for sha in (
115
+ attestation.pre_repair_sha,
116
+ attestation.pinned_base_sha,
117
+ attestation.repair_output_sha,
118
+ attestation.reviewed_head_sha,
119
+ ):
120
+ _require_pattern(sha, r"[0-9a-f]{40}", "invalid_commit_sha")
121
+ _require_pattern(attestation.criteria_source_sha256, r"[0-9a-f]{64}", "invalid_sha256")
122
+ if attestation.criteria_source != _CRITERIA_SOURCE:
123
+ raise ReconciliationValidationError("criterion_coverage_mismatch")
124
+ _validate_identities(attestation)
125
+ for collection in (attestation.changed_files, attestation.file_verdicts, attestation.criterion_verdicts):
126
+ if type(collection) is not tuple:
127
+ raise ReconciliationValidationError("invalid_collection_type")
128
+ _validate_files(attestation)
129
+ _validate_criteria(attestation.criterion_verdicts)
130
+
131
+
132
+ def _validate_identities(attestation: ReconciliationAttestation) -> None:
133
+ """Check claimed login and locator consistency, not provider identity."""
134
+ for login in (
135
+ attestation.configured_approver_login,
136
+ attestation.reviewer_login,
137
+ attestation.repair_writer_login,
138
+ ):
139
+ _require_pattern(login, _LOGIN, "invalid_login")
140
+ if attestation.reviewer_login != attestation.configured_approver_login:
141
+ raise ReconciliationValidationError("reviewer_not_configured_approver")
142
+ if attestation.reviewer_login == attestation.repair_writer_login:
143
+ raise ReconciliationValidationError("reviewer_is_repair_writer")
144
+ expected_ref = f"github://{attestation.repository}/pull/{attestation.pr_number}/reviews"
145
+ if attestation.provider_evidence_ref != expected_ref:
146
+ raise ReconciliationValidationError("invalid_provider_evidence_ref")
147
+
148
+
149
+ def _require_inventory(values: tuple[str, ...], duplicate_reason: str) -> None:
150
+ """Require unique identifiers in lexicographic order."""
151
+ if len(set(values)) != len(values):
152
+ raise ReconciliationValidationError(duplicate_reason)
153
+ if tuple(sorted(values)) != values:
154
+ raise ReconciliationValidationError("noncanonical_order")
155
+
156
+
157
+ def _validate_files(attestation: ReconciliationAttestation) -> None:
158
+ """Require exact, accepted, behavior-described coverage of the claimed inventory."""
159
+ if not attestation.changed_files:
160
+ raise ReconciliationValidationError("file_inventory_empty")
161
+ for path in attestation.changed_files:
162
+ _validate_path(path)
163
+ _require_inventory(attestation.changed_files, "duplicate_file")
164
+ for file_record in attestation.file_verdicts:
165
+ _validate_file_record(file_record)
166
+ file_paths = tuple(record.path for record in attestation.file_verdicts)
167
+ _require_inventory(file_paths, "duplicate_file")
168
+ if file_paths != attestation.changed_files:
169
+ raise ReconciliationValidationError("file_verdict_coverage_mismatch")
170
+
171
+
172
+ def _validate_file_record(file_record: ReconciliationFileVerdict) -> None:
173
+ """Require a structurally valid accepted file claim."""
174
+ if type(file_record) is not ReconciliationFileVerdict:
175
+ raise ReconciliationValidationError("invalid_record_type")
176
+ _validate_path(file_record.path)
177
+ _require_pattern(file_record.content_sha256, r"[0-9a-f]{64}", "invalid_sha256")
178
+ if file_record.verdict not in ("accepted", "rejected", "unresolved"):
179
+ raise ReconciliationValidationError("invalid_file_verdict")
180
+ if file_record.verdict != "accepted":
181
+ raise ReconciliationValidationError(f"file_verdict_{file_record.verdict}")
182
+ if not isinstance(file_record.behavior, str) or not file_record.behavior.strip():
183
+ raise ReconciliationValidationError("empty_file_behavior")
184
+
185
+
186
+ def _validate_criteria(records: tuple[ReconciliationCriterionVerdict, ...]) -> None:
187
+ """Require accepted claims covering all seven original criterion IDs."""
188
+ for criterion_record in records:
189
+ if type(criterion_record) is not ReconciliationCriterionVerdict:
190
+ raise ReconciliationValidationError("invalid_record_type")
191
+ if criterion_record.criterion_id not in _CRITERION_IDS:
192
+ raise ReconciliationValidationError("criterion_coverage_mismatch")
193
+ if criterion_record.verdict not in ("accepted", "rejected", "unresolved"):
194
+ raise ReconciliationValidationError("invalid_criterion_verdict")
195
+ if criterion_record.verdict != "accepted":
196
+ raise ReconciliationValidationError(f"criterion_verdict_{criterion_record.verdict}")
197
+ if not isinstance(criterion_record.rationale, str) or not criterion_record.rationale.strip():
198
+ raise ReconciliationValidationError("empty_criterion_rationale")
199
+ criterion_ids = tuple(record.criterion_id for record in records)
200
+ _require_inventory(criterion_ids, "duplicate_criterion")
201
+ if criterion_ids != _CRITERION_IDS:
202
+ raise ReconciliationValidationError("criterion_coverage_mismatch")
203
+
204
+
205
+ def _canonical_json(payload: Any) -> bytes:
206
+ """Serialize canonical ASCII JSON as UTF-8 bytes."""
207
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False).encode(
208
+ "utf-8"
209
+ )
210
+
211
+
212
+ def encode_reconciliation_review_body(attestation: ReconciliationAttestation) -> str:
213
+ """Encode a complete claim, enforcing the envelope size before field validation."""
214
+ if type(attestation) is not ReconciliationAttestation:
215
+ raise ReconciliationValidationError("invalid_record_type")
216
+ try:
217
+ payload = _canonical_json(asdict(attestation))
218
+ except TypeError, ValueError, RecursionError:
219
+ # Non-JSON caller values have no encodable body to size. Validation
220
+ # supplies their precise reason without coercing the original records.
221
+ validate_reconciliation_attestation(attestation)
222
+ raise ReconciliationValidationError("invalid_record_type") from None
223
+ encoded = base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=")
224
+ digest = hashlib.sha256(payload).hexdigest()
225
+ body = f"<!-- agdt-reconciliation:v1 sha256={digest} -->\n{encoded}\n{_FOOTER}"
226
+ if len(body) > _MAX_BODY_LENGTH:
227
+ raise ReconciliationValidationError("review_body_too_large")
228
+ validate_reconciliation_attestation(attestation)
229
+ return body
230
+
231
+
232
+ def _json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
233
+ """Reject duplicate keys at every JSON object depth."""
234
+ result: dict[str, Any] = {}
235
+ for key, value in pairs:
236
+ if key in result:
237
+ raise ReconciliationValidationError("invalid_json")
238
+ result[key] = value
239
+ return result
240
+
241
+
242
+ def _record_from_json[Record: ReconciliationAttestation | ReconciliationFileVerdict | ReconciliationCriterionVerdict](
243
+ record_type: type[Record], payload: Any
244
+ ) -> Record:
245
+ """Require the exact key set before constructing a frozen record."""
246
+ if type(payload) is not dict:
247
+ raise ReconciliationValidationError("invalid_record_type")
248
+ expected = {field.name for field in fields(record_type)}
249
+ if payload.keys() - expected:
250
+ raise ReconciliationValidationError("unknown_field")
251
+ if expected - payload.keys():
252
+ raise ReconciliationValidationError("missing_field")
253
+ return record_type(**payload)
254
+
255
+
256
+ def _decode_base64url(encoded: str) -> bytes:
257
+ """Decode strictly unpadded, canonical RFC 4648 URL-safe Base64."""
258
+ if re.fullmatch(r"[A-Za-z0-9_-]+", encoded) is None:
259
+ raise ReconciliationValidationError("invalid_base64url")
260
+ try:
261
+ payload = base64.b64decode(encoded + "=" * (-len(encoded) % 4), altchars=b"-_", validate=True)
262
+ except binascii.Error as exc:
263
+ raise ReconciliationValidationError("invalid_base64url") from exc
264
+ if base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=") != encoded:
265
+ raise ReconciliationValidationError("invalid_base64url")
266
+ return payload
267
+
268
+
269
+ def decode_reconciliation_review_body(body: str) -> ReconciliationAttestation:
270
+ """Decode only one exact canonical block; do not authenticate its claims."""
271
+ if not isinstance(body, str):
272
+ raise ReconciliationValidationError("malformed_metadata")
273
+ if len(body) > _MAX_BODY_LENGTH:
274
+ raise ReconciliationValidationError("review_body_too_large")
275
+ lines = body.split("\n")
276
+ if len(lines) != 3:
277
+ raise ReconciliationValidationError("malformed_metadata")
278
+ header = re.fullmatch(r"<!-- agdt-reconciliation:v1 sha256=([0-9a-f]{64}) -->", lines[0])
279
+ if header is None or lines[2] != _FOOTER:
280
+ raise ReconciliationValidationError("malformed_metadata")
281
+ payload = _decode_base64url(lines[1])
282
+ if hashlib.sha256(payload).hexdigest() != header[1]:
283
+ raise ReconciliationValidationError("payload_digest_mismatch")
284
+ try:
285
+ data = json.loads(payload.decode("utf-8"), object_pairs_hook=_json_object)
286
+ canonical = _canonical_json(data)
287
+ except (ValueError, RecursionError) as exc:
288
+ raise ReconciliationValidationError("invalid_json") from exc
289
+ if canonical != payload:
290
+ raise ReconciliationValidationError("noncanonical_payload")
291
+ _record_from_json(ReconciliationAttestation, data)
292
+ record_types: tuple[
293
+ tuple[str, type[ReconciliationFileVerdict] | type[ReconciliationCriterionVerdict] | None], ...
294
+ ] = (
295
+ ("changed_files", None),
296
+ ("file_verdicts", ReconciliationFileVerdict),
297
+ ("criterion_verdicts", ReconciliationCriterionVerdict),
298
+ )
299
+ for key, record_type in record_types:
300
+ collection = data[key]
301
+ if type(collection) is not list:
302
+ raise ReconciliationValidationError("invalid_collection_type")
303
+ data[key] = tuple(
304
+ _record_from_json(record_type, record) if record_type is not None else record for record in collection
305
+ )
306
+ attestation = _record_from_json(ReconciliationAttestation, data)
307
+ validate_reconciliation_attestation(attestation)
308
+ return attestation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-devtools
3
- Version: 0.2.575
3
+ Version: 0.2.577
4
4
  Summary: Agentic devtools integrate Jira, DevOps & more
5
5
  Author: ayaiayorg
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  agentic_devtools/__init__.py,sha256=J_Zw_vWKghk-cLmqI83hXQmSiS8zMhGIHM5WPLDkZuo,242
2
- agentic_devtools/_version.py,sha256=Kwc4VXVEzGORnf7xChq1gdA9uZ6IF4gqkLChshs24TA,524
2
+ agentic_devtools/_version.py,sha256=TIWdW15l89P8z-XtWYdluJmDXV7ryxUz-CBFY28AsZc,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
@@ -220,7 +220,7 @@ agentic_devtools/cli/checks/quality_baseline.py,sha256=PbSYAiJdWwEDP8SGGUXNpJVxk
220
220
  agentic_devtools/cli/checks/setup_drift.py,sha256=bVR0F4wpwt8rOl829tGXKqI1PDUbg9JsXTShgYaJSSM,7666
221
221
  agentic_devtools/cli/checks/setup_drift_changed_files.py,sha256=73IuzquyLDKhWderCvFE5TXnHuBxBcQTSbg2p15-wKE,3719
222
222
  agentic_devtools/cli/checks/skill_classification.py,sha256=vZGv2IjT-uEMEDguWhdZPyTLB2QD4rTVnATS7Agrjrc,8521
223
- agentic_devtools/cli/checks/smart_module_tests.py,sha256=HxMM1VAP44N4IsNy1ELOaFtuJ2-ryqahU3jBRjhGOJ4,7675
223
+ agentic_devtools/cli/checks/smart_module_tests.py,sha256=wYr_o9ojqQStEA8VfFS0YbBYrwsGNHtfEgoPk2067dk,8063
224
224
  agentic_devtools/cli/checks/structure.py,sha256=b6Z_eXoV4D6WU2EDaFBDRTHmHzHYos1dv5pNLTbs80E,1771
225
225
  agentic_devtools/cli/checks/tests.py,sha256=ZJoMkTdbo3H2_ojadG3nEOw4xUmUciER4N_2mO_5sUg,5237
226
226
  agentic_devtools/cli/checks/xdist_policy.py,sha256=JSb3HijPjpiSV0Gh7PLX4nWc0Bvb2Bi5rAl4Ww8l0NY,1156
@@ -299,6 +299,7 @@ agentic_devtools/cli/ci/evaluator/snapshot_markdown_blocks.py,sha256=0myRRK4CpYP
299
299
  agentic_devtools/cli/ci/pipeline/__init__.py,sha256=kVtyTqirPi_pXeEPpa3nYqW15ILPiAVpGGWML3CEl3s,755
300
300
  agentic_devtools/cli/ci/pipeline/base.py,sha256=KanjtOEcz6myQPoNqNYO5bsbHL5LPhm-PFnL8yT7Q2M,1268
301
301
  agentic_devtools/cli/ci/pipeline/command.py,sha256=f4KfVWkKheIb4xaUbyO9dF1idy5wLR-qPtZHEXy1MQg,12428
302
+ agentic_devtools/cli/ci/pipeline/conflict_reconciliation.py,sha256=H_O67JPHeqByOfsDvKXg9bsN2CIYhMcJpO_xIduEDto,13662
302
303
  agentic_devtools/cli/ci/pipeline/critique_gate.py,sha256=OGaaTy_21pbPBvJSNyjTXvxWTAWYLbCSmrKARc4JYaw,3763
303
304
  agentic_devtools/cli/ci/pipeline/critique_snapshot.py,sha256=_lJyfCLqeEw9WDMV7qQsYZljgfdDhalhoGtG9Lw0Hvo,9203
304
305
  agentic_devtools/cli/ci/pipeline/deferral.py,sha256=M0WSLAJol9xLe1RuWNKiuRAw3EqU8VULDJqUSwM0HCw,19426
@@ -1288,8 +1289,8 @@ agentic_devtools/_bundled_skills/skills/speckit-plan/SKILL.md,sha256=qYLpzgLCIZ6
1288
1289
  agentic_devtools/_bundled_skills/skills/speckit-specify/SKILL.md,sha256=HbHRbdphMqbtNw6fL6NcYCj6sSFmUyk2S4U0bZ9p_l4,13681
1289
1290
  agentic_devtools/_bundled_skills/skills/write-github-commit-message/SKILL.md,sha256=Ep1lR8NDN117Iytj172VZgqazFiLpJ56Z7RXgaTnxR8,2626
1290
1291
  agentic_devtools/_bundled_skills/skills/write-github-commit-message/commit-types.md,sha256=bTy_t68ZD6R87rFIzq9wybTu8Wudoahb7nB9IWC4bFo,1839
1291
- agentic_devtools-0.2.575.dist-info/METADATA,sha256=HividaPOs7GGox5SsbY7PViXXa8lQCKtzLL3wRzKuOA,35393
1292
- agentic_devtools-0.2.575.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
1293
- agentic_devtools-0.2.575.dist-info/entry_points.txt,sha256=kL35_Y5OHI6XpPneeBTcX82ORjjfx2VIK6XfmKHgNm4,13630
1294
- agentic_devtools-0.2.575.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
1295
- agentic_devtools-0.2.575.dist-info/RECORD,,
1292
+ agentic_devtools-0.2.577.dist-info/METADATA,sha256=UDLYuI2Tr9N_i5CYmG3b1TL9uHo8cntMfnpr8BWPO1E,35393
1293
+ agentic_devtools-0.2.577.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
1294
+ agentic_devtools-0.2.577.dist-info/entry_points.txt,sha256=kL35_Y5OHI6XpPneeBTcX82ORjjfx2VIK6XfmKHgNm4,13630
1295
+ agentic_devtools-0.2.577.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
1296
+ agentic_devtools-0.2.577.dist-info/RECORD,,