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.
- 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/cli/setup/commands.py +87 -12
- agentic_devtools/skill_classification.py +51 -2
- agentic_devtools/skill_injector.py +67 -7
- {agentic_devtools-0.2.330.dist-info → agentic_devtools-0.2.332.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.330.dist-info → agentic_devtools-0.2.332.dist-info}/RECORD +14 -13
- {agentic_devtools-0.2.330.dist-info → agentic_devtools-0.2.332.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.330.dist-info → agentic_devtools-0.2.332.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.330.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
|
|
|
@@ -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
|
-
|
|
1320
|
+
inject_skills_with_summary = None # type: ignore[assignment]
|
|
1267
1321
|
try:
|
|
1268
|
-
from agentic_devtools.skill_injector import
|
|
1322
|
+
from agentic_devtools.skill_injector import ( # noqa: PLC0415
|
|
1323
|
+
inject_skills_with_summary as _inject_with_summary,
|
|
1324
|
+
)
|
|
1269
1325
|
|
|
1270
|
-
|
|
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
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
379
|
-
|
|
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,12 +1,12 @@
|
|
|
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
|
|
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=
|
|
9
|
-
agentic_devtools/skill_injector.py,sha256=
|
|
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=
|
|
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
|
|
@@ -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=
|
|
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.
|
|
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
|