td-ai-tools 1.0.2
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.
- package/README.md +49 -0
- package/agents/README.md +10 -0
- package/agents/horizon-component-library/AGENTS.md +184 -0
- package/agents/horizon-component-library/README.md +10 -0
- package/agents/horizon-component-library/scripts/td-guard.sh +49 -0
- package/bin/cli.js +209 -0
- package/package.json +23 -0
- package/scripts/smoke-install.sh +56 -0
- package/skills/README.md +15 -0
- package/skills/cache-reset/SKILL.md +18 -0
- package/skills/cache-reset/agents/openai.yaml +4 -0
- package/skills/car-ticket-generator/SKILL.md +130 -0
- package/skills/car-ticket-generator/agents/openai.yaml +4 -0
- package/skills/everhour-basecamp-estimates/.env.example +2 -0
- package/skills/everhour-basecamp-estimates/SKILL.md +73 -0
- package/skills/everhour-basecamp-estimates/agents/openai.yaml +4 -0
- package/skills/everhour-basecamp-estimates/scripts/update_estimates.py +518 -0
- package/skills/everhour-basecamp-estimates/tests/test_update_estimates.py +93 -0
- package/skills/horizon-component-migration/SKILL.md +59 -0
- package/skills/horizon-component-migration/agents/openai.yaml +4 -0
- package/skills/pr-solver/SKILL.md +50 -0
- package/skills/pr-solver/agents/openai.yaml +4 -0
- package/skills/pr-solver/references/github-pr-reviewthreads-graphql.md +63 -0
- package/skills/pr-solver/scripts/list_unresolved_threads.py +307 -0
- package/skills/pull-request/SKILL.md +216 -0
- package/skills/pull-request/agents/openai.yaml +4 -0
- package/skills/record-changes/SKILL.md +75 -0
- package/skills/record-changes/agents/openai.yaml +4 -0
- package/skills/record-changes/scripts/branch_diff_context.py +190 -0
- package/skills/stylesheet-migration/SKILL.md +36 -0
- package/skills/stylesheet-migration/agents/openai.yaml +6 -0
- package/skills/stylesheet-migration/scripts/__pycache__/liquid_stylesheet_migrator.cpython-312.pyc +0 -0
- package/skills/stylesheet-migration/scripts/__pycache__/test_liquid_stylesheet_migrator.cpython-312.pyc +0 -0
- package/skills/stylesheet-migration/scripts/liquid_stylesheet_migrator.py +204 -0
- package/skills/stylesheet-migration/scripts/migrate_stylesheet_tags.py +172 -0
- package/skills/stylesheet-migration/scripts/test_liquid_stylesheet_migrator.py +254 -0
- package/skills/td-js-vanilla-rules/SKILL.md +70 -0
- package/skills/td-js-vanilla-rules/agents/openai.yaml +3 -0
- package/skills/td-review/SKILL.md +122 -0
- package/skills/td-review/agents/openai.yaml +4 -0
- package/skills/td-review/agents/td-theme-reviewer.md +221 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-solver
|
|
3
|
+
description: Resolve GitHub pull request feedback by querying unresolved review conversations with the GitHub GraphQL API and implementing code changes for each unresolved thread. Use when a task includes a GitHub PR URL, asks to address open review comments, or asks to clear unresolved conversations; request a PR link if it is missing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GH PR Unresolved Conversations
|
|
7
|
+
|
|
8
|
+
Follow this workflow to turn unresolved PR review feedback into code fixes.
|
|
9
|
+
|
|
10
|
+
## Collect Input
|
|
11
|
+
|
|
12
|
+
1. Parse a GitHub PR link from the user request.
|
|
13
|
+
2. Ask for the PR link if none is provided. Do not continue until a link is available.
|
|
14
|
+
3. Accept `https://github.com/<owner>/<repo>/pull/<number>`.
|
|
15
|
+
|
|
16
|
+
## Fetch Unresolved Conversations with GraphQL
|
|
17
|
+
|
|
18
|
+
1. Use the GitHub GraphQL API, not the REST API, because unresolved status is only available on `reviewThreads.isResolved`.
|
|
19
|
+
2. Ensure one of these environment variables is set before querying: `GH_TOKEN` or `GITHUB_TOKEN`.
|
|
20
|
+
3. Run the bundled script:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python3 .agents/skills/pr-solver/scripts/list_unresolved_threads.py "<pr-url>" --format json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
4. Treat each returned thread where `is_resolved` is `false` as work to address.
|
|
27
|
+
5. If scripting is unavailable, run the GraphQL query in `references/github-pr-reviewthreads-graphql.md` directly.
|
|
28
|
+
|
|
29
|
+
## Implement Fixes Thread by Thread
|
|
30
|
+
|
|
31
|
+
1. Process unresolved threads one at a time.
|
|
32
|
+
2. Read the entire conversation in each thread before editing.
|
|
33
|
+
3. Convert the feedback into code changes in the referenced file and nearby context.
|
|
34
|
+
4. Prioritize the newest comment while preserving requirements from earlier comments.
|
|
35
|
+
5. Run targeted validation after each fix: tests, lint, or type checks relevant to touched files.
|
|
36
|
+
6. Mark a thread as blocked only when required context is missing or the feedback cannot be satisfied safely.
|
|
37
|
+
|
|
38
|
+
## Report Results
|
|
39
|
+
|
|
40
|
+
1. Report every unresolved thread with one status: `fixed`, `partially fixed`, or `blocked`.
|
|
41
|
+
2. Include file references for each implemented fix.
|
|
42
|
+
3. Include validation commands executed and whether they passed.
|
|
43
|
+
4. Include thread URLs for any partial or blocked outcomes.
|
|
44
|
+
5. Note that GitHub conversation resolution should happen after verification in the PR UI.
|
|
45
|
+
|
|
46
|
+
## Resources
|
|
47
|
+
|
|
48
|
+
- `.agents/skills/pr-solver/scripts/list_unresolved_threads.py`: Query GitHub GraphQL `reviewThreads`, paginate, and output unresolved conversations.
|
|
49
|
+
- `.agents/skills/pr-solver/references/github-pr-reviewthreads-graphql.md`: Direct GraphQL fallback query and field guidance.
|
|
50
|
+
- The installer also mirrors this skill under `.claude/skills/pr-solver/`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# GitHub PR Review Threads GraphQL Reference
|
|
2
|
+
|
|
3
|
+
Use this query when you cannot run `scripts/list_unresolved_threads.py`.
|
|
4
|
+
|
|
5
|
+
## Why GraphQL
|
|
6
|
+
|
|
7
|
+
Use GraphQL because unresolved status is exposed on `PullRequest.reviewThreads.nodes[].isResolved`.
|
|
8
|
+
REST pull request review endpoints do not provide equivalent conversation resolution state.
|
|
9
|
+
|
|
10
|
+
## Endpoint and Auth
|
|
11
|
+
|
|
12
|
+
- Endpoint: `https://api.github.com/graphql`
|
|
13
|
+
- Header: `Authorization: Bearer <GH_TOKEN or GITHUB_TOKEN>`
|
|
14
|
+
- Header: `Content-Type: application/json`
|
|
15
|
+
|
|
16
|
+
## Query
|
|
17
|
+
|
|
18
|
+
```graphql
|
|
19
|
+
query($owner: String!, $repo: String!, $number: Int!, $after: String) {
|
|
20
|
+
repository(owner: $owner, name: $repo) {
|
|
21
|
+
pullRequest(number: $number) {
|
|
22
|
+
number
|
|
23
|
+
title
|
|
24
|
+
url
|
|
25
|
+
reviewThreads(first: 50, after: $after) {
|
|
26
|
+
nodes {
|
|
27
|
+
id
|
|
28
|
+
isResolved
|
|
29
|
+
isOutdated
|
|
30
|
+
path
|
|
31
|
+
line
|
|
32
|
+
startLine
|
|
33
|
+
originalLine
|
|
34
|
+
originalStartLine
|
|
35
|
+
diffSide
|
|
36
|
+
comments(first: 100) {
|
|
37
|
+
nodes {
|
|
38
|
+
id
|
|
39
|
+
body
|
|
40
|
+
createdAt
|
|
41
|
+
url
|
|
42
|
+
author {
|
|
43
|
+
login
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
pageInfo {
|
|
49
|
+
hasNextPage
|
|
50
|
+
endCursor
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Pagination
|
|
59
|
+
|
|
60
|
+
1. Start with `after: null`.
|
|
61
|
+
2. Repeat while `reviewThreads.pageInfo.hasNextPage` is `true`.
|
|
62
|
+
3. Pass `endCursor` into the next request as `after`.
|
|
63
|
+
4. Treat threads with `isResolved == false` as unresolved work items.
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List GitHub PR review threads and unresolved conversations via GraphQL."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
GITHUB_GRAPHQL_URL = "https://api.github.com/graphql"
|
|
16
|
+
|
|
17
|
+
PR_URL_PATTERN = re.compile(
|
|
18
|
+
r"^https?://github\.com/(?P<owner>[^/\s]+)/(?P<repo>[^/\s]+)/pull/(?P<number>\d+)(?:[/?#].*)?$"
|
|
19
|
+
)
|
|
20
|
+
PR_REF_PATTERN = re.compile(
|
|
21
|
+
r"^(?P<owner>[A-Za-z0-9_.-]+)/(?P<repo>[A-Za-z0-9_.-]+)#(?P<number>\d+)$"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
QUERY = """
|
|
25
|
+
query($owner: String!, $repo: String!, $number: Int!, $after: String) {
|
|
26
|
+
repository(owner: $owner, name: $repo) {
|
|
27
|
+
pullRequest(number: $number) {
|
|
28
|
+
number
|
|
29
|
+
title
|
|
30
|
+
url
|
|
31
|
+
reviewThreads(first: 50, after: $after) {
|
|
32
|
+
nodes {
|
|
33
|
+
id
|
|
34
|
+
isResolved
|
|
35
|
+
isOutdated
|
|
36
|
+
path
|
|
37
|
+
line
|
|
38
|
+
startLine
|
|
39
|
+
originalLine
|
|
40
|
+
originalStartLine
|
|
41
|
+
diffSide
|
|
42
|
+
comments(first: 100) {
|
|
43
|
+
nodes {
|
|
44
|
+
id
|
|
45
|
+
body
|
|
46
|
+
createdAt
|
|
47
|
+
url
|
|
48
|
+
author {
|
|
49
|
+
login
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
pageInfo {
|
|
55
|
+
hasNextPage
|
|
56
|
+
endCursor
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_pr(value: str) -> Tuple[str, str, int]:
|
|
66
|
+
match = PR_URL_PATTERN.match(value.strip())
|
|
67
|
+
if not match:
|
|
68
|
+
match = PR_REF_PATTERN.match(value.strip())
|
|
69
|
+
if not match:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"PR must be https://github.com/<owner>/<repo>/pull/<number> or <owner>/<repo>#<number>."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
owner = match.group("owner")
|
|
75
|
+
repo = match.group("repo")
|
|
76
|
+
number = int(match.group("number"))
|
|
77
|
+
return owner, repo, number
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_token(token_env: str) -> str:
|
|
81
|
+
for key in [item.strip() for item in token_env.split(",") if item.strip()]:
|
|
82
|
+
token = os.getenv(key)
|
|
83
|
+
if token:
|
|
84
|
+
return token
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
f"Missing GitHub token. Set one of: {token_env}. "
|
|
87
|
+
"The token needs repo read permissions for private repositories."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def graphql_request(query: str, variables: Dict[str, object], token: str) -> Dict[str, object]:
|
|
92
|
+
body = json.dumps({"query": query, "variables": variables}).encode("utf-8")
|
|
93
|
+
req = urllib.request.Request(
|
|
94
|
+
GITHUB_GRAPHQL_URL,
|
|
95
|
+
data=body,
|
|
96
|
+
method="POST",
|
|
97
|
+
headers={
|
|
98
|
+
"Authorization": f"Bearer {token}",
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"Accept": "application/vnd.github+json",
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with urllib.request.urlopen(req) as response:
|
|
106
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
107
|
+
except urllib.error.HTTPError as exc:
|
|
108
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
109
|
+
raise RuntimeError(f"GraphQL request failed: HTTP {exc.code}: {detail}") from exc
|
|
110
|
+
except urllib.error.URLError as exc:
|
|
111
|
+
raise RuntimeError(f"Network error while calling GitHub GraphQL: {exc}") from exc
|
|
112
|
+
|
|
113
|
+
if payload.get("errors"):
|
|
114
|
+
raise RuntimeError(f"GraphQL errors: {json.dumps(payload['errors'], indent=2)}")
|
|
115
|
+
|
|
116
|
+
data = payload.get("data")
|
|
117
|
+
if not isinstance(data, dict):
|
|
118
|
+
raise RuntimeError("GraphQL response did not include a valid data object.")
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def fetch_review_threads(owner: str, repo: str, number: int, token: str) -> Tuple[Dict[str, object], List[Dict[str, object]]]:
|
|
123
|
+
all_threads: List[Dict[str, object]] = []
|
|
124
|
+
cursor: Optional[str] = None
|
|
125
|
+
pull_request: Optional[Dict[str, object]] = None
|
|
126
|
+
|
|
127
|
+
while True:
|
|
128
|
+
variables: Dict[str, object] = {
|
|
129
|
+
"owner": owner,
|
|
130
|
+
"repo": repo,
|
|
131
|
+
"number": number,
|
|
132
|
+
"after": cursor,
|
|
133
|
+
}
|
|
134
|
+
data = graphql_request(QUERY, variables, token)
|
|
135
|
+
repository = data.get("repository")
|
|
136
|
+
if not isinstance(repository, dict):
|
|
137
|
+
raise RuntimeError("Repository not found or not accessible with this token.")
|
|
138
|
+
|
|
139
|
+
pull_request = repository.get("pullRequest")
|
|
140
|
+
if not isinstance(pull_request, dict):
|
|
141
|
+
raise RuntimeError(f"Pull request #{number} was not found in {owner}/{repo}.")
|
|
142
|
+
|
|
143
|
+
review_threads = pull_request.get("reviewThreads") or {}
|
|
144
|
+
nodes = review_threads.get("nodes") or []
|
|
145
|
+
all_threads.extend([node for node in nodes if isinstance(node, dict)])
|
|
146
|
+
|
|
147
|
+
page_info = review_threads.get("pageInfo") or {}
|
|
148
|
+
has_next = bool(page_info.get("hasNextPage"))
|
|
149
|
+
if not has_next:
|
|
150
|
+
return pull_request, all_threads
|
|
151
|
+
|
|
152
|
+
next_cursor = page_info.get("endCursor")
|
|
153
|
+
if not isinstance(next_cursor, str) or not next_cursor:
|
|
154
|
+
return pull_request, all_threads
|
|
155
|
+
cursor = next_cursor
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def normalize_thread(thread: Dict[str, object]) -> Dict[str, object]:
|
|
159
|
+
comments_container = thread.get("comments") or {}
|
|
160
|
+
comment_nodes = comments_container.get("nodes") or []
|
|
161
|
+
comments: List[Dict[str, object]] = []
|
|
162
|
+
for comment in comment_nodes:
|
|
163
|
+
if not isinstance(comment, dict):
|
|
164
|
+
continue
|
|
165
|
+
author = comment.get("author") or {}
|
|
166
|
+
comments.append(
|
|
167
|
+
{
|
|
168
|
+
"id": comment.get("id"),
|
|
169
|
+
"author": (author.get("login") if isinstance(author, dict) else None),
|
|
170
|
+
"body": comment.get("body"),
|
|
171
|
+
"created_at": comment.get("createdAt"),
|
|
172
|
+
"url": comment.get("url"),
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
latest = comments[-1] if comments else {}
|
|
177
|
+
return {
|
|
178
|
+
"id": thread.get("id"),
|
|
179
|
+
"is_resolved": bool(thread.get("isResolved")),
|
|
180
|
+
"is_outdated": bool(thread.get("isOutdated")),
|
|
181
|
+
"path": thread.get("path"),
|
|
182
|
+
"line": thread.get("line"),
|
|
183
|
+
"start_line": thread.get("startLine"),
|
|
184
|
+
"original_line": thread.get("originalLine"),
|
|
185
|
+
"original_start_line": thread.get("originalStartLine"),
|
|
186
|
+
"diff_side": thread.get("diffSide"),
|
|
187
|
+
"latest_comment": latest,
|
|
188
|
+
"comments": comments,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def shorten(text: object, max_chars: int) -> str:
|
|
193
|
+
if not isinstance(text, str):
|
|
194
|
+
return ""
|
|
195
|
+
compact = " ".join(text.split())
|
|
196
|
+
if len(compact) <= max_chars:
|
|
197
|
+
return compact
|
|
198
|
+
if max_chars <= 3:
|
|
199
|
+
return compact[:max_chars]
|
|
200
|
+
return f"{compact[:max_chars - 3]}..."
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def render_markdown(result: Dict[str, object], max_body_chars: int, include_resolved: bool) -> None:
|
|
204
|
+
pull_request = result["pull_request"]
|
|
205
|
+
threads: List[Dict[str, object]] = result["threads"]
|
|
206
|
+
unresolved_count = result["unresolved_count"]
|
|
207
|
+
|
|
208
|
+
print(f"# PR #{pull_request['number']}: {pull_request['title']}")
|
|
209
|
+
print(pull_request["url"])
|
|
210
|
+
print()
|
|
211
|
+
print(f"- Total review threads: {len(threads)}")
|
|
212
|
+
print(f"- Unresolved review threads: {unresolved_count}")
|
|
213
|
+
print()
|
|
214
|
+
|
|
215
|
+
if not threads:
|
|
216
|
+
print("No review threads were found.")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
visible_threads = threads if include_resolved else [t for t in threads if not t["is_resolved"]]
|
|
220
|
+
for index, thread in enumerate(visible_threads, start=1):
|
|
221
|
+
location = thread["path"] or "(no file path)"
|
|
222
|
+
line = thread["line"] or thread["start_line"] or thread["original_line"] or "?"
|
|
223
|
+
latest = thread["latest_comment"] or {}
|
|
224
|
+
latest_author = latest.get("author") or "unknown"
|
|
225
|
+
latest_body = shorten(latest.get("body"), max_body_chars)
|
|
226
|
+
latest_url = latest.get("url") or ""
|
|
227
|
+
|
|
228
|
+
print(f"## Thread {index}")
|
|
229
|
+
print(f"- ID: {thread['id']}")
|
|
230
|
+
print(f"- Resolved: {thread['is_resolved']}")
|
|
231
|
+
print(f"- Outdated: {thread['is_outdated']}")
|
|
232
|
+
print(f"- Location: {location}:{line}")
|
|
233
|
+
print(f"- Latest comment author: {latest_author}")
|
|
234
|
+
if latest_url:
|
|
235
|
+
print(f"- Latest comment URL: {latest_url}")
|
|
236
|
+
if latest_body:
|
|
237
|
+
print(f"- Latest comment body: {latest_body}")
|
|
238
|
+
print(f"- Comment count: {len(thread['comments'])}")
|
|
239
|
+
print()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main() -> int:
|
|
243
|
+
parser = argparse.ArgumentParser(
|
|
244
|
+
description="Fetch PR review threads via GitHub GraphQL and list unresolved conversations."
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"pr",
|
|
248
|
+
help="PR URL (https://github.com/<owner>/<repo>/pull/<number>) or <owner>/<repo>#<number>",
|
|
249
|
+
)
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--token-env",
|
|
252
|
+
default="GH_TOKEN,GITHUB_TOKEN",
|
|
253
|
+
help="Comma-separated env vars to check for a GitHub token (default: GH_TOKEN,GITHUB_TOKEN).",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--format",
|
|
257
|
+
choices=("markdown", "json"),
|
|
258
|
+
default="markdown",
|
|
259
|
+
help="Output format (default: markdown).",
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
"--include-resolved",
|
|
263
|
+
action="store_true",
|
|
264
|
+
help="Include resolved threads in output.",
|
|
265
|
+
)
|
|
266
|
+
parser.add_argument(
|
|
267
|
+
"--max-body-chars",
|
|
268
|
+
type=int,
|
|
269
|
+
default=800,
|
|
270
|
+
help="Maximum latest-comment body length in markdown output.",
|
|
271
|
+
)
|
|
272
|
+
args = parser.parse_args()
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
owner, repo, number = parse_pr(args.pr)
|
|
276
|
+
token = resolve_token(args.token_env)
|
|
277
|
+
pull_request, raw_threads = fetch_review_threads(owner, repo, number, token)
|
|
278
|
+
threads = [normalize_thread(thread) for thread in raw_threads]
|
|
279
|
+
unresolved_count = sum(1 for thread in threads if not thread["is_resolved"])
|
|
280
|
+
|
|
281
|
+
result = {
|
|
282
|
+
"pull_request": {
|
|
283
|
+
"owner": owner,
|
|
284
|
+
"repo": repo,
|
|
285
|
+
"number": pull_request.get("number"),
|
|
286
|
+
"title": pull_request.get("title"),
|
|
287
|
+
"url": pull_request.get("url"),
|
|
288
|
+
},
|
|
289
|
+
"unresolved_count": unresolved_count,
|
|
290
|
+
"threads": threads,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if args.format == "json":
|
|
294
|
+
if not args.include_resolved:
|
|
295
|
+
result["threads"] = [thread for thread in threads if not thread["is_resolved"]]
|
|
296
|
+
print(json.dumps(result, indent=2))
|
|
297
|
+
return 0
|
|
298
|
+
|
|
299
|
+
render_markdown(result, args.max_body_chars, args.include_resolved)
|
|
300
|
+
return 0
|
|
301
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
302
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
303
|
+
return 1
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
sys.exit(main())
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pull request
|
|
3
|
+
description: Generates well-structured GitHub pull request descriptions for Shopify theme development teams by analyzing git diffs and gathering context. Use this skill whenever a developer asks to create a PR, open a pull request, write a PR description, or submit code for review — even if they just say "make a PR" or "create a pull request". Also trigger when someone mentions needing a PR title, PR summary, or PR testing steps.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PR Description Generator
|
|
7
|
+
|
|
8
|
+
Helps Shopify theme developers create well-structured pull request descriptions by analyzing code changes and collecting context. Follow these steps in order.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Step 1: Get Branch Info
|
|
13
|
+
|
|
14
|
+
Ask the developer for:
|
|
15
|
+
1. **Source branch** — the feature/fix branch with their changes
|
|
16
|
+
2. **Target branch** — the branch to merge into (e.g. `main`, `develop`, `release/x.x`)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 2: Analyze the Diff
|
|
21
|
+
|
|
22
|
+
Run this command to fetch and diff the branches:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git fetch origin && git diff origin/{target_branch}...origin/{source_branch}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Analyze the diff for:
|
|
29
|
+
- Files changed, added, or deleted
|
|
30
|
+
- Nature of changes (feature, bug fix, refactor, styling, etc.)
|
|
31
|
+
- Key implementation details and patterns
|
|
32
|
+
- Potential impacts or side effects
|
|
33
|
+
- Testable and user-facing changes
|
|
34
|
+
|
|
35
|
+
### Shopify-Specific Analysis Guide
|
|
36
|
+
|
|
37
|
+
| File Type | What to Look For | Testing Implications |
|
|
38
|
+
|-----------|-----------------|----------------------|
|
|
39
|
+
| `.liquid` sections/blocks | Schema changes, new settings, render tags, conditionals | Test each new setting; test conditional states |
|
|
40
|
+
| `.liquid` snippets | Parameters, usage contexts | Test in all contexts where snippet is used |
|
|
41
|
+
| `.js` files | Event listeners, DOM manipulation, API calls | Test interactions; test error states |
|
|
42
|
+
| `.css/.scss` files | Media queries, custom properties, animations | Test across breakpoints; test reduced motion |
|
|
43
|
+
| `locales/*.json` | New translation keys | Verify translations appear correctly |
|
|
44
|
+
| `config/*.json` | Theme settings | Test global settings impact |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Step 3: Generate PR Content
|
|
49
|
+
|
|
50
|
+
Based on the diff, generate:
|
|
51
|
+
|
|
52
|
+
### PR Title
|
|
53
|
+
Concise and descriptive. Use conventional commit style where appropriate (`feat:`, `fix:`, `refactor:`, `chore:`).
|
|
54
|
+
|
|
55
|
+
### PR Summary
|
|
56
|
+
1–2 sentences in plain, non-technical language describing what the changes accomplish from a user/business perspective.
|
|
57
|
+
|
|
58
|
+
### Approach
|
|
59
|
+
Technical description covering:
|
|
60
|
+
- Key files modified and why
|
|
61
|
+
- Implementation strategy
|
|
62
|
+
- Notable patterns or techniques
|
|
63
|
+
- Trade-offs or decisions made
|
|
64
|
+
|
|
65
|
+
### Testing Steps
|
|
66
|
+
Generate comprehensive, grouped testing steps as checkboxes. Cover all relevant categories:
|
|
67
|
+
|
|
68
|
+
**Functional Testing**
|
|
69
|
+
- New features or modified behavior
|
|
70
|
+
- Form inputs, buttons, interactive elements
|
|
71
|
+
- Conditional logic and different states
|
|
72
|
+
|
|
73
|
+
**Visual/UI Testing**
|
|
74
|
+
- Layout across breakpoints (mobile, tablet, desktop)
|
|
75
|
+
- New or modified CSS
|
|
76
|
+
- Section/block appearance in theme editor
|
|
77
|
+
- Hover states, transitions, animations
|
|
78
|
+
|
|
79
|
+
**Theme Editor Testing**
|
|
80
|
+
- New settings in section/block schemas
|
|
81
|
+
- Setting validations and default values
|
|
82
|
+
- Live preview updates
|
|
83
|
+
- Block add/remove/reorder
|
|
84
|
+
|
|
85
|
+
**Edge Cases**
|
|
86
|
+
- Empty states (no content, no images)
|
|
87
|
+
- Maximum content (long text, many items)
|
|
88
|
+
- Missing or broken assets
|
|
89
|
+
|
|
90
|
+
**Accessibility**
|
|
91
|
+
- Keyboard navigation
|
|
92
|
+
- Screen reader compatibility
|
|
93
|
+
- Focus states and tab order
|
|
94
|
+
- Color contrast
|
|
95
|
+
|
|
96
|
+
**Browser/Device**
|
|
97
|
+
- Cross-browser compatibility
|
|
98
|
+
- Mobile touch interactions
|
|
99
|
+
|
|
100
|
+
Format example:
|
|
101
|
+
```markdown
|
|
102
|
+
**Feature Functionality**
|
|
103
|
+
- [ ] Verify the "Show price" toggle appears in section settings
|
|
104
|
+
- [ ] Confirm price displays when toggle is enabled
|
|
105
|
+
- [ ] Confirm price is hidden when toggle is disabled
|
|
106
|
+
|
|
107
|
+
**Responsive Behavior**
|
|
108
|
+
- [ ] Test on mobile (< 768px) — items should stack vertically
|
|
109
|
+
- [ ] Test on desktop (≥ 768px) — items should display in a grid
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Step 4: Collect Additional Info
|
|
115
|
+
|
|
116
|
+
Ask the developer for the following (cannot be inferred from code):
|
|
117
|
+
|
|
118
|
+
1. **Basecamp Links** — Links to relevant Basecamp Cards or Todos
|
|
119
|
+
2. **Other Considerations** — Edge cases, known limitations, notes for reviewers
|
|
120
|
+
3. **Demo Links** — Shopify store preview URL and theme editor/customizer URL
|
|
121
|
+
4. **Additional Information** — Any other relevant context
|
|
122
|
+
|
|
123
|
+
Then present the generated testing steps and ask:
|
|
124
|
+
- Any steps to remove (not applicable)?
|
|
125
|
+
- Any additional scenarios to add?
|
|
126
|
+
- Any specific test data or configuration needed?
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Step 5: Review & Confirm
|
|
131
|
+
|
|
132
|
+
Present the complete PR description to the developer for final review. Ask them to confirm it's accurate or request changes.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Step 6: Confirm PR Type
|
|
137
|
+
|
|
138
|
+
Ask the developer to confirm the project type, as this determines the reviewer(s):
|
|
139
|
+
|
|
140
|
+
| PR Type | Reviewer(s) |
|
|
141
|
+
|---------|------------|
|
|
142
|
+
| **Retainer** | `@matrjohnson` |
|
|
143
|
+
| **Statamic** | `@matrjohnson` (use admin + preview links) |
|
|
144
|
+
| **Project** | `@andisadiazl` |
|
|
145
|
+
| **Component Library** | `@matrjohnson`, `@andisadiazl` |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Step 7: Create the Pull Request
|
|
150
|
+
|
|
151
|
+
Once confirmed, create the PR with GitHub CLI:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
gh pr create \
|
|
155
|
+
--base "{target_branch}" \
|
|
156
|
+
--head "{source_branch}" \
|
|
157
|
+
--title "{auto_generated_title}" \
|
|
158
|
+
--body "{generated_pr_body}" \
|
|
159
|
+
--reviewer "{reviewers}"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- **On success**: Share the PR URL with the developer.
|
|
163
|
+
- **On failure**: Display the error and troubleshoot:
|
|
164
|
+
- Not authenticated → `gh auth status` / `gh auth login`
|
|
165
|
+
- Branch not pushed → `git push -u origin {source_branch}`
|
|
166
|
+
- No commits between branches → inform developer
|
|
167
|
+
- PR already exists → provide link to existing PR
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## PR Body Template
|
|
172
|
+
|
|
173
|
+
Use this exact format for the `--body` parameter:
|
|
174
|
+
|
|
175
|
+
```markdown
|
|
176
|
+
### PR Summary:
|
|
177
|
+
{auto_generated_summary}
|
|
178
|
+
|
|
179
|
+
### Tasks Included?
|
|
180
|
+
{basecamp_links}
|
|
181
|
+
|
|
182
|
+
### What approach did you take?
|
|
183
|
+
{auto_generated_approach}
|
|
184
|
+
|
|
185
|
+
### Other considerations
|
|
186
|
+
{considerations}
|
|
187
|
+
|
|
188
|
+
### Testing steps/scenarios
|
|
189
|
+
{auto_generated_and_reviewed_testing_steps_as_checkboxes}
|
|
190
|
+
|
|
191
|
+
### Demo links
|
|
192
|
+
- [Store]({store_url})
|
|
193
|
+
- [Editor]({editor_url})
|
|
194
|
+
|
|
195
|
+
### Checklist
|
|
196
|
+
- [ ] Followed [theme code principles](https://github.com/Shopify/dawn/blob/main/.github/CONTRIBUTING.md#theme-code-principles)
|
|
197
|
+
- [ ] Linted with [Theme Check](https://github.com/Shopify/theme-check)
|
|
198
|
+
- [ ] Tested on [mobile](https://shopify.dev/themes/store/requirements#mobile-browser-requirements)
|
|
199
|
+
- [ ] Tested on [multiple browsers](https://shopify.dev/themes/store/requirements#desktop-browser-requirements)
|
|
200
|
+
- [ ] Tested for [accessibility](https://shopify.dev/themes/best-practices/accessibility)
|
|
201
|
+
|
|
202
|
+
### Additional Information
|
|
203
|
+
{additional_info}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Workflow Summary
|
|
209
|
+
|
|
210
|
+
1. Ask for source and target branches
|
|
211
|
+
2. Fetch and analyze the diff
|
|
212
|
+
3. Generate title, summary, approach, and testing steps
|
|
213
|
+
4. Collect Basecamp links, demo URLs, and other info; review testing steps
|
|
214
|
+
5. Present complete PR for final confirmation
|
|
215
|
+
6. Confirm PR type and assign reviewer(s)
|
|
216
|
+
7. Create the PR with `gh pr create` and share the URL
|