agentic-devtools 0.2.330__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.
@@ -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.330'
22
- __version_tuple__ = version_tuple = (0, 2, 330)
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 HTML.
279
+ """Parse suppressed comments from a Copilot review body.
300
280
 
301
- Extracts comments from the ``<details>`` block with summary containing
302
- "suppressed due to low confidence". Each entry is expected to have a bold
303
- or code-formatted file path followed by comment body text.
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 CHANGES_REQUESTED, or COMMENTED with inline comments.
25
- Unknown inline counts fail closed and are treated as actionable.
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
- Parses ``"generated N comments"`` and ``"Suppressed (N)"`` from the review
275
- body. When the body format is unrecognised, falls back to the API inline
276
- comment count (fail-closed).
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
- body_comment_count = _parse_body_comment_count(body)
281
- suppressed_count = _parse_suppressed_count(body)
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, classifies the review status, and
7
- returns structured JSON to stdout while writing state keys.
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(review_state: str, inline_count: int, suppressed_count: int) -> tuple[str, str]:
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
- status, action_required = _classify_review_status(review_state, inline_count, suppressed_count)
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
 
@@ -17,7 +17,7 @@ import os
17
17
  import re
18
18
  import sys
19
19
  from pathlib import Path
20
- from typing import Any, cast
20
+ from typing import TYPE_CHECKING, Any, cast
21
21
  from urllib.parse import urlparse
22
22
 
23
23
  from agentic_devtools.cli.cert_utils import ensure_ca_bundle as _ensure_ca_bundle
@@ -33,6 +33,9 @@ try:
33
33
  except ImportError:
34
34
  _VALID_ISSUE_ADAPTERS = frozenset({"jira", "github", "markdown"})
35
35
 
36
+ if TYPE_CHECKING: # pragma: no cover - typing-only import
37
+ from agentic_devtools.skill_injector import InjectionSummary
38
+
36
39
  _MANAGED_BIN_DIR = Path.home() / ".agdt" / "bin"
37
40
 
38
41
  _BANNER = """\
@@ -807,6 +810,57 @@ def _generate_setup_scripts(git_root: Path) -> None:
807
810
  print(msg)
808
811
 
809
812
 
813
+ def _resolve_injection_axes(git_root: Path) -> tuple[str | None, str | None]:
814
+ """Resolve filter-capable skill-injection axes from the persisted platform config.
815
+
816
+ Reads the raw ``platform`` section from the repo's
817
+ ``.github/agdt-config.json`` (via :func:`load_repo_config`, which never
818
+ raises and returns ``{}`` when the file is absent or malformed) and maps it
819
+ to a filter-capable ``(issue_adapter, code_hosting)`` pair via
820
+ :func:`resolve_platform_context`.
821
+
822
+ Returns ``(None, None)`` — legacy inject-all — when:
823
+
824
+ - no config exists yet (a first-time setup run before platform detection has
825
+ written a config in a prior/earlier step),
826
+ - the ``platform`` section is absent or not a mapping, or
827
+ - neither axis holds a confidently-resolved, filter-capable value
828
+ (``jira``/``github`` adapter, ``github``/``azure_devops`` hosting).
829
+
830
+ A returned ``None`` axis means "inject-all for that axis"; when both axes are
831
+ ``None`` injection is byte-identical to the legacy inject-all behavior.
832
+ """
833
+ from agentic_devtools.config import load_repo_config # noqa: PLC0415
834
+ from agentic_devtools.skill_classification import resolve_platform_context # noqa: PLC0415
835
+
836
+ raw_config = load_repo_config(str(git_root))
837
+ return resolve_platform_context(raw_config.get("platform"))
838
+
839
+
840
+ def _format_injection_summary(
841
+ summary: "InjectionSummary",
842
+ issue_adapter: str | None,
843
+ code_hosting: str | None,
844
+ ) -> str:
845
+ """Build the success line printed after a skill-injection pass.
846
+
847
+ When both axes are ``None`` (legacy inject-all), no filter was applied and
848
+ the pruned count is always zero, so the line omits the prune/platform
849
+ detail. When at least one axis is resolved, the line reports how many files
850
+ were pruned and which axes constrained injection (an unresolved axis renders
851
+ as ``unrestricted``).
852
+ """
853
+ if issue_adapter is None and code_hosting is None:
854
+ return f" ✓ Injected {summary.injected} agent/prompt skills (no platform filter applied)"
855
+
856
+ adapter_label = issue_adapter if issue_adapter is not None else "unrestricted"
857
+ hosting_label = code_hosting if code_hosting is not None else "unrestricted"
858
+ return (
859
+ f" ✓ Injected {summary.injected} agent/prompt skills, pruned {summary.pruned} "
860
+ f"(issue_adapter={adapter_label}, code_hosting={hosting_label})"
861
+ )
862
+
863
+
810
864
  def setup_cmd() -> None:
