agentic-devtools 0.2.331__py3-none-any.whl → 0.2.332__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/github_provider.py +12 -65
- agentic_devtools/cli/ci/pipeline/actions/dispatch_repair.py +42 -2
- agentic_devtools/cli/ci/pipeline/gate_verdict.py +33 -39
- agentic_devtools/cli/github/ccr_review_format.py +279 -0
- agentic_devtools/cli/github/copilot_review_status.py +55 -5
- {agentic_devtools-0.2.331.dist-info → agentic_devtools-0.2.332.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.331.dist-info → agentic_devtools-0.2.332.dist-info}/RECORD +11 -10
- {agentic_devtools-0.2.331.dist-info → agentic_devtools-0.2.332.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.331.dist-info → agentic_devtools-0.2.332.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.331.dist-info → agentic_devtools-0.2.332.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.332'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 332)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -59,6 +59,7 @@ from agentic_devtools.cli.ci.resolution.tiers.outdated import OutdatedTier
|
|
|
59
59
|
from agentic_devtools.cli.ci.resolution.tiers.sdk_evaluation import SdkEvaluationTier
|
|
60
60
|
from agentic_devtools.cli.ci.retry import RetryableError, retry_with_backoff
|
|
61
61
|
from agentic_devtools.cli.git.commit_template import _derive_issue_link_from_key, resolve_commit_message_from_template
|
|
62
|
+
from agentic_devtools.cli.github.ccr_review_format import extract_suppressed_comment_entries
|
|
62
63
|
from agentic_devtools.cli.subprocess_utils import run_safe
|
|
63
64
|
from agentic_devtools.config import load_platform_config
|
|
64
65
|
from agentic_devtools.state import get_state_dir
|
|
@@ -272,35 +273,19 @@ query($owner: String!, $name: String!, $endCursor: String!) {
|
|
|
272
273
|
}
|
|
273
274
|
"""
|
|
274
275
|
|
|
275
|
-
# Regex to extract the <details> block containing suppressed comments.
|
|
276
|
-
# Uses DOTALL so `.*?` spans newlines.
|
|
277
|
-
_SUPPRESSED_DETAILS_RE = re.compile(
|
|
278
|
-
r"<details>\s*<summary>[^<]*suppressed due to low confidence[^<]*</summary>(.*?)</details>",
|
|
279
|
-
re.DOTALL | re.IGNORECASE,
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# Regex to extract individual entries within the suppressed block.
|
|
283
|
-
# Each entry starts with a bold or code-formatted file path, followed by body text.
|
|
284
|
-
# Matches patterns like:
|
|
285
|
-
# **path/to/file.py**: body text
|
|
286
|
-
# `path/to/file.py`: body text
|
|
287
|
-
# **`path/to/file.py`**: body text
|
|
288
|
-
_SUPPRESSED_ENTRY_RE = re.compile(
|
|
289
|
-
r"(?:\*\*`?|`)" # opening bold/code marker
|
|
290
|
-
r"([^`*\n]+?)" # file path (captured)
|
|
291
|
-
r"(?:`?\*\*|`)" # closing bold/code marker
|
|
292
|
-
r"\s*:?\s*" # optional colon separator
|
|
293
|
-
r"(.*?)(?=\n(?:\*\*`?|`)[^`*\n]+?(?:`?\*\*|`)\s*:?|\Z)", # body (captured, up to next entry or end)
|
|
294
|
-
re.DOTALL,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
276
|
|
|
277
|
+
# Suppressed-comment recovery from a Copilot review body.
|
|
298
278
|
def _parse_suppressed_from_review_body(review_body: str) -> list[ReviewCommentInfo]:
|
|
299
|
-
"""Parse suppressed comments from a Copilot review body
|
|
279
|
+
"""Parse suppressed comments from a Copilot review body.
|
|
300
280
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
281
|
+
Delegates block/entry extraction to the shared CCR parser
|
|
282
|
+
(:func:`agentic_devtools.cli.github.ccr_review_format.extract_suppressed_comment_entries`),
|
|
283
|
+
which understands **both** the legacy ``<details><summary>… suppressed …</summary>``
|
|
284
|
+
format and the new CCR private-preview format (a
|
|
285
|
+
``### Comments suppressed due to low confidence (N)`` heading inside a
|
|
286
|
+
``<details><summary>Review details</summary>`` block). Each recovered
|
|
287
|
+
``(path, body)`` pair is wrapped as a ``ReviewCommentInfo`` with
|
|
288
|
+
``is_suppressed=True`` and a unique negative sentinel ID.
|
|
304
289
|
|
|
305
290
|
Args:
|
|
306
291
|
review_body: Full review body text (may contain HTML).
|
|
@@ -309,29 +294,9 @@ def _parse_suppressed_from_review_body(review_body: str) -> list[ReviewCommentIn
|
|
|
309
294
|
List of ``ReviewCommentInfo`` with ``is_suppressed=True`` and negative
|
|
310
295
|
sentinel IDs. Returns empty list if no suppressed block is found.
|
|
311
296
|
"""
|
|
312
|
-
if not review_body:
|
|
313
|
-
return []
|
|
314
|
-
|
|
315
|
-
match = _SUPPRESSED_DETAILS_RE.search(review_body)
|
|
316
|
-
if not match:
|
|
317
|
-
return []
|
|
318
|
-
|
|
319
|
-
block_content = match.group(1).strip()
|
|
320
|
-
if not block_content:
|
|
321
|
-
return []
|
|
322
|
-
|
|
323
297
|
entries: list[ReviewCommentInfo] = []
|
|
324
298
|
sentinel_id = -1
|
|
325
|
-
|
|
326
|
-
for entry_match in _SUPPRESSED_ENTRY_RE.finditer(block_content):
|
|
327
|
-
path = entry_match.group(1).strip()
|
|
328
|
-
body = entry_match.group(2).strip()
|
|
329
|
-
|
|
330
|
-
if not path:
|
|
331
|
-
path = "(unknown file)"
|
|
332
|
-
if not body:
|
|
333
|
-
continue
|
|
334
|
-
|
|
299
|
+
for path, body in extract_suppressed_comment_entries(review_body):
|
|
335
300
|
entries.append(
|
|
336
301
|
ReviewCommentInfo(
|
|
337
302
|
id=sentinel_id,
|
|
@@ -343,24 +308,6 @@ def _parse_suppressed_from_review_body(review_body: str) -> list[ReviewCommentIn
|
|
|
343
308
|
)
|
|
344
309
|
sentinel_id -= 1
|
|
345
310
|
|
|
346
|
-
# If we found a details block but no structured entries, try a line-based
|
|
347
|
-
# fallback: treat each non-empty line as a standalone comment.
|
|
348
|
-
if not entries:
|
|
349
|
-
for line in block_content.splitlines():
|
|
350
|
-
line = line.strip()
|
|
351
|
-
if not line:
|
|
352
|
-
continue
|
|
353
|
-
entries.append(
|
|
354
|
-
ReviewCommentInfo(
|
|
355
|
-
id=sentinel_id,
|
|
356
|
-
path="(unknown file)",
|
|
357
|
-
body=line,
|
|
358
|
-
html_url="",
|
|
359
|
-
is_suppressed=True,
|
|
360
|
-
)
|
|
361
|
-
)
|
|
362
|
-
sentinel_id -= 1
|
|
363
|
-
|
|
364
311
|
return entries
|
|
365
312
|
|
|
366
313
|
|
|
@@ -11,18 +11,48 @@ from agentic_devtools.cli.ci.guards import (
|
|
|
11
11
|
)
|
|
12
12
|
from agentic_devtools.cli.ci.models import COPILOT_LOGINS, ReviewInfo
|
|
13
13
|
from agentic_devtools.cli.ci.pipeline.exclusion import ExclusionContext
|
|
14
|
+
from agentic_devtools.cli.ci.pipeline.gate_verdict import (
|
|
15
|
+
REASON_HAS_COMMENTS,
|
|
16
|
+
REASON_NEW_CCR_NOT_APPROVED,
|
|
17
|
+
REASON_SUPPRESSED_COMMENTS,
|
|
18
|
+
)
|
|
14
19
|
from agentic_devtools.cli.ci.pipeline.models import ActionDecision, ActionResult
|
|
15
20
|
from agentic_devtools.cli.ci.pipeline.snapshot import DerivedState, PRStateSnapshot
|
|
16
21
|
from agentic_devtools.cli.ci.provider import CIPlatformProvider
|
|
17
22
|
|
|
18
23
|
logger = logging.getLogger(__name__)
|
|
19
24
|
|
|
25
|
+
#: Gate-verdict reasons that mean the HEAD Copilot review carries actionable
|
|
26
|
+
#: feedback in its *body* (posted comments, suppressed/low-confidence comments,
|
|
27
|
+
#: or a new-CCR-format "Not ready to approve" verdict). Freshness reasons
|
|
28
|
+
#: (awaiting-fresh, content-changed) are deliberately excluded — they call for a
|
|
29
|
+
#: *new* review, not a repair dispatch.
|
|
30
|
+
_CONTENT_BLOCKING_GATE_REASONS: frozenset[str] = frozenset(
|
|
31
|
+
{
|
|
32
|
+
REASON_HAS_COMMENTS,
|
|
33
|
+
REASON_SUPPRESSED_COMMENTS,
|
|
34
|
+
REASON_NEW_CCR_NOT_APPROVED,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
20
38
|
|
|
21
39
|
def _is_copilot_review_actionable(snapshot: PRStateSnapshot) -> bool:
|
|
22
40
|
"""Return True if the Copilot review on HEAD is actionable.
|
|
23
41
|
|
|
24
|
-
Actionable means
|
|
25
|
-
|
|
42
|
+
Actionable means:
|
|
43
|
+
|
|
44
|
+
* ``CHANGES_REQUESTED`` on HEAD, or
|
|
45
|
+
* ``COMMENTED`` on HEAD with inline comments (unknown inline counts fail
|
|
46
|
+
closed and are treated as actionable), or
|
|
47
|
+
* ``COMMENTED`` on HEAD with **zero** counted inline comments but a
|
|
48
|
+
content-blocking gate verdict for that same review.
|
|
49
|
+
|
|
50
|
+
The third branch closes a new-CCR-format gap: the private-preview format can
|
|
51
|
+
block the gate purely from the review body (a "Not ready to approve" verdict
|
|
52
|
+
heading, or body-only suppressed comments) while exposing no inline comments
|
|
53
|
+
through the API. Without it, such a PR is blocked by the gate yet never
|
|
54
|
+
triggers repair — a permanent stall. Freshness reasons are excluded because
|
|
55
|
+
they require a *new* review, not a repair.
|
|
26
56
|
"""
|
|
27
57
|
if snapshot.review_state == "CHANGES_REQUESTED" and snapshot.copilot_review_id > 0:
|
|
28
58
|
return True
|
|
@@ -32,6 +62,16 @@ def _is_copilot_review_actionable(snapshot: PRStateSnapshot) -> bool:
|
|
|
32
62
|
and snapshot.copilot_review_inline_count != 0
|
|
33
63
|
):
|
|
34
64
|
return True
|
|
65
|
+
verdict = snapshot.copilot_gate_verdict
|
|
66
|
+
if (
|
|
67
|
+
snapshot.review_state == "COMMENTED"
|
|
68
|
+
and snapshot.copilot_review_id > 0
|
|
69
|
+
and verdict is not None
|
|
70
|
+
and not verdict.passed
|
|
71
|
+
and verdict.reason in _CONTENT_BLOCKING_GATE_REASONS
|
|
72
|
+
and verdict.review_id == snapshot.copilot_review_id
|
|
73
|
+
):
|
|
74
|
+
return True
|
|
35
75
|
return False
|
|
36
76
|
|
|
37
77
|
|
|
@@ -21,6 +21,12 @@ from dataclasses import dataclass
|
|
|
21
21
|
from typing import TYPE_CHECKING
|
|
22
22
|
|
|
23
23
|
from agentic_devtools.cli.ci.models import COPILOT_LOGINS, ReviewInfo
|
|
24
|
+
from agentic_devtools.cli.github.ccr_review_format import (
|
|
25
|
+
VERDICT_NOT_APPROVE,
|
|
26
|
+
parse_reported_comment_count,
|
|
27
|
+
parse_suppressed_count,
|
|
28
|
+
parse_verdict,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
31
|
if TYPE_CHECKING:
|
|
26
32
|
from agentic_devtools.cli.ci.provider import CIPlatformProvider
|
|
@@ -51,6 +57,9 @@ REASON_HAS_COMMENTS = "copilot_review_has_comments"
|
|
|
51
57
|
#: Copilot's review body reports suppressed/low-confidence comments > 0.
|
|
52
58
|
REASON_SUPPRESSED_COMMENTS = "copilot_review_suppressed_comments"
|
|
53
59
|
|
|
60
|
+
#: New CCR format review body has a "Not ready to approve" verdict heading.
|
|
61
|
+
REASON_NEW_CCR_NOT_APPROVED = "copilot_review_new_format_not_approved"
|
|
62
|
+
|
|
54
63
|
#: PR diff content changed since the prior-commit review.
|
|
55
64
|
REASON_CONTENT_CHANGED = "copilot_review_content_changed_since_review"
|
|
56
65
|
|
|
@@ -131,40 +140,6 @@ def _select_latest_prior_review(reviews: list[ReviewInfo], head_sha: str) -> Rev
|
|
|
131
140
|
return max(candidates, key=lambda r: (r.submitted_at, r.id))
|
|
132
141
|
|
|
133
142
|
|
|
134
|
-
def _parse_body_comment_count(body: str) -> int | None:
|
|
135
|
-
"""Parse Copilot's self-reported comment count from *body*.
|
|
136
|
-
|
|
137
|
-
Matches:
|
|
138
|
-
- ``"generated no comments"``
|
|
139
|
-
- ``"generated no new comments"`` (re-review phrasing)
|
|
140
|
-
- ``"generated N comment(s)"``
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
An integer count, or ``None`` when the body format is unrecognised.
|
|
144
|
-
"""
|
|
145
|
-
if re.search(r"generated no( new)? comments", body, re.IGNORECASE):
|
|
146
|
-
return 0
|
|
147
|
-
m = re.search(r"generated (\d+) comment", body, re.IGNORECASE)
|
|
148
|
-
if m:
|
|
149
|
-
return int(m.group(1))
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _parse_suppressed_count(body: str) -> int:
|
|
154
|
-
"""Parse the suppressed / low-confidence comment count from *body*.
|
|
155
|
-
|
|
156
|
-
Matches ``Suppressed (N)`` and ``Low confidence (N)`` as reported by
|
|
157
|
-
Copilot in the review summary text.
|
|
158
|
-
"""
|
|
159
|
-
m = re.search(r"[Ss]uppressed[^.(]*\((\d+)\)", body)
|
|
160
|
-
if m:
|
|
161
|
-
return int(m.group(1))
|
|
162
|
-
m = re.search(r"[Ll]ow confidence[^.(]*\((\d+)\)", body)
|
|
163
|
-
if m:
|
|
164
|
-
return int(m.group(1))
|
|
165
|
-
return 0
|
|
166
|
-
|
|
167
|
-
|
|
168
143
|
def _evaluate_synthetic_review(
|
|
169
144
|
review: ReviewInfo,
|
|
170
145
|
provider: CIPlatformProvider,
|
|
@@ -271,14 +246,33 @@ def _evaluate_standard_review(
|
|
|
271
246
|
) -> CopilotGateVerdict:
|
|
272
247
|
"""Evaluate a standard (non-synthetic) Copilot review.
|
|
273
248
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
249
|
+
Handles both the legacy CCR body format (``"generated N comments"``,
|
|
250
|
+
``"Suppressed (N)"``) and the new-style CCR private-preview format with a
|
|
251
|
+
``### <emoji> <verdict>`` heading and ``**Comments generated:** N new``
|
|
252
|
+
metrics footer. When the body format is unrecognised, falls back to the
|
|
253
|
+
API inline comment count (fail-closed).
|
|
277
254
|
"""
|
|
278
255
|
body = review.body
|
|
279
256
|
|
|
280
|
-
|
|
281
|
-
|
|
257
|
+
# --- New CCR format: check verdict heading first ---
|
|
258
|
+
# "### 🟡 Not ready to approve" → block immediately regardless of comment counts.
|
|
259
|
+
# "### ✅ Ready to approve" → proceed with comment-count checks below.
|
|
260
|
+
new_ccr_verdict = parse_verdict(body)
|
|
261
|
+
if new_ccr_verdict == VERDICT_NOT_APPROVE:
|
|
262
|
+
suppressed_count = parse_suppressed_count(body)
|
|
263
|
+
detail_parts = ["Copilot review heading indicates 'Not ready to approve'"]
|
|
264
|
+
if suppressed_count > 0:
|
|
265
|
+
detail_parts.append(f"{suppressed_count} suppressed/low-confidence comment(s)")
|
|
266
|
+
return CopilotGateVerdict(
|
|
267
|
+
passed=False,
|
|
268
|
+
reason=REASON_NEW_CCR_NOT_APPROVED,
|
|
269
|
+
review_id=review.id,
|
|
270
|
+
suppressed_count=suppressed_count,
|
|
271
|
+
details="; ".join(detail_parts),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
body_comment_count = parse_reported_comment_count(body)
|
|
275
|
+
suppressed_count = parse_suppressed_count(body)
|
|
282
276
|
|
|
283
277
|
if body_comment_count is None:
|
|
284
278
|
# Body format unrecognised — fall back to the API inline count.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Canonical parsers for Copilot Code Review (CCR) review bodies.
|
|
2
|
+
|
|
3
|
+
This module is the **single source of truth** for extracting structured
|
|
4
|
+
information from a Copilot Code Review body. It understands two body formats:
|
|
5
|
+
|
|
6
|
+
**Legacy format** — prose summary with:
|
|
7
|
+
|
|
8
|
+
- ``"generated N comment(s)"`` / ``"generated no comments"`` phrasing.
|
|
9
|
+
- ``Suppressed (N)`` / ``Low confidence (N)`` counts.
|
|
10
|
+
- A ``<details><summary>… suppressed due to low confidence …</summary>…</details>``
|
|
11
|
+
block whose entries are ``**path**: body`` / ``` `path`: body ``` lines.
|
|
12
|
+
|
|
13
|
+
**New CCR private-preview format** — a ``### <emoji> <verdict>`` heading
|
|
14
|
+
(``### 🟡 Not ready to approve`` / ``### ✅ Ready to approve``) followed by a
|
|
15
|
+
``<details><summary>Review details</summary>`` block containing:
|
|
16
|
+
|
|
17
|
+
- A ``### Comments suppressed due to low confidence (N)`` heading, whose entries
|
|
18
|
+
are ``**path:line**`` on one line followed by a ``* body`` bullet (and, often,
|
|
19
|
+
a fenced code block).
|
|
20
|
+
- A metrics footer (``- **Files reviewed:** …``, ``- **Comments generated:** N``,
|
|
21
|
+
``- **Review effort level:** …``).
|
|
22
|
+
|
|
23
|
+
Centralising this logic keeps every consumer in the AI PR loop consistent:
|
|
24
|
+
|
|
25
|
+
- :mod:`agentic_devtools.cli.ci.pipeline.gate_verdict` (approval/merge gate)
|
|
26
|
+
- :mod:`agentic_devtools.cli.ci.github_provider` (suppressed-comment recovery
|
|
27
|
+
for repair dispatch)
|
|
28
|
+
- :mod:`agentic_devtools.cli.github.copilot_review_status` (poll-ready
|
|
29
|
+
review-status classification)
|
|
30
|
+
|
|
31
|
+
Public API
|
|
32
|
+
----------
|
|
33
|
+
- :data:`VERDICT_NOT_APPROVE`, :data:`VERDICT_APPROVE` — verdict constants.
|
|
34
|
+
- :data:`UNKNOWN_FILE` — placeholder path for unattributed suppressed comments.
|
|
35
|
+
- :func:`parse_verdict` — new-format ``### <verdict>`` heading → verdict.
|
|
36
|
+
- :func:`parse_reported_comment_count` — self-reported posted-comment count.
|
|
37
|
+
- :func:`parse_suppressed_count` — self-reported suppressed/low-confidence count.
|
|
38
|
+
- :func:`extract_suppressed_comment_entries` — ``(path, body)`` pairs for every
|
|
39
|
+
recoverable suppressed comment (both formats).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import re
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Public constants
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
#: Verdict returned when the new-format heading blocks approval.
|
|
51
|
+
VERDICT_NOT_APPROVE = "not_approve"
|
|
52
|
+
|
|
53
|
+
#: Verdict returned when the new-format heading approves.
|
|
54
|
+
VERDICT_APPROVE = "approve"
|
|
55
|
+
|
|
56
|
+
#: Placeholder path used when a suppressed comment cannot be attributed to a file.
|
|
57
|
+
UNKNOWN_FILE = "(unknown file)"
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Verdict heading
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
#: Matches any ``###``-level (or deeper) heading line; the heading text is captured.
|
|
64
|
+
_HEADING_RE = re.compile(r"^#{3,}\s+(.+?)\s*$", re.MULTILINE)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_verdict(body: str) -> str | None:
|
|
68
|
+
"""Parse the review verdict from a new-style CCR heading.
|
|
69
|
+
|
|
70
|
+
Scans for ``###``-level headings introduced by the new CCR private-preview
|
|
71
|
+
format (e.g. ``### 🟡 Not ready to approve``, ``### ✅ Ready to approve``)
|
|
72
|
+
and returns the verdict from the first heading that expresses one.
|
|
73
|
+
|
|
74
|
+
``"not ready to approve"`` is checked before ``"ready to approve"`` so the
|
|
75
|
+
substring match cannot mis-classify a blocking heading as approving.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
body: Full review body text.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
:data:`VERDICT_NOT_APPROVE` when a blocking verdict heading is found,
|
|
82
|
+
:data:`VERDICT_APPROVE` when an approving verdict heading is found, or
|
|
83
|
+
``None`` when no recognised CCR verdict heading is present (in which
|
|
84
|
+
case callers should fall back to their existing logic).
|
|
85
|
+
"""
|
|
86
|
+
if not body:
|
|
87
|
+
return None
|
|
88
|
+
for match in _HEADING_RE.finditer(body):
|
|
89
|
+
heading_text = match.group(1).strip().lower()
|
|
90
|
+
if "not ready to approve" in heading_text:
|
|
91
|
+
return VERDICT_NOT_APPROVE
|
|
92
|
+
if "ready to approve" in heading_text:
|
|
93
|
+
return VERDICT_APPROVE
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Reported comment count
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
#: Legacy "generated no [new] comments" phrasing → count 0.
|
|
102
|
+
_GENERATED_NONE_RE = re.compile(r"generated no( new)? comments", re.IGNORECASE)
|
|
103
|
+
|
|
104
|
+
#: Legacy "generated N comment(s)" phrasing → count N.
|
|
105
|
+
_GENERATED_N_RE = re.compile(r"generated (\d+) comment", re.IGNORECASE)
|
|
106
|
+
|
|
107
|
+
#: New CCR metrics-footer "**Comments generated:** N [new]" → count N.
|
|
108
|
+
#: The trailing "new" qualifier is optional — some reviews report a bare count
|
|
109
|
+
#: (``**Comments generated:** 4``) while re-reviews report ``0 new``.
|
|
110
|
+
_METRICS_GENERATED_RE = re.compile(
|
|
111
|
+
r"Comments generated[^:\n]*:\s*\*{0,2}\s*(\d+)(?:\s+new)?\b",
|
|
112
|
+
re.IGNORECASE,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def parse_reported_comment_count(body: str) -> int | None:
|
|
117
|
+
"""Parse Copilot's self-reported posted-comment count from *body*.
|
|
118
|
+
|
|
119
|
+
Matches, in priority order:
|
|
120
|
+
|
|
121
|
+
- ``"generated no comments"`` / ``"generated no new comments"`` → ``0``
|
|
122
|
+
- ``"generated N comment(s)"`` → ``N``
|
|
123
|
+
- ``"**Comments generated:** N [new]"`` (new CCR metrics footer) → ``N``
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
body: Full review body text.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
An integer count, or ``None`` when no recognised count pattern is
|
|
130
|
+
present (callers fail closed on ``None``).
|
|
131
|
+
"""
|
|
132
|
+
if not body:
|
|
133
|
+
return None
|
|
134
|
+
if _GENERATED_NONE_RE.search(body):
|
|
135
|
+
return 0
|
|
136
|
+
match = _GENERATED_N_RE.search(body)
|
|
137
|
+
if match:
|
|
138
|
+
return int(match.group(1))
|
|
139
|
+
match = _METRICS_GENERATED_RE.search(body)
|
|
140
|
+
if match:
|
|
141
|
+
return int(match.group(1))
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# Suppressed / low-confidence count
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
#: "… suppressed … (N)" — matches both the legacy ``<summary>`` text and the
|
|
150
|
+
#: new-format ``### Comments suppressed due to low confidence (N)`` heading.
|
|
151
|
+
_SUPPRESSED_COUNT_RE = re.compile(r"[Ss]uppressed[^.(]*\((\d+)\)")
|
|
152
|
+
|
|
153
|
+
#: "… low confidence … (N)" fallback phrasing.
|
|
154
|
+
_LOW_CONFIDENCE_COUNT_RE = re.compile(r"[Ll]ow confidence[^.(]*\((\d+)\)")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_suppressed_count(body: str) -> int:
|
|
158
|
+
"""Parse the suppressed / low-confidence comment count from *body*.
|
|
159
|
+
|
|
160
|
+
Matches ``Suppressed (N)`` (including the new format's
|
|
161
|
+
``### Comments suppressed due to low confidence (N)`` heading) and, as a
|
|
162
|
+
fallback, ``Low confidence (N)``.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
body: Full review body text.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The parsed count, or ``0`` when no suppressed-count pattern is present.
|
|
169
|
+
"""
|
|
170
|
+
if not body:
|
|
171
|
+
return 0
|
|
172
|
+
match = _SUPPRESSED_COUNT_RE.search(body)
|
|
173
|
+
if match:
|
|
174
|
+
return int(match.group(1))
|
|
175
|
+
match = _LOW_CONFIDENCE_COUNT_RE.search(body)
|
|
176
|
+
if match:
|
|
177
|
+
return int(match.group(1))
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Suppressed comment entries
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
#: Legacy: the ``<details>`` block whose ``<summary>`` names the suppressed set.
|
|
186
|
+
_LEGACY_SUPPRESSED_BLOCK_RE = re.compile(
|
|
187
|
+
r"<details>\s*<summary>[^<]*suppressed due to low confidence[^<]*</summary>(.*?)</details>",
|
|
188
|
+
re.DOTALL | re.IGNORECASE,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
#: New format: content after the ``### Comments suppressed …`` heading, bounded
|
|
192
|
+
#: before the next heading, the metrics footer, or the ``</details>`` close so the
|
|
193
|
+
#: footer lines (``- **Files reviewed:** …``) are never captured as entries.
|
|
194
|
+
_NEW_SUPPRESSED_BLOCK_RE = re.compile(
|
|
195
|
+
r"#{2,}\s+Comments?\s+suppressed\s+due\s+to\s+low\s+confidence[^\n]*\n"
|
|
196
|
+
r"(.*?)"
|
|
197
|
+
r"(?=\n\s*#{2,}\s" # next heading
|
|
198
|
+
r"|\n\s*[-*]\s+\*\*(?:Files reviewed|Comments generated|Review effort)" # metrics footer
|
|
199
|
+
r"|</details>" # end of the Review details block
|
|
200
|
+
r"|\Z)",
|
|
201
|
+
re.DOTALL | re.IGNORECASE,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
#: A single suppressed entry: a bold/code file path followed by body text.
|
|
205
|
+
#: Matches ``**path**: body``, ``` `path`: body ```, ``**`path`**: body``, and the
|
|
206
|
+
#: new format's ``**path:line**`` header followed by a ``* body`` bullet.
|
|
207
|
+
_SUPPRESSED_ENTRY_RE = re.compile(
|
|
208
|
+
r"(?:\*\*`?|`)" # opening bold/code marker
|
|
209
|
+
r"([^`*\n]+?)" # file path (captured)
|
|
210
|
+
r"(?:`?\*\*|`)" # closing bold/code marker
|
|
211
|
+
r"\s*:?\s*" # optional colon separator
|
|
212
|
+
r"(.*?)(?=\n(?:\*\*`?|`)[^`*\n]+?(?:`?\*\*|`)\s*:?|\Z)", # body up to next entry or end
|
|
213
|
+
re.DOTALL,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _find_suppressed_block(body: str) -> str | None:
|
|
218
|
+
"""Return the suppressed-comment block content for either format, or ``None``.
|
|
219
|
+
|
|
220
|
+
Prefers the legacy ``<details>`` block; falls back to the new-format
|
|
221
|
+
``### Comments suppressed …`` section. Returns ``None`` when neither is
|
|
222
|
+
present or the located block is empty.
|
|
223
|
+
"""
|
|
224
|
+
legacy = _LEGACY_SUPPRESSED_BLOCK_RE.search(body)
|
|
225
|
+
if legacy:
|
|
226
|
+
return legacy.group(1).strip() or None
|
|
227
|
+
new = _NEW_SUPPRESSED_BLOCK_RE.search(body)
|
|
228
|
+
if new:
|
|
229
|
+
return new.group(1).strip() or None
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def extract_suppressed_comment_entries(body: str) -> list[tuple[str, str]]:
|
|
234
|
+
"""Extract ``(path, body)`` pairs for suppressed comments in *body*.
|
|
235
|
+
|
|
236
|
+
Handles both the legacy ``<details>`` block and the new-format
|
|
237
|
+
``### Comments suppressed …`` section. When a structured entry has no file
|
|
238
|
+
path, :data:`UNKNOWN_FILE` is used. When the block contains no structured
|
|
239
|
+
entries at all, each non-blank line becomes a standalone
|
|
240
|
+
``(UNKNOWN_FILE, line)`` fallback comment.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
body: Full review body text (may contain HTML/Markdown).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
A list of ``(path, comment_body)`` tuples in document order. Empty when
|
|
247
|
+
no suppressed block is found or the block yields no non-empty content.
|
|
248
|
+
"""
|
|
249
|
+
if not body:
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
block_content = _find_suppressed_block(body)
|
|
253
|
+
if not block_content:
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
entries: list[tuple[str, str]] = []
|
|
257
|
+
for entry_match in _SUPPRESSED_ENTRY_RE.finditer(block_content):
|
|
258
|
+
path = entry_match.group(1).strip() or UNKNOWN_FILE
|
|
259
|
+
entry_body = entry_match.group(2).strip()
|
|
260
|
+
# The new CCR format renders each comment as a markdown bullet
|
|
261
|
+
# (``* comment``); strip a single leading bullet marker so the recovered
|
|
262
|
+
# comment text is clean. Legacy bodies carry no bullet, so this is a
|
|
263
|
+
# no-op for them.
|
|
264
|
+
if entry_body[:2] in ("* ", "- "):
|
|
265
|
+
entry_body = entry_body[2:].strip()
|
|
266
|
+
if not entry_body:
|
|
267
|
+
continue
|
|
268
|
+
entries.append((path, entry_body))
|
|
269
|
+
|
|
270
|
+
# If the block matched but produced no structured entries, treat each
|
|
271
|
+
# non-empty line as a standalone comment (fail-open recovery).
|
|
272
|
+
if not entries:
|
|
273
|
+
for line in block_content.splitlines():
|
|
274
|
+
stripped = line.strip()
|
|
275
|
+
if not stripped:
|
|
276
|
+
continue
|
|
277
|
+
entries.append((UNKNOWN_FILE, stripped))
|
|
278
|
+
|
|
279
|
+
return entries
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
This module provides the ``agdt-gh-copilot-review-status`` CLI command that
|
|
4
4
|
fetches reviews via the ``gh`` CLI REST API, filters to the Copilot reviewer
|
|
5
5
|
bot on the current head commit, counts inline and suppressed (minimized)
|
|
6
|
-
comments via GraphQL cursor pagination,
|
|
7
|
-
|
|
6
|
+
comments via GraphQL cursor pagination, additionally parses the review **body**
|
|
7
|
+
for the new CCR private-preview format (a "Not ready to approve" verdict and
|
|
8
|
+
body-only suppressed comments that are not GitHub-minimized), classifies the
|
|
9
|
+
review status, and returns structured JSON to stdout while writing state keys.
|
|
8
10
|
|
|
9
11
|
Public API
|
|
10
12
|
----------
|
|
@@ -25,6 +27,12 @@ from typing import Any
|
|
|
25
27
|
|
|
26
28
|
from ...state import get_value, set_value
|
|
27
29
|
from ..subprocess_utils import run_safe
|
|
30
|
+
from .ccr_review_format import (
|
|
31
|
+
VERDICT_NOT_APPROVE,
|
|
32
|
+
parse_reported_comment_count,
|
|
33
|
+
parse_suppressed_count,
|
|
34
|
+
parse_verdict,
|
|
35
|
+
)
|
|
28
36
|
from .repo_resolution import resolve_github_repo
|
|
29
37
|
|
|
30
38
|
# ---------------------------------------------------------------------------
|
|
@@ -282,14 +290,42 @@ def _count_suppressed_comments(review_node_id: str) -> int:
|
|
|
282
290
|
return suppressed_count
|
|
283
291
|
|
|
284
292
|
|
|
285
|
-
def _classify_review_status(
|
|
293
|
+
def _classify_review_status(
|
|
294
|
+
review_state: str,
|
|
295
|
+
inline_count: int,
|
|
296
|
+
suppressed_count: int,
|
|
297
|
+
*,
|
|
298
|
+
not_approved: bool = False,
|
|
299
|
+
body_comment_count: int | None = None,
|
|
300
|
+
) -> tuple[str, str]:
|
|
286
301
|
"""Classify the overall Copilot review status.
|
|
287
302
|
|
|
303
|
+
Args:
|
|
304
|
+
review_state: The review's ``state`` (``APPROVED`` / ``COMMENTED`` /
|
|
305
|
+
``CHANGES_REQUESTED`` / …). The new CCR private-preview reviews are
|
|
306
|
+
always ``COMMENTED``, so the ``not_approved`` body verdict — not the
|
|
307
|
+
review state — is what makes them blocking.
|
|
308
|
+
inline_count: Copilot-authored inline review comments (REST).
|
|
309
|
+
suppressed_count: Suppressed/low-confidence comments, merged across
|
|
310
|
+
GraphQL-minimized comments and the review body so the new format's
|
|
311
|
+
body-only suppressed comments are counted.
|
|
312
|
+
not_approved: ``True`` when the review body carries a new-format
|
|
313
|
+
"Not ready to approve" verdict heading — always treated as feedback
|
|
314
|
+
regardless of comment counts or review state.
|
|
315
|
+
body_comment_count: Copilot's self-reported posted-comment count parsed
|
|
316
|
+
from the review body (``None`` when unrecognised); a positive value
|
|
317
|
+
is treated as feedback even when the REST inline count is 0.
|
|
318
|
+
|
|
288
319
|
Returns:
|
|
289
320
|
A ``(status, action_required)`` tuple.
|
|
290
321
|
"""
|
|
322
|
+
# A new-format "Not ready to approve" verdict is always blocking feedback,
|
|
323
|
+
# even with 0 inline/suppressed comments (prose-only verdict).
|
|
324
|
+
if not_approved:
|
|
325
|
+
return ("has-feedback", "address-copilot-review")
|
|
326
|
+
|
|
291
327
|
# Feedback check has highest priority
|
|
292
|
-
if inline_count > 0 or suppressed_count > 0:
|
|
328
|
+
if inline_count > 0 or suppressed_count > 0 or (body_comment_count or 0) > 0:
|
|
293
329
|
return ("has-feedback", "address-copilot-review")
|
|
294
330
|
|
|
295
331
|
if review_state == "CHANGES_REQUESTED":
|
|
@@ -350,6 +386,7 @@ def get_copilot_review_status(pr_number: int, repo: str, head_sha: str) -> dict[
|
|
|
350
386
|
review_node_id: str | None = review.get("node_id")
|
|
351
387
|
review_state: str = review.get("state", "")
|
|
352
388
|
submitted_at: str | None = review.get("submitted_at")
|
|
389
|
+
review_body: str = review.get("body") or ""
|
|
353
390
|
|
|
354
391
|
inline_count = _count_inline_comments(pr_number, repo, review_id)
|
|
355
392
|
|
|
@@ -362,7 +399,20 @@ def get_copilot_review_status(pr_number: int, repo: str, head_sha: str) -> dict[
|
|
|
362
399
|
file=sys.stderr,
|
|
363
400
|
)
|
|
364
401
|
|
|
365
|
-
|
|
402
|
+
# New CCR private-preview format: the verdict and suppressed comments live in
|
|
403
|
+
# the review *body*, not as GitHub-minimized comments. Merge the body-derived
|
|
404
|
+
# signals so a body-only "Not ready to approve" review is not misread as clean.
|
|
405
|
+
not_approved = parse_verdict(review_body) == VERDICT_NOT_APPROVE
|
|
406
|
+
body_comment_count = parse_reported_comment_count(review_body)
|
|
407
|
+
suppressed_count = max(suppressed_count, parse_suppressed_count(review_body))
|
|
408
|
+
|
|
409
|
+
status, action_required = _classify_review_status(
|
|
410
|
+
review_state,
|
|
411
|
+
inline_count,
|
|
412
|
+
suppressed_count,
|
|
413
|
+
not_approved=not_approved,
|
|
414
|
+
body_comment_count=body_comment_count,
|
|
415
|
+
)
|
|
366
416
|
|
|
367
417
|
review_url = f"https://github.com/{repo}/pull/{pr_number}#pullrequestreview-{review_id}"
|
|
368
418
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
agentic_devtools/__init__.py,sha256=J_Zw_vWKghk-cLmqI83hXQmSiS8zMhGIHM5WPLDkZuo,242
|
|
2
|
-
agentic_devtools/_version.py,sha256=
|
|
2
|
+
agentic_devtools/_version.py,sha256=W05wKAPL9NTr_eL2ykSOaNckhAZGNqg5k74bBouriC0,524
|
|
3
3
|
agentic_devtools/agdt_gitignore.py,sha256=aBPBQe7M0GLH8NIp1NsyN9ZiO80fNGOi46IcT5A4SK4,1569
|
|
4
4
|
agentic_devtools/background_tasks.py,sha256=IVC1XJKQzPBP8wCRNCZX_iZCg9FJY_GBUzddSJj7xqw,17473
|
|
5
5
|
agentic_devtools/config.py,sha256=DEVxTVZhsQbVQwO9qdB_1h19aBpjMzF6xJY_ePYLGLs,15629
|
|
@@ -136,7 +136,7 @@ agentic_devtools/cli/ci/agent_assignment.py,sha256=UFlSNju1exEjDvrzHfiQ5snGcd2ka
|
|
|
136
136
|
agentic_devtools/cli/ci/commands.py,sha256=Y4dJUuPF9zfXCggyXM_fctatTYqoZ3l0WmvujU-RD6o,6465
|
|
137
137
|
agentic_devtools/cli/ci/delete_review_comments_command.py,sha256=Fqv8zl33oY5z0BmUbyxDAoRxJen8TtgHa1WHDRVaXTU,8237
|
|
138
138
|
agentic_devtools/cli/ci/exceptions.py,sha256=zpKWOgnBbHHQgAQRvdRW4VFnE_qdgqgmo8r1H-x47l4,1086
|
|
139
|
-
agentic_devtools/cli/ci/github_provider.py,sha256=
|
|
139
|
+
agentic_devtools/cli/ci/github_provider.py,sha256=296yPPaNFtedDM1o_aa6JAVOs8t_qGOVSR1EiXyXznE,200944
|
|
140
140
|
agentic_devtools/cli/ci/guards.py,sha256=-kfmTMb4KoMeyHFTUJjfDWoXbNZ3waX-9NdBrq1Bcy4,26927
|
|
141
141
|
agentic_devtools/cli/ci/job_logs.py,sha256=s3Ufinl2WQPVISq8g11ZXU5GJ0WCKDmZlbyGJzvJEVU,9911
|
|
142
142
|
agentic_devtools/cli/ci/logging_config.py,sha256=Tw_h8LB709gbMd0WZMATLAPIJ1r_c_nE18Bi4bvldHA,2340
|
|
@@ -161,7 +161,7 @@ agentic_devtools/cli/ci/pipeline/command.py,sha256=CmsncrER2mgFYz8QAl5NbVzaCLdoH
|
|
|
161
161
|
agentic_devtools/cli/ci/pipeline/deferral.py,sha256=bNw5znZ0k_fhP1AdPo6hgZ9y-q86JrE-v_nBf1XJBSM,8241
|
|
162
162
|
agentic_devtools/cli/ci/pipeline/exceptions.py,sha256=4UXy2weeQZex_lp4sILNxlYfGjetxbR-tjms2ReT4iw,451
|
|
163
163
|
agentic_devtools/cli/ci/pipeline/exclusion.py,sha256=mrOG5G1-m3U-GM4YgtMTuzmoj02TXB6GbNxemRoLtV4,931
|
|
164
|
-
agentic_devtools/cli/ci/pipeline/gate_verdict.py,sha256=
|
|
164
|
+
agentic_devtools/cli/ci/pipeline/gate_verdict.py,sha256=Fv_sOYIYGtJ5AczBxjTaI6rVOUYlCpfMSSl8kdO8C18,17997
|
|
165
165
|
agentic_devtools/cli/ci/pipeline/models.py,sha256=L7qhU286K9TB6b_g6N9G-_aeYkDHRVM1JJ1Q0dWrt5U,2166
|
|
166
166
|
agentic_devtools/cli/ci/pipeline/runner.py,sha256=lUJNXPZKbI7Tfkj_xC2-_Im4RqkRbh-vsMbRCJCATlo,9657
|
|
167
167
|
agentic_devtools/cli/ci/pipeline/session_detector.py,sha256=dWJyZIv7NcJNUl7TIsYqNfzNmZu3vAxHaQp1wMqe70o,8926
|
|
@@ -171,7 +171,7 @@ agentic_devtools/cli/ci/pipeline/summary.py,sha256=CCvIPuje1hYvR1OIgogF-2SN5au_S
|
|
|
171
171
|
agentic_devtools/cli/ci/pipeline/actions/__init__.py,sha256=HbtdDtLr_ut8qWedTCRuZujKHWqo8lJfymxPpLoDF5E,1292
|
|
172
172
|
agentic_devtools/cli/ci/pipeline/actions/apply_suggestions.py,sha256=HuCdn-cFvMJ_inEEBXI1A6eOE9voUBk7DRkiJGgxvtc,21149
|
|
173
173
|
agentic_devtools/cli/ci/pipeline/actions/approve.py,sha256=CZKPihPg6L1o_8AymoGsnX5BfujCbYB8q5xr8VQ6YY8,6584
|
|
174
|
-
agentic_devtools/cli/ci/pipeline/actions/dispatch_repair.py,sha256=
|
|
174
|
+
agentic_devtools/cli/ci/pipeline/actions/dispatch_repair.py,sha256=8WCjpLsTSdTUCQCo4XBQI2EMTepuIbNHQo4YaIipeq8,16194
|
|
175
175
|
agentic_devtools/cli/ci/pipeline/actions/guards.py,sha256=UIkTgv5_aKqSVNRFlvl_JiAdFswm1uV5VJJfgu5FmFc,4101
|
|
176
176
|
agentic_devtools/cli/ci/pipeline/actions/merge.py,sha256=LOfPoYITFrvpO9H5UJHiXnbykStT0J3GnCmfJiGDVrA,9925
|
|
177
177
|
agentic_devtools/cli/ci/pipeline/actions/publish.py,sha256=A00Lk-vZKnhOCN2O5mkks0tayr7NA5eZfUA95CUP7gs,3990
|
|
@@ -240,7 +240,8 @@ agentic_devtools/cli/github/__init__.py,sha256=HmNZ0PuYe8owe8qS5j5C1MzrKi772eGCM
|
|
|
240
240
|
agentic_devtools/cli/github/apply_thread_autofix.py,sha256=PBJ8MwWaB92lCBOOzc-3S7ykYf5i2CswFUo-4XUneL0,32037
|
|
241
241
|
agentic_devtools/cli/github/async_commands.py,sha256=2drgrnay85RVR_oj1j7-qdBv-uusAvgvUbKwvNmO6o0,33688
|
|
242
242
|
agentic_devtools/cli/github/browser_apply_autofix.py,sha256=j13V7djjMrdOJikYaK8x4z1soe9Su6wQ6zq7L1uXCS8,19441
|
|
243
|
-
agentic_devtools/cli/github/
|
|
243
|
+
agentic_devtools/cli/github/ccr_review_format.py,sha256=2VDug2ApSjBmU5Hk53Nd8jIjR4Y6ciL91qw_w79IyOs,11028
|
|
244
|
+
agentic_devtools/cli/github/copilot_review_status.py,sha256=2infcWJe_bg4vLHDI6XHLL863lQJybnMqc7anuG63Cw,17987
|
|
244
245
|
agentic_devtools/cli/github/issue_commands.py,sha256=bZYs_0n-T3V4WlG2BX-xK4qH9uqk4e20fpvWWf3ZV6w,19964
|
|
245
246
|
agentic_devtools/cli/github/issue_dedup.py,sha256=g9HEX3tbAbZjt_Z9FN8WXD_d-mhDUajrS2Gne7nJ1EY,3432
|
|
246
247
|
agentic_devtools/cli/github/issue_dedup_integration.py,sha256=C9-0nMds7jZOKweNMT09yONcvtH6X6k88TH3yOpsnvc,14433
|
|
@@ -888,8 +889,8 @@ agentic_devtools/_bundled_skills/prompts/speckit.plan.prompt.md,sha256=IJja5r2Sd
|
|
|
888
889
|
agentic_devtools/_bundled_skills/prompts/speckit.specify.prompt.md,sha256=eyzE3GRi2hyW30a6xPYOU7q6MJf0skrD-baEGURYqpg,31
|
|
889
890
|
agentic_devtools/_bundled_skills/prompts/speckit.tasks.prompt.md,sha256=iPxXwon5nV6dNcJV8-JoP3PssKUVXctNiG-C9SsRhB8,29
|
|
890
891
|
agentic_devtools/_bundled_skills/prompts/speckit.taskstoissues.prompt.md,sha256=L5Y21PMSoUcPAAdHy2Jnf-wGVdi04jV_pPvyOJZfpm0,37
|
|
891
|
-
agentic_devtools-0.2.
|
|
892
|
-
agentic_devtools-0.2.
|
|
893
|
-
agentic_devtools-0.2.
|
|
894
|
-
agentic_devtools-0.2.
|
|
895
|
-
agentic_devtools-0.2.
|
|
892
|
+
agentic_devtools-0.2.332.dist-info/METADATA,sha256=2BJ-K1TYEWIAepVS_EtES8e15vcoX5DWhAWiTDoQOKU,29796
|
|
893
|
+
agentic_devtools-0.2.332.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
894
|
+
agentic_devtools-0.2.332.dist-info/entry_points.txt,sha256=Xs0VhRkREzs61FdX63Ed1qa1-WDeBppn7BvMdRIYYZ4,11395
|
|
895
|
+
agentic_devtools-0.2.332.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
|
|
896
|
+
agentic_devtools-0.2.332.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|