@laitszkin/apollo-toolkit 2.0.0

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.
Files changed (204) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +100 -0
  3. package/LICENSE +21 -0
  4. package/README.md +144 -0
  5. package/align-project-documents/SKILL.md +94 -0
  6. package/align-project-documents/agents/openai.yaml +4 -0
  7. package/analyse-app-logs/LICENSE +21 -0
  8. package/analyse-app-logs/README.md +126 -0
  9. package/analyse-app-logs/SKILL.md +121 -0
  10. package/analyse-app-logs/agents/openai.yaml +4 -0
  11. package/analyse-app-logs/references/investigation-checklist.md +58 -0
  12. package/analyse-app-logs/references/log-signal-patterns.md +52 -0
  13. package/answering-questions-with-research/SKILL.md +46 -0
  14. package/answering-questions-with-research/agents/openai.yaml +4 -0
  15. package/bin/apollo-toolkit.js +7 -0
  16. package/commit-and-push/LICENSE +21 -0
  17. package/commit-and-push/README.md +26 -0
  18. package/commit-and-push/SKILL.md +70 -0
  19. package/commit-and-push/agents/openai.yaml +4 -0
  20. package/commit-and-push/references/branch-naming.md +15 -0
  21. package/commit-and-push/references/commit-messages.md +19 -0
  22. package/deep-research-topics/LICENSE +21 -0
  23. package/deep-research-topics/README.md +43 -0
  24. package/deep-research-topics/SKILL.md +84 -0
  25. package/deep-research-topics/agents/openai.yaml +4 -0
  26. package/develop-new-features/LICENSE +21 -0
  27. package/develop-new-features/README.md +52 -0
  28. package/develop-new-features/SKILL.md +105 -0
  29. package/develop-new-features/agents/openai.yaml +4 -0
  30. package/develop-new-features/references/testing-e2e.md +35 -0
  31. package/develop-new-features/references/testing-integration.md +42 -0
  32. package/develop-new-features/references/testing-property-based.md +44 -0
  33. package/develop-new-features/references/testing-unit.md +37 -0
  34. package/discover-edge-cases/CHANGELOG.md +19 -0
  35. package/discover-edge-cases/LICENSE +21 -0
  36. package/discover-edge-cases/README.md +87 -0
  37. package/discover-edge-cases/SKILL.md +124 -0
  38. package/discover-edge-cases/agents/openai.yaml +4 -0
  39. package/discover-edge-cases/references/architecture-edge-cases.md +41 -0
  40. package/discover-edge-cases/references/code-edge-cases.md +46 -0
  41. package/docs-to-voice/.env.example +106 -0
  42. package/docs-to-voice/CHANGELOG.md +71 -0
  43. package/docs-to-voice/LICENSE +21 -0
  44. package/docs-to-voice/README.md +118 -0
  45. package/docs-to-voice/SKILL.md +107 -0
  46. package/docs-to-voice/agents/openai.yaml +4 -0
  47. package/docs-to-voice/scripts/docs_to_voice.py +1385 -0
  48. package/docs-to-voice/scripts/docs_to_voice.sh +11 -0
  49. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +210 -0
  50. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +115 -0
  51. package/docs-to-voice/tests/test_docs_to_voice_settings.py +43 -0
  52. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +57 -0
  53. package/enhance-existing-features/CHANGELOG.md +35 -0
  54. package/enhance-existing-features/LICENSE +21 -0
  55. package/enhance-existing-features/README.md +54 -0
  56. package/enhance-existing-features/SKILL.md +120 -0
  57. package/enhance-existing-features/agents/openai.yaml +4 -0
  58. package/enhance-existing-features/references/e2e-tests.md +25 -0
  59. package/enhance-existing-features/references/integration-tests.md +30 -0
  60. package/enhance-existing-features/references/property-based-tests.md +33 -0
  61. package/enhance-existing-features/references/unit-tests.md +29 -0
  62. package/feature-propose/LICENSE +21 -0
  63. package/feature-propose/README.md +23 -0
  64. package/feature-propose/SKILL.md +107 -0
  65. package/feature-propose/agents/openai.yaml +4 -0
  66. package/feature-propose/references/enhancement-features.md +25 -0
  67. package/feature-propose/references/important-features.md +25 -0
  68. package/feature-propose/references/mvp-features.md +25 -0
  69. package/feature-propose/references/performance-features.md +25 -0
  70. package/financial-research/SKILL.md +208 -0
  71. package/financial-research/agents/openai.yaml +4 -0
  72. package/financial-research/assets/weekly_market_report_template.md +45 -0
  73. package/fix-github-issues/SKILL.md +98 -0
  74. package/fix-github-issues/agents/openai.yaml +4 -0
  75. package/fix-github-issues/scripts/list_issues.py +148 -0
  76. package/fix-github-issues/tests/test_list_issues.py +127 -0
  77. package/generate-spec/LICENSE +21 -0
  78. package/generate-spec/README.md +61 -0
  79. package/generate-spec/SKILL.md +96 -0
  80. package/generate-spec/agents/openai.yaml +4 -0
  81. package/generate-spec/references/templates/checklist.md +78 -0
  82. package/generate-spec/references/templates/spec.md +55 -0
  83. package/generate-spec/references/templates/tasks.md +35 -0
  84. package/generate-spec/scripts/create-specs +123 -0
  85. package/harden-app-security/CHANGELOG.md +27 -0
  86. package/harden-app-security/LICENSE +21 -0
  87. package/harden-app-security/README.md +46 -0
  88. package/harden-app-security/SKILL.md +127 -0
  89. package/harden-app-security/agents/openai.yaml +4 -0
  90. package/harden-app-security/references/agent-attack-catalog.md +117 -0
  91. package/harden-app-security/references/common-software-attack-catalog.md +168 -0
  92. package/harden-app-security/references/red-team-extreme-scenarios.md +81 -0
  93. package/harden-app-security/references/risk-checklist.md +78 -0
  94. package/harden-app-security/references/security-test-patterns-agent.md +101 -0
  95. package/harden-app-security/references/security-test-patterns-finance.md +88 -0
  96. package/harden-app-security/references/test-snippets.md +73 -0
  97. package/improve-observability/SKILL.md +114 -0
  98. package/improve-observability/agents/openai.yaml +4 -0
  99. package/learn-skill-from-conversations/CHANGELOG.md +15 -0
  100. package/learn-skill-from-conversations/LICENSE +22 -0
  101. package/learn-skill-from-conversations/README.md +47 -0
  102. package/learn-skill-from-conversations/SKILL.md +85 -0
  103. package/learn-skill-from-conversations/agents/openai.yaml +4 -0
  104. package/learn-skill-from-conversations/scripts/extract_recent_conversations.py +369 -0
  105. package/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +176 -0
  106. package/learning-error-book/SKILL.md +112 -0
  107. package/learning-error-book/agents/openai.yaml +4 -0
  108. package/learning-error-book/assets/error_book_template.md +66 -0
  109. package/learning-error-book/scripts/render_markdown_to_pdf.py +367 -0
  110. package/lib/cli.js +338 -0
  111. package/lib/installer.js +225 -0
  112. package/maintain-project-constraints/SKILL.md +109 -0
  113. package/maintain-project-constraints/agents/openai.yaml +4 -0
  114. package/maintain-skill-catalog/README.md +18 -0
  115. package/maintain-skill-catalog/SKILL.md +66 -0
  116. package/maintain-skill-catalog/agents/openai.yaml +4 -0
  117. package/novel-to-short-video/CHANGELOG.md +53 -0
  118. package/novel-to-short-video/LICENSE +21 -0
  119. package/novel-to-short-video/README.md +63 -0
  120. package/novel-to-short-video/SKILL.md +233 -0
  121. package/novel-to-short-video/agents/openai.yaml +4 -0
  122. package/novel-to-short-video/references/plan-template.md +71 -0
  123. package/novel-to-short-video/references/roles-json.md +41 -0
  124. package/open-github-issue/LICENSE +21 -0
  125. package/open-github-issue/README.md +97 -0
  126. package/open-github-issue/SKILL.md +119 -0
  127. package/open-github-issue/agents/openai.yaml +4 -0
  128. package/open-github-issue/scripts/open_github_issue.py +380 -0
  129. package/open-github-issue/tests/test_open_github_issue.py +159 -0
  130. package/open-source-pr-workflow/CHANGELOG.md +32 -0
  131. package/open-source-pr-workflow/LICENSE +21 -0
  132. package/open-source-pr-workflow/README.md +23 -0
  133. package/open-source-pr-workflow/SKILL.md +123 -0
  134. package/open-source-pr-workflow/agents/openai.yaml +4 -0
  135. package/openai-text-to-image-storyboard/.env.example +10 -0
  136. package/openai-text-to-image-storyboard/CHANGELOG.md +49 -0
  137. package/openai-text-to-image-storyboard/LICENSE +21 -0
  138. package/openai-text-to-image-storyboard/README.md +99 -0
  139. package/openai-text-to-image-storyboard/SKILL.md +107 -0
  140. package/openai-text-to-image-storyboard/agents/openai.yaml +4 -0
  141. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +763 -0
  142. package/package.json +36 -0
  143. package/record-spending/SKILL.md +113 -0
  144. package/record-spending/agents/openai.yaml +4 -0
  145. package/record-spending/references/account-format.md +33 -0
  146. package/record-spending/references/workbook-layout.md +84 -0
  147. package/resolve-review-comments/SKILL.md +122 -0
  148. package/resolve-review-comments/agents/openai.yaml +4 -0
  149. package/resolve-review-comments/references/adoption-criteria.md +23 -0
  150. package/resolve-review-comments/scripts/review_threads.py +425 -0
  151. package/resolve-review-comments/tests/test_review_threads.py +74 -0
  152. package/review-change-set/LICENSE +21 -0
  153. package/review-change-set/README.md +55 -0
  154. package/review-change-set/SKILL.md +103 -0
  155. package/review-change-set/agents/openai.yaml +4 -0
  156. package/review-codebases/LICENSE +21 -0
  157. package/review-codebases/README.md +67 -0
  158. package/review-codebases/SKILL.md +109 -0
  159. package/review-codebases/agents/openai.yaml +4 -0
  160. package/scripts/install_skills.ps1 +283 -0
  161. package/scripts/install_skills.sh +262 -0
  162. package/scripts/validate_openai_agent_config.py +194 -0
  163. package/scripts/validate_skill_frontmatter.py +110 -0
  164. package/specs-to-project-docs/LICENSE +21 -0
  165. package/specs-to-project-docs/README.md +57 -0
  166. package/specs-to-project-docs/SKILL.md +111 -0
  167. package/specs-to-project-docs/agents/openai.yaml +4 -0
  168. package/specs-to-project-docs/references/templates/architecture.md +29 -0
  169. package/specs-to-project-docs/references/templates/configuration.md +29 -0
  170. package/specs-to-project-docs/references/templates/developer-guide.md +33 -0
  171. package/specs-to-project-docs/references/templates/docs-index.md +39 -0
  172. package/specs-to-project-docs/references/templates/features.md +25 -0
  173. package/specs-to-project-docs/references/templates/getting-started.md +38 -0
  174. package/specs-to-project-docs/references/templates/readme.md +49 -0
  175. package/systematic-debug/LICENSE +21 -0
  176. package/systematic-debug/README.md +81 -0
  177. package/systematic-debug/SKILL.md +59 -0
  178. package/systematic-debug/agents/openai.yaml +4 -0
  179. package/text-to-short-video/.env.example +36 -0
  180. package/text-to-short-video/LICENSE +21 -0
  181. package/text-to-short-video/README.md +82 -0
  182. package/text-to-short-video/SKILL.md +221 -0
  183. package/text-to-short-video/agents/openai.yaml +4 -0
  184. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +350 -0
  185. package/version-release/CHANGELOG.md +53 -0
  186. package/version-release/LICENSE +21 -0
  187. package/version-release/README.md +28 -0
  188. package/version-release/SKILL.md +94 -0
  189. package/version-release/agents/openai.yaml +4 -0
  190. package/version-release/references/branch-naming.md +15 -0
  191. package/version-release/references/changelog-writing.md +8 -0
  192. package/version-release/references/commit-messages.md +19 -0
  193. package/version-release/references/readme-writing.md +12 -0
  194. package/version-release/references/semantic-versioning.md +12 -0
  195. package/video-production/CHANGELOG.md +104 -0
  196. package/video-production/LICENSE +18 -0
  197. package/video-production/README.md +68 -0
  198. package/video-production/SKILL.md +213 -0
  199. package/video-production/agents/openai.yaml +4 -0
  200. package/video-production/references/plan-template.md +54 -0
  201. package/video-production/references/roles-json.md +41 -0
  202. package/weekly-financial-event-report/SKILL.md +195 -0
  203. package/weekly-financial-event-report/agents/openai.yaml +4 -0
  204. package/weekly-financial-event-report/assets/financial_event_report_template.md +53 -0
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ LIST_QUERY = """
12
+ query($owner: String!, $name: String!, $number: Int!, $after: String) {
13
+ repository(owner: $owner, name: $name) {
14
+ pullRequest(number: $number) {
15
+ reviewThreads(first: 100, after: $after) {
16
+ nodes {
17
+ id
18
+ isResolved
19
+ isOutdated
20
+ path
21
+ line
22
+ startLine
23
+ comments(first: 20) {
24
+ nodes {
25
+ id
26
+ url
27
+ body
28
+ author {
29
+ login
30
+ }
31
+ createdAt
32
+ path
33
+ line
34
+ outdated
35
+ }
36
+ }
37
+ }
38
+ pageInfo {
39
+ hasNextPage
40
+ endCursor
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ """
47
+
48
+ RESOLVE_MUTATION = """
49
+ mutation($threadId: ID!) {
50
+ resolveReviewThread(input: {threadId: $threadId}) {
51
+ thread {
52
+ id
53
+ isResolved
54
+ }
55
+ }
56
+ }
57
+ """
58
+
59
+
60
+ def parse_args() -> argparse.Namespace:
61
+ parser = argparse.ArgumentParser(
62
+ description="List and resolve GitHub PR review threads via gh graphql."
63
+ )
64
+ subparsers = parser.add_subparsers(dest="command", required=True)
65
+
66
+ list_parser = subparsers.add_parser("list", help="List review threads.")
67
+ add_common_args(list_parser)
68
+ list_parser.add_argument(
69
+ "--state",
70
+ choices=["unresolved", "resolved", "all"],
71
+ default="unresolved",
72
+ help="Thread state filter.",
73
+ )
74
+ list_parser.add_argument(
75
+ "--output",
76
+ choices=["table", "json"],
77
+ default="table",
78
+ help="Output format.",
79
+ )
80
+
81
+ resolve_parser = subparsers.add_parser("resolve", help="Resolve selected threads.")
82
+ add_common_args(resolve_parser)
83
+ resolve_parser.add_argument(
84
+ "--thread-id",
85
+ action="append",
86
+ default=[],
87
+ help="Thread GraphQL ID to resolve (repeatable).",
88
+ )
89
+ resolve_parser.add_argument(
90
+ "--thread-id-file",
91
+ help="Path to JSON file containing thread IDs.",
92
+ )
93
+ resolve_parser.add_argument(
94
+ "--all-unresolved",
95
+ action="store_true",
96
+ help="Resolve every unresolved thread in the PR.",
97
+ )
98
+ resolve_parser.add_argument(
99
+ "--dry-run",
100
+ action="store_true",
101
+ help="Print thread IDs without resolving.",
102
+ )
103
+
104
+ return parser.parse_args()
105
+
106
+
107
+ def add_common_args(parser: argparse.ArgumentParser) -> None:
108
+ parser.add_argument("--repo", help="Target repository in owner/name format.")
109
+ parser.add_argument("--pr", type=positive_int, help="Pull request number.")
110
+
111
+
112
+ def positive_int(raw: str) -> int:
113
+ value = int(raw)
114
+ if value <= 0:
115
+ raise argparse.ArgumentTypeError("value must be a positive integer")
116
+ return value
117
+
118
+
119
+ def run_gh(cmd: list[str], expect_json: bool = False) -> Any:
120
+ try:
121
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
122
+ except FileNotFoundError as exc:
123
+ raise RuntimeError("gh CLI is not installed or not in PATH") from exc
124
+ except subprocess.CalledProcessError as exc:
125
+ stderr = exc.stderr.strip() or "gh command failed"
126
+ raise RuntimeError(stderr) from exc
127
+
128
+ if not expect_json:
129
+ return result.stdout.strip()
130
+
131
+ try:
132
+ return json.loads(result.stdout)
133
+ except json.JSONDecodeError as exc:
134
+ raise RuntimeError("Failed to parse gh JSON output") from exc
135
+
136
+
137
+ def parse_owner_repo(repo: str) -> tuple[str, str]:
138
+ parts = repo.split("/")
139
+ if len(parts) != 2 or not parts[0] or not parts[1]:
140
+ raise ValueError("repo must be in owner/name format")
141
+ return parts[0], parts[1]
142
+
143
+
144
+ def resolve_repo(repo: str | None) -> str:
145
+ if repo:
146
+ parse_owner_repo(repo)
147
+ return repo
148
+
149
+ return run_gh(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])
150
+
151
+
152
+ def resolve_pr_number(repo: str, pr: int | None) -> int:
153
+ if pr is not None:
154
+ return pr
155
+
156
+ value = run_gh(["gh", "pr", "view", "--repo", repo, "--json", "number", "--jq", ".number"])
157
+ try:
158
+ return int(value)
159
+ except ValueError as exc:
160
+ raise RuntimeError("Unable to infer PR number from current branch context") from exc
161
+
162
+
163
+ def gh_graphql(query: str, variables: dict[str, Any]) -> dict[str, Any]:
164
+ cmd = ["gh", "api", "graphql", "-f", f"query={query}"]
165
+ for key, value in variables.items():
166
+ cmd.extend(["-F", f"{key}={json.dumps(value)}"])
167
+ return run_gh(cmd, expect_json=True)
168
+
169
+
170
+ def fetch_review_threads(repo: str, pr_number: int) -> list[dict[str, Any]]:
171
+ owner, name = parse_owner_repo(repo)
172
+ threads: list[dict[str, Any]] = []
173
+ after: str | None = None
174
+
175
+ while True:
176
+ payload = gh_graphql(
177
+ LIST_QUERY,
178
+ {
179
+ "owner": owner,
180
+ "name": name,
181
+ "number": pr_number,
182
+ "after": after,
183
+ },
184
+ )
185
+ pr = payload["data"]["repository"]["pullRequest"]
186
+ if pr is None:
187
+ raise RuntimeError(f"PR #{pr_number} not found in {repo}")
188
+
189
+ review_threads = pr["reviewThreads"]
190
+ threads.extend(review_threads.get("nodes", []))
191
+
192
+ page_info = review_threads["pageInfo"]
193
+ if not page_info.get("hasNextPage"):
194
+ break
195
+ after = page_info.get("endCursor")
196
+
197
+ return threads
198
+
199
+
200
+ def filter_threads(threads: list[dict[str, Any]], state: str) -> list[dict[str, Any]]:
201
+ if state == "all":
202
+ return threads
203
+ if state == "resolved":
204
+ return [item for item in threads if item.get("isResolved")]
205
+ return [item for item in threads if not item.get("isResolved")]
206
+
207
+
208
+ def normalize_thread(thread: dict[str, Any]) -> dict[str, Any]:
209
+ comments = thread.get("comments", {}).get("nodes", [])
210
+ normalized_comments = [
211
+ {
212
+ "id": comment.get("id"),
213
+ "url": comment.get("url"),
214
+ "author": (comment.get("author") or {}).get("login"),
215
+ "body": comment.get("body", ""),
216
+ "created_at": comment.get("createdAt"),
217
+ "path": comment.get("path"),
218
+ "line": comment.get("line"),
219
+ "outdated": comment.get("outdated"),
220
+ }
221
+ for comment in comments
222
+ ]
223
+
224
+ return {
225
+ "thread_id": thread.get("id"),
226
+ "is_resolved": thread.get("isResolved"),
227
+ "is_outdated": thread.get("isOutdated"),
228
+ "path": thread.get("path"),
229
+ "line": thread.get("line"),
230
+ "start_line": thread.get("startLine"),
231
+ "comments": normalized_comments,
232
+ }
233
+
234
+
235
+ def truncate(text: str, width: int) -> str:
236
+ if len(text) <= width:
237
+ return text
238
+ if width <= 3:
239
+ return text[:width]
240
+ return text[: width - 3] + "..."
241
+
242
+
243
+ def preview_body(thread: dict[str, Any]) -> str:
244
+ comments = thread.get("comments", [])
245
+ if not comments:
246
+ return "-"
247
+ body = comments[0].get("body", "").replace("\n", " ").strip()
248
+ return truncate(body or "-", 72)
249
+
250
+
251
+ def render_location(thread: dict[str, Any]) -> str:
252
+ path = thread.get("path") or "-"
253
+ line = thread.get("line")
254
+ if line is None:
255
+ return path
256
+ return f"{path}:{line}"
257
+
258
+
259
+ def print_table(threads: list[dict[str, Any]]) -> None:
260
+ widths = {
261
+ "idx": 4,
262
+ "thread": 12,
263
+ "location": 36,
264
+ "author": 18,
265
+ "preview": 72,
266
+ }
267
+ header = (
268
+ f"{'#':<{widths['idx']}} "
269
+ f"{'THREAD_ID':<{widths['thread']}} "
270
+ f"{'LOCATION':<{widths['location']}} "
271
+ f"{'AUTHOR':<{widths['author']}} "
272
+ f"{'COMMENT_PREVIEW':<{widths['preview']}}"
273
+ )
274
+ print(header)
275
+ print("-" * len(header))
276
+
277
+ for idx, thread in enumerate(threads, start=1):
278
+ comments = thread.get("comments", [])
279
+ author = comments[0].get("author") if comments else "-"
280
+ row = (
281
+ f"{idx:<{widths['idx']}} "
282
+ f"{truncate(thread.get('thread_id', '-') or '-', widths['thread']):<{widths['thread']}} "
283
+ f"{truncate(render_location(thread), widths['location']):<{widths['location']}} "
284
+ f"{truncate(author or '-', widths['author']):<{widths['author']}} "
285
+ f"{preview_body(thread):<{widths['preview']}}"
286
+ )
287
+ print(row)
288
+
289
+
290
+ def load_thread_ids(path: str) -> list[str]:
291
+ raw = Path(path).read_text(encoding="utf-8")
292
+ payload = json.loads(raw)
293
+
294
+ if isinstance(payload, list):
295
+ ids = payload
296
+ elif isinstance(payload, dict):
297
+ if "thread_ids" in payload:
298
+ ids = payload["thread_ids"]
299
+ elif "adopted_thread_ids" in payload:
300
+ ids = payload["adopted_thread_ids"]
301
+ elif "threads" in payload:
302
+ ids = [
303
+ item.get("thread_id")
304
+ for item in payload["threads"]
305
+ if isinstance(item, dict)
306
+ ]
307
+ else:
308
+ raise ValueError("JSON must include thread_ids, adopted_thread_ids, or threads")
309
+ else:
310
+ raise ValueError("Unsupported JSON payload for thread IDs")
311
+
312
+ output = [item for item in ids if isinstance(item, str) and item.strip()]
313
+ return list(dict.fromkeys(output))
314
+
315
+
316
+ def collect_thread_ids(args: argparse.Namespace, unresolved_threads: list[dict[str, Any]]) -> list[str]:
317
+ ids: list[str] = []
318
+
319
+ if args.all_unresolved:
320
+ ids.extend([item["thread_id"] for item in unresolved_threads if item.get("thread_id")])
321
+
322
+ ids.extend(args.thread_id)
323
+
324
+ if args.thread_id_file:
325
+ ids.extend(load_thread_ids(args.thread_id_file))
326
+
327
+ normalized = [item for item in ids if item]
328
+ return list(dict.fromkeys(normalized))
329
+
330
+
331
+ def resolve_threads(thread_ids: list[str], dry_run: bool) -> tuple[list[str], list[dict[str, str]]]:
332
+ resolved: list[str] = []
333
+ failed: list[dict[str, str]] = []
334
+
335
+ for thread_id in thread_ids:
336
+ if dry_run:
337
+ resolved.append(thread_id)
338
+ continue
339
+
340
+ try:
341
+ payload = gh_graphql(RESOLVE_MUTATION, {"threadId": thread_id})
342
+ thread = payload["data"]["resolveReviewThread"]["thread"]
343
+ if not thread or not thread.get("isResolved"):
344
+ raise RuntimeError("thread did not resolve")
345
+ resolved.append(thread_id)
346
+ except Exception as exc: # pylint: disable=broad-except
347
+ failed.append({"thread_id": thread_id, "error": str(exc)})
348
+
349
+ return resolved, failed
350
+
351
+
352
+ def cmd_list(args: argparse.Namespace) -> int:
353
+ repo = resolve_repo(args.repo)
354
+ pr_number = resolve_pr_number(repo, args.pr)
355
+
356
+ threads = fetch_review_threads(repo, pr_number)
357
+ filtered = filter_threads(threads, args.state)
358
+ normalized = [normalize_thread(item) for item in filtered]
359
+
360
+ result = {
361
+ "repo": repo,
362
+ "pr_number": pr_number,
363
+ "state": args.state,
364
+ "thread_count": len(normalized),
365
+ "threads": normalized,
366
+ }
367
+
368
+ if args.output == "json":
369
+ print(json.dumps(result, indent=2, ensure_ascii=False))
370
+ else:
371
+ print(f"Repository: {repo}")
372
+ print(f"PR: #{pr_number}")
373
+ print(f"Threads ({args.state}): {len(normalized)}")
374
+ print_table(normalized)
375
+
376
+ return 0
377
+
378
+
379
+ def cmd_resolve(args: argparse.Namespace) -> int:
380
+ repo = resolve_repo(args.repo)
381
+ pr_number = resolve_pr_number(repo, args.pr)
382
+
383
+ threads = fetch_review_threads(repo, pr_number)
384
+ unresolved = [normalize_thread(item) for item in filter_threads(threads, "unresolved")]
385
+ thread_ids = collect_thread_ids(args, unresolved)
386
+
387
+ if not thread_ids:
388
+ print(
389
+ "Error: no thread IDs selected. Use --thread-id, --thread-id-file, or --all-unresolved.",
390
+ file=sys.stderr,
391
+ )
392
+ return 1
393
+
394
+ resolved, failed = resolve_threads(thread_ids, args.dry_run)
395
+
396
+ summary = {
397
+ "repo": repo,
398
+ "pr_number": pr_number,
399
+ "requested": thread_ids,
400
+ "resolved": resolved,
401
+ "failed": failed,
402
+ "dry_run": args.dry_run,
403
+ }
404
+ print(json.dumps(summary, indent=2, ensure_ascii=False))
405
+
406
+ return 0 if not failed else 1
407
+
408
+
409
+ def main() -> int:
410
+ args = parse_args()
411
+
412
+ try:
413
+ if args.command == "list":
414
+ return cmd_list(args)
415
+ if args.command == "resolve":
416
+ return cmd_resolve(args)
417
+ print(f"Unsupported command: {args.command}", file=sys.stderr)
418
+ return 1
419
+ except Exception as exc: # pylint: disable=broad-except
420
+ print(f"Error: {exc}", file=sys.stderr)
421
+ return 1
422
+
423
+
424
+ if __name__ == "__main__":
425
+ sys.exit(main())
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.util
7
+ import json
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "review_threads.py"
13
+ SPEC = importlib.util.spec_from_file_location("review_threads", SCRIPT_PATH)
14
+ MODULE = importlib.util.module_from_spec(SPEC)
15
+ SPEC.loader.exec_module(MODULE)
16
+
17
+
18
+ class ReviewThreadsTests(unittest.TestCase):
19
+ def test_parse_owner_repo(self) -> None:
20
+ self.assertEqual(MODULE.parse_owner_repo("octo/repo"), ("octo", "repo"))
21
+
22
+ def test_parse_owner_repo_rejects_invalid_format(self) -> None:
23
+ with self.assertRaises(ValueError):
24
+ MODULE.parse_owner_repo("octo")
25
+
26
+ def test_parse_owner_repo_rejects_extra_segments(self) -> None:
27
+ with self.assertRaises(ValueError):
28
+ MODULE.parse_owner_repo("octo/repo/extra")
29
+
30
+ def test_load_thread_ids_supports_multiple_shapes(self) -> None:
31
+ payload = {"adopted_thread_ids": ["A", "B", "A"]}
32
+ with tempfile.TemporaryDirectory() as tmp_dir:
33
+ tmp_path = Path(tmp_dir) / "ids.json"
34
+ tmp_path.write_text(json.dumps(payload), encoding="utf-8")
35
+ ids = MODULE.load_thread_ids(str(tmp_path))
36
+
37
+ self.assertEqual(ids, ["A", "B"])
38
+
39
+ def test_collect_thread_ids_from_flags(self) -> None:
40
+ args = argparse.Namespace(
41
+ all_unresolved=True,
42
+ thread_id=["thread-2"],
43
+ thread_id_file=None,
44
+ )
45
+ unresolved = [{"thread_id": "thread-1"}, {"thread_id": "thread-2"}]
46
+
47
+ ids = MODULE.collect_thread_ids(args, unresolved)
48
+
49
+ self.assertEqual(ids, ["thread-1", "thread-2"])
50
+
51
+ def test_load_thread_ids_ignores_non_dict_thread_entries(self) -> None:
52
+ payload = {"threads": [{"thread_id": "A"}, "bad", 123, {"thread_id": "B"}]}
53
+ with tempfile.TemporaryDirectory() as tmp_dir:
54
+ tmp_path = Path(tmp_dir) / "ids.json"
55
+ tmp_path.write_text(json.dumps(payload), encoding="utf-8")
56
+ ids = MODULE.load_thread_ids(str(tmp_path))
57
+
58
+ self.assertEqual(ids, ["A", "B"])
59
+
60
+ def test_render_location_without_line(self) -> None:
61
+ self.assertEqual(MODULE.render_location({"path": "a.txt", "line": None}), "a.txt")
62
+
63
+ def test_filter_threads(self) -> None:
64
+ data = [
65
+ {"id": "a", "isResolved": True},
66
+ {"id": "b", "isResolved": False},
67
+ ]
68
+ self.assertEqual(len(MODULE.filter_threads(data, "resolved")), 1)
69
+ self.assertEqual(len(MODULE.filter_threads(data, "unresolved")), 1)
70
+ self.assertEqual(len(MODULE.filter_threads(data, "all")), 2)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ unittest.main()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lai Tsz Kin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ # review-change-set
2
+
3
+ `review-change-set` is a Codex skill for reviewing the active git diff like an independent reviewer.
4
+
5
+ ## What this skill does
6
+
7
+ This skill:
8
+
9
+ 1. Reads the current git change set end-to-end.
10
+ 2. Rejects authorship bias, including confidence in code written earlier in the same conversation.
11
+ 3. Looks for architecture-level abstraction opportunities.
12
+ 4. Looks for code that can be simplified without changing behavior.
13
+ 5. Runs `harden-app-security` as a required adversarial cross-check for code-affecting changes.
14
+
15
+ ## When to use
16
+
17
+ Use this skill when the task asks you to:
18
+
19
+ - review the current diff before commit or PR,
20
+ - find abstraction or modularization opportunities,
21
+ - look for simplification or refactor candidates,
22
+ - provide a reviewer-style second opinion on current changes.
23
+
24
+ ## Core principles
25
+
26
+ - Read the diff first, then judge.
27
+ - Treat recent edits as untrusted until evidence proves otherwise.
28
+ - Prefer fewer, stronger findings over broad speculative advice.
29
+ - Focus on architecture and simplification, not cosmetic style feedback.
30
+ - Reuse confirmed security findings from `harden-app-security` instead of hand-waving about risk.
31
+
32
+ ## Example
33
+
34
+ Prompt example:
35
+
36
+ ```text
37
+ Review the current git diff like a skeptical reviewer.
38
+ Find any architectural abstractions that should be extracted and any code that can be simplified.
39
+ Do not defend the current implementation just because it was written in this conversation.
40
+ ```
41
+
42
+ Expected behavior:
43
+
44
+ - changed files are read before conclusions,
45
+ - findings cite exact evidence,
46
+ - architecture issues are prioritized over style comments,
47
+ - security-sensitive changes are cross-checked through `harden-app-security`.
48
+
49
+ ## References
50
+
51
+ - [`SKILL.md`](./SKILL.md) - full workflow and execution rules.
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: review-change-set
3
+ description: Review the current git change set from an unbiased reviewer perspective, identify architecture-level abstraction opportunities and code simplification candidates, and challenge security assumptions through harden-app-security. Use when users ask for a diff review, refactor review, abstraction review, simplification review, or a pre-commit/pre-PR second opinion on current changes.
4
+ ---
5
+
6
+ # Review Change Set
7
+
8
+ ## Dependencies
9
+
10
+ - Required: none.
11
+ - Conditional: `harden-app-security` for code-affecting changes before finalizing review conclusions.
12
+ - Optional: none.
13
+ - Fallback: If the required security cross-check is unavailable for a code-affecting scope, stop and report the missing dependency.
14
+
15
+ ## Standards
16
+
17
+ - Evidence: Read the full active diff plus the minimum dependency chain needed to understand the changed behavior.
18
+ - Execution: Review architecture first, then simplification opportunities, then integrate confirmed security findings.
19
+ - Quality: Judge the change set as an outsider, keep only actionable findings, and avoid inventing concerns the security pass did not confirm.
20
+ - Output: Return review scope, architecture findings, simplification findings, security cross-check results, and residual uncertainty.
21
+
22
+ ## Overview
23
+
24
+ Use this skill to review the active git change set as an outsider, not as the original author. The goal is to find actionable abstraction and simplification opportunities with evidence, not to defend the current implementation.
25
+
26
+ ## Non-negotiable Review Rules
27
+
28
+ - Read the full active change set before judging any design choice.
29
+ - Discard authorship bias completely, including changes written earlier in the same conversation by this agent.
30
+ - Judge the diff from a reviewer perspective: the burden of proof is on the code, not on the author's intent.
31
+ - Prefer architecture and maintainability findings over style-only feedback.
32
+ - Recommend abstraction only when it reduces duplication, clarifies ownership, or stabilizes boundaries.
33
+ - Recommend simplification only when it preserves behavior while reducing complexity or ambiguity.
34
+
35
+ ## Workflow
36
+
37
+ ### 1) Inspect the active git state
38
+
39
+ - Run `git status -sb`, `git diff --stat`, and `git diff --cached --stat`.
40
+ - If both staged and unstaged changes exist, review both and label which findings apply to each surface.
41
+ - If there is no active diff, report `No active git change set to review` and stop.
42
+
43
+ ### 2) Build a factual baseline
44
+
45
+ - Read every changed file end-to-end.
46
+ - Read the minimum dependency chain needed to understand new helpers, moved logic, interfaces, and callers.
47
+ - Reconstruct actual behavior from code, tests, configuration, and executable evidence only.
48
+ - Ignore earlier planning context unless it is explicitly encoded in the repository.
49
+
50
+ ### 3) Review architecture first
51
+
52
+ Check whether the diff introduces or preserves problems such as:
53
+
54
+ - duplicated workflows that should live behind one module or helper,
55
+ - cross-layer leakage or ownership confusion,
56
+ - helper placement that hides domain boundaries,
57
+ - repeated condition trees or mapping logic that should be centralized,
58
+ - unstable interfaces or parameter shapes that should be normalized.
59
+
60
+ Keep only findings that name the proposed abstraction target and explain why the current structure is weaker.
61
+
62
+ ### 4) Review simplification opportunities second
63
+
64
+ Check whether the diff can be simplified through:
65
+
66
+ - removing redundant branches, wrappers, or state,
67
+ - flattening deeply nested control flow,
68
+ - collapsing duplicated validation or conversion logic,
69
+ - shrinking overly broad functions into clearer units,
70
+ - deleting dead or no-longer-needed compatibility paths.
71
+
72
+ Do not recommend refactors that merely move complexity around.
73
+
74
+ ### 5) Run the security cross-check
75
+
76
+ - Invoke `harden-app-security` on the same code-affecting scope.
77
+ - Integrate confirmed security findings into the final review when they materially affect the safety of the proposed structure.
78
+ - Do not invent security concerns that the dependency did not confirm.
79
+
80
+ ### 6) Report only actionable review output
81
+
82
+ Deliver:
83
+
84
+ 1. Review scope
85
+ - staged / unstaged coverage
86
+ - additional files read for context
87
+ 2. Architecture findings
88
+ - title
89
+ - evidence (`path:line`)
90
+ - abstraction candidate
91
+ - why the current design is weaker
92
+ 3. Simplification findings
93
+ - title
94
+ - evidence (`path:line`)
95
+ - simplification candidate
96
+ - expected benefit
97
+ 4. Security cross-check
98
+ - confirmed findings reused from `harden-app-security`
99
+ - reason they matter to this diff review
100
+ 5. Residual uncertainty
101
+ - hypotheses or follow-up checks that were not confirmed
102
+
103
+ If no actionable issue is found, report `No actionable abstraction or simplification finding identified`.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Review Change Set"
3
+ short_description: "Review the active git diff for abstraction and simplification opportunities"
4
+ default_prompt: "Use $review-change-set to read the active git change set end-to-end, discard any bias toward code written earlier in the conversation, review it like an independent reviewer for architecture-level abstraction and simplification opportunities, and run $harden-app-security as an adversarial cross-check for code-affecting changes."