811
865
  """Full setup: install Copilot CLI + GitHub CLI, then verify all dependencies.
812
866
 
@@ -1263,26 +1317,47 @@ def setup_cmd() -> None:
1263
1317
  # best-effort optional feature: guard the import so that agdt-setup still
1264
1318
  # works even if the module is missing or uses syntax/features not supported
1265
1319
  # by the current interpreter.
1266
- inject_skills = None # type: ignore[assignment]
1320
+ inject_skills_with_summary = None # type: ignore[assignment]
1267
1321
  try:
1268
- from agentic_devtools.skill_injector import inject_skills as _inject_skills
1322
+ from agentic_devtools.skill_injector import ( # noqa: PLC0415
1323
+ inject_skills_with_summary as _inject_with_summary,
1324
+ )
1269
1325
 
1270
- inject_skills = _inject_skills
1326
+ inject_skills_with_summary = _inject_with_summary
1271
1327
  except (SyntaxError, ImportError) as exc:
1272
1328
  print(
1273
1329
  f" ⚠ Failed to import skill injector ({exc!r}) — skipping agent/prompt skill injection",
1274
1330
  file=sys.stderr,
1275
1331
  )
1276
1332
 
1277
- if inject_skills is not None and inject_skills(git_root):
1278
- print(" ✓ Injected agent/prompt skills into .github/agents/ and .github/prompts/")
1279
- repo_mutations_succeeded = True
1280
- elif inject_skills is not None:
1281
- print(
1282
- " ⚠ Failed to inject agent/prompt skills this may be due to"
1283
- " directory permissions or missing/corrupted bundled skills",
1284
- file=sys.stderr,
1333
+ if inject_skills_with_summary is not None:
1334
+ # Resolve filter-capable platform axes from the persisted config
1335
+ # (written by an earlier platform-detection run). Both axes are
1336
+ # None — and injection is byte-identical to the legacy inject-all —
1337
+ # only when the config is absent, the platform section is missing
1338
+ # or non-mapping, or neither axis holds a confidently-resolved
1339
+ # value ({jira,github} adapter / {github,azure_devops} hosting).
1340
+ # Note: if a prior run already wrote load_platform_config()
1341
+ # defaults (e.g. issue_adapter="jira") into
1342
+ # .github/agdt-config.json, those persisted values ARE
1343
+ # filter-capable and can activate the issue-adapter axis here.
1344
+ # Defaults are NOT applied at injection time — only values
1345
+ # already present in the file matter.
1346
+ inj_issue_adapter, inj_code_hosting = _resolve_injection_axes(git_root)
1347
+ inj_success, inj_summary = inject_skills_with_summary(
1348
+ git_root,
1349
+ issue_adapter=inj_issue_adapter,
1350
+ code_hosting=inj_code_hosting,
1285
1351
  )
1352
+ if inj_success:
1353
+ print(_format_injection_summary(inj_summary, inj_issue_adapter, inj_code_hosting))
1354
+ repo_mutations_succeeded = True
1355
+ else:
1356
+ print(
1357
+ " ⚠ Failed to inject agent/prompt skills — this may be due to"
1358
+ " directory permissions or missing/corrupted bundled skills",
1359
+ file=sys.stderr,
1360
+ )
1286
1361
 
1287
1362
  # ── Platform & Workflow Setup ──────────────────────────────
1288
1363
  if not args.system_only:
@@ -1,6 +1,6 @@
1
1
  """Classification reader for context-aware conditional skill injection.
2
2
 
3
- Provides a ``Classification`` dataclass and two public functions:
3
+ Provides a ``Classification`` dataclass and three public functions:
4
4
 
5
5
  - ``parse_classification(frontmatter)`` — defensively reads the ``agdt`` block
6
6
  from a frontmatter mapping and returns a ``Classification``. Never raises,
@@ -8,6 +8,9 @@ Provides a ``Classification`` dataclass and two public functions:
8
8
  - ``should_inject(classification, *, issue_adapter, code_hosting)`` — pure
9
9
  predicate deciding whether a skill should be injected given resolved platform
10
10
  values.
11
+ - ``resolve_platform_context(platform)`` — maps a raw ``platform`` config
12
+ section to the filter-capable ``(issue_adapter, code_hosting)`` axes to
13
+ forward to injection. Never raises, never mutates the input.
11
14
  """
12
15
 
13
16
  from __future__ import annotations
@@ -19,7 +22,7 @@ from typing import Any
19
22
 
20
23
  from agentic_devtools.config import VALID_CODE_HOSTING, VALID_ISSUE_ADAPTERS
21
24
 
22
- __all__ = ["Classification", "parse_classification", "should_inject"]
25
+ __all__ = ["Classification", "parse_classification", "resolve_platform_context", "should_inject"]
23
26
 
24
27
 
25
28
  @dataclass(frozen=True)
@@ -38,6 +41,14 @@ class Classification:
38
41
  _TRUE_TOKENS: frozenset[str] = frozenset({"true", "yes", "on", "1"})
39
42
  _FALSE_TOKENS: frozenset[str] = frozenset({"false", "no", "off", "0"})
40
43
 
44
+ # Filter-capable platform values. Only these confidently-resolved values
45
+ # activate an injection axis; every other value — the non-filter-capable
46
+ # catch-alls (``markdown`` for issue_adapter, ``other`` for code_hosting), an
47
+ # absent key, a non-string value, or a non-mapping ``platform`` section — leaves
48
+ # that axis unrestricted (``None``), yielding legacy inject-all for that axis.
49
+ _FILTER_CAPABLE_ISSUE_ADAPTERS: frozenset[str] = frozenset({"jira", "github"})
50
+ _FILTER_CAPABLE_CODE_HOSTING: frozenset[str] = frozenset({"github", "azure_devops"})
51
+
41
52
 
42
53
  def _coerce_always(value: Any) -> bool:
43
54
  """Coerce an ``always`` field value to bool using an explicit allowlist.
@@ -209,3 +220,41 @@ def should_inject(
209
220
  return False
210
221
 
211
222
  return True
223
+
224
+
225
+ def resolve_platform_context(platform: Any) -> tuple[str | None, str | None]:
226
+ """Resolve filter-capable ``(issue_adapter, code_hosting)`` from a platform config.
227
+
228
+ Reads a *raw* ``platform`` config section (the value of the ``platform`` key
229
+ in ``.github/agdt-config.json``) and returns the pair of resolved injection
230
+ axes suitable for forwarding to :func:`inject_skills`. Only
231
+ *confidently-resolved, filter-capable* values activate an axis:
232
+
233
+ - ``issue_adapter`` ∈ {``jira``, ``github``}
234
+ - ``code_hosting`` ∈ {``github``, ``azure_devops``}
235
+
236
+ Every other input leaves that axis unrestricted (``None``):
237
+
238
+ - non-filter-capable catch-all values (``markdown`` for the adapter axis,
239
+ ``other`` for the hosting axis),
240
+ - absent keys,
241
+ - non-string values, and
242
+ - a non-mapping ``platform`` argument (including ``None``).
243
+
244
+ A ``None`` axis means "inject-all for that axis"; when *both* axes are
245
+ ``None`` injection is byte-identical to the legacy inject-all behavior.
246
+
247
+ The function is pure: it never raises and never mutates *platform*.
248
+ """
249
+ if not isinstance(platform, Mapping):
250
+ return None, None
251
+
252
+ raw_adapter = platform.get("issue_adapter")
253
+ issue_adapter = (
254
+ raw_adapter if isinstance(raw_adapter, str) and raw_adapter in _FILTER_CAPABLE_ISSUE_ADAPTERS else None
255
+ )
256
+
257
+ raw_hosting = platform.get("code_hosting")
258
+ code_hosting = raw_hosting if isinstance(raw_hosting, str) and raw_hosting in _FILTER_CAPABLE_CODE_HOSTING else None
259
+
260
+ return issue_adapter, code_hosting
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import re
13
13
  import shutil
14
14
  import warnings
15
+ from dataclasses import dataclass
15
16
  from pathlib import Path
16
17
 
17
18
  import yaml
@@ -345,12 +346,32 @@ def _normalize_platform_arg(
345
346
  # ---------------------------------------------------------------------------
346
347
 
347
348
 
348
- def inject_skills(
349
+ @dataclass(frozen=True)
350
+ class InjectionSummary:
351
+ """Counts describing the outcome of a skill-injection pass.
352
+
353
+ Attributes:
354
+ injected: Total number of managed ``agdt.*`` skill files selected for
355
+ injection across both kinds (agents + prompts), de-duplicated by
356
+ flattened filename within each kind and excluding the generated
357
+ ``agdt.README.md`` manifests. This is a best-effort count and can
358
+ include files counted before a later write-time ``OSError``.
359
+ pruned: Total number of source files removed by the classification
360
+ filter across both kinds. Always ``0`` when neither platform axis
361
+ is resolved (legacy inject-all), because filtering is skipped
362
+ entirely in that case.
363
+ """
364
+
365
+ injected: int
366
+ pruned: int
367
+
368
+
369
+ def inject_skills_with_summary(
349
370
  git_root: Path | None,
350
371
  *,
351
372
  issue_adapter: str | None = None,
352
373
  code_hosting: str | None = None,
353
- ) -> bool:
374
+ ) -> tuple[bool, InjectionSummary]:
354
375
  """Mirror bundled agent/prompt files into the target repo.
355
376
 
356
377
  Places files directly into ``{git_root}/.github/agents/`` and
@@ -375,16 +396,21 @@ def inject_skills(
375
396
  :class:`RuntimeWarning`.
376
397
 
377
398
  Returns:
378
- ``True`` when both kinds (agents/prompts) were injected successfully.
379
- Returns ``False`` when:
399
+ A ``(success, summary)`` tuple. ``success`` is ``True`` when both
400
+ kinds (agents/prompts) were injected successfully, and ``False`` when:
380
401
  - ``git_root`` is ``None``,
381
402
  - a source directory for a required kind cannot be resolved,
382
403
  - a ``UnicodeDecodeError`` occurs while reading source files (non-UTF8
383
404
  content), or
384
405
  - an ``OSError`` occurs while writing mirrored files or manifests.
406
+ ``summary`` is an :class:`InjectionSummary` carrying best-effort counts
407
+ of injected and pruned files (populated even on the ``OSError`` path).
385
408
  """
386
409
  if git_root is None:
387
- return False
410
+ return False, InjectionSummary(injected=0, pruned=0)
411
+
412
+ injected_total = 0
413
+ pruned_total = 0
388
414
 
389
415
  # Normalize both axes: strip whitespace; treat empty strings and
390
416
  # unrecognised values as None (unresolved / inject-all for that axis).
@@ -447,6 +473,7 @@ def inject_skills(
447
473
  # README manifest reflect only actually-injected files.
448
474
  fm_cache: dict[Path, dict[str, object]] = {}
449
475
  if issue_adapter is not None or code_hosting is not None:
476
+ pre_filter_count = len(source_files)
450
477
  filtered: list[Path] = []
451
478
  for src in source_files:
452
479
  try:
@@ -466,6 +493,7 @@ def inject_skills(
466
493
  fm_cache[src] = fm
467
494
  filtered.append(src)
468
495
  source_files = filtered
496
+ pruned_total += pre_filter_count - len(source_files)
469
497
 
470
498
  # Build set of flattened filenames for stale-cleanup comparison.
471
499
  # Also detect duplicate flat names — would cause silent overwriting.
@@ -503,6 +531,8 @@ def inject_skills(
503
531
  flat_name_origins[flat_name] = src
504
532
  source_rel_names.add(flat_name)
505
533
 
534
+ injected_total += len(source_rel_names)
535
+
506
536
  # Copy files, flattening subdirectory structure into filenames.
507
537
  # Iterate the de-duplicated mapping so each flat_name is written
508
538
  # exactly once (the last source wins, consistent with the warning).
@@ -548,6 +578,36 @@ def inject_skills(
548
578
  encoding="utf-8",
549
579
  )
550
580
 
551
- return overall_success
581
+ return overall_success, InjectionSummary(injected=injected_total, pruned=pruned_total)
552
582
  except OSError:
553
- return False
583
+ return False, InjectionSummary(injected=injected_total, pruned=pruned_total)
584
+
585
+
586
+ # Private-name alias kept for internal/test imports that reference
587
+ # ``_inject_skills_with_summary``.
588
+ _inject_skills_with_summary = inject_skills_with_summary
589
+
590
+
591
+ def inject_skills(
592
+ git_root: Path | None,
593
+ *,
594
+ issue_adapter: str | None = None,
595
+ code_hosting: str | None = None,
596
+ ) -> bool:
597
+ """Mirror bundled agent/prompt files into the target repo (bool wrapper).
598
+
599
+ Thin backward-compatible wrapper around
600
+ :func:`inject_skills_with_summary` that discards the
601
+ :class:`InjectionSummary` and returns only the success flag. See
602
+ :func:`inject_skills_with_summary` for full argument and behavior details.
603
+
604
+ Returns:
605
+ ``True`` when both kinds (agents/prompts) were injected successfully,
606
+ ``False`` otherwise (see :func:`inject_skills_with_summary`).
607
+ """
608
+ success, _summary = inject_skills_with_summary(
609
+ git_root,
610
+ issue_adapter=issue_adapter,
611
+ code_hosting=code_hosting,
612
+ )
613
+ return success
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-devtools
3
- Version: 0.2.330
3
+ Version: 0.2.332
4
4
  Summary: Agentic devtools integrate Jira, DevOps & more
5
5
  Author: ayaiayorg
6
6
  License-Expression: MIT
@@ -1,12 +1,12 @@
1
1
  agentic_devtools/__init__.py,sha256=J_Zw_vWKghk-cLmqI83hXQmSiS8zMhGIHM5WPLDkZuo,242
2
- agentic_devtools/_version.py,sha256=yw4gh-3pxmSlDvPv-VudqAFGlEfaXnJoT9K9vnHE8cM,524
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
6
6
  agentic_devtools/context_budget.py,sha256=db4w2KzH9Q6rS4k99uyY6eDB7550P8S-ppYV6hYN5nY,13385
7
7
  agentic_devtools/file_locking.py,sha256=bhVMxmMKiDn9fYmbAYzIjVfWHG-_br5IfHg7zN5XhHE,6747
8
- agentic_devtools/skill_classification.py,sha256=_bYuSraEHXGxZ5uq8EMDa3Eh4CuEWW6WtNfCo1V7d9w,7108
9
- agentic_devtools/skill_injector.py,sha256=W_jMOhsbeKRwozBUbONk0YyETbKtAVgOTAPfcPYBffI,23775
8
+ agentic_devtools/skill_classification.py,sha256=hCeojZ-q33h4pbjs7IuQ2jUWZ3xAOrlSrHOiiQ0sLws,9473
9
+ agentic_devtools/skill_injector.py,sha256=Hjeu9uyoSxiG89jCWlblrm95jnJk5auoTmxm13hDDM4,26226
10
10
  agentic_devtools/state.py,sha256=Gy0IQI8rOmwN3sq88HX-OG1wLjyWzZrWwNEyN49DW14,65708
11
11
  agentic_devtools/submission_manager.py,sha256=XrKEjxzyH2rX-3HOqIk6KVEjDKAyvjiFzQtg7PyF81w,22056
12
12
  agentic_devtools/submission_processor.py,sha256=H05p3juE0q2N4Ru034ig-KG9CLkK2h2u7W45surZP9E,15927
@@ -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=Qqsn6-nguIgB0KZJ_R1XmbR5V0PQtTx9EL7_3Fx8Xtc,202343
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=PpZCakSiKSwzyLCodJ_mTIR4LB11sIHtSwldsk9SZn0,17723
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=eo-DTOBbnN3xw4yXKRe2yJczK8CluaEgCkpBX3IOc-0,14533
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/copilot_review_status.py,sha256=g5CG1x5ojCKrOsYB5MaKLgX8M4aeYewUMqngN6Oc1yQ,15646
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
@@ -300,7 +301,7 @@ agentic_devtools/cli/segments/commands.py,sha256=ah0sdZXSVQ-L2G0RtwJ6iEYLmred8cN
300
301
  agentic_devtools/cli/setup/__init__.py,sha256=-VWMVxm_3UrXEqbxRZi-dG8Wbzq8H4o4HsJP9RAHbaI,1520
301
302
  agentic_devtools/cli/setup/autorun.py,sha256=vJfXqTIL-JxbUZUvymq2CQsKfftpkRYvvgdNjYSBmbg,6868
302
303
  agentic_devtools/cli/setup/autorun_resolution.py,sha256=w0lOKbVH7h_uOOd5XSm9CUZBXkhns197K-SoILSDKGQ,3034
303
- agentic_devtools/cli/setup/commands.py,sha256=_YU2f0Bh3DUt8XRyN5JBSf74MawSQ9zOMFbDn_0KEmE,83973
304
+ agentic_devtools/cli/setup/commands.py,sha256=XaqhbjxASRYgzlg-cqO_JR3zXDHngFockyohJSgKRWw,87854
304
305
  agentic_devtools/cli/setup/commit_template_setup.py,sha256=DRcLUWxIZrfiu5BaoH_qo6bZBFQlp9cdiAmweTM3c_A,4886
305
306
  agentic_devtools/cli/setup/copilot-instructions.md,sha256=UGAHBwSMRPY1d4vd_HySkF5-d7D5iVfNWF0rcwBkrZw,1083
306
307
  agentic_devtools/cli/setup/copilot_cli_installer.py,sha256=yHGDQ32K5jLykD5RuY3lmk2FH7k36fsqrZxnKazurLg,10056
@@ -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.330.dist-info/METADATA,sha256=JwWbNGOi0QUWoBNeBH3jwqzH5wsKvDFnYYnTme03SSk,29796
892
- agentic_devtools-0.2.330.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
893
- agentic_devtools-0.2.330.dist-info/entry_points.txt,sha256=Xs0VhRkREzs61FdX63Ed1qa1-WDeBppn7BvMdRIYYZ4,11395
894
- agentic_devtools-0.2.330.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
895
- agentic_devtools-0.2.330.dist-info/RECORD,,
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,,