@xiaotianxt/skills 0.1.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 (120) hide show
  1. package/EXCLUDED.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -0
  4. package/SECURITY.md +23 -0
  5. package/SOURCES.md +45 -0
  6. package/bin/skills.mjs +241 -0
  7. package/package.json +38 -0
  8. package/skills/1password/SKILL.md +94 -0
  9. package/skills/1password/agents/openai.yaml +4 -0
  10. package/skills/1password/references/item-management.md +80 -0
  11. package/skills/1password/references/op-cli.md +107 -0
  12. package/skills/apple-calendar-event/SKILL.md +81 -0
  13. package/skills/apple-calendar-event/agents/openai.yaml +4 -0
  14. package/skills/apple-calendar-event/scripts/calendar_audit.py +201 -0
  15. package/skills/apple-calendar-event/scripts/calendar_event.py +164 -0
  16. package/skills/bro-browser/SKILL.md +118 -0
  17. package/skills/bro-browser/agents/openai.yaml +4 -0
  18. package/skills/bro-browser/references/tool-map.md +102 -0
  19. package/skills/bro-browser/references/workflows.md +146 -0
  20. package/skills/bro-browser/scripts/bro-call.mjs +189 -0
  21. package/skills/calendar/SKILL.md +182 -0
  22. package/skills/calendar/agents/openai.yaml +4 -0
  23. package/skills/calendar/references/operations.md +255 -0
  24. package/skills/calendar/scripts/calendar_list_review.py +157 -0
  25. package/skills/calendar/scripts/event_dedupe_preview.py +155 -0
  26. package/skills/canvas/SKILL.md +70 -0
  27. package/skills/canvas/agents/openai.yaml +4 -0
  28. package/skills/canvas/references/canvas-api.md +76 -0
  29. package/skills/course-exam-review-planner/SKILL.md +127 -0
  30. package/skills/cx/SKILL.md +25 -0
  31. package/skills/gh-fix-ci/LICENSE.txt +201 -0
  32. package/skills/gh-fix-ci/SKILL.md +81 -0
  33. package/skills/gh-fix-ci/agents/openai.yaml +6 -0
  34. package/skills/gh-fix-ci/assets/github-small.svg +3 -0
  35. package/skills/gh-fix-ci/assets/github.png +0 -0
  36. package/skills/gh-fix-ci/scripts/inspect_pr_checks.py +509 -0
  37. package/skills/gh-review-workflow/SKILL.md +61 -0
  38. package/skills/gh-review-workflow/agents/openai.yaml +4 -0
  39. package/skills/gh-review-workflow/references/workflow.md +48 -0
  40. package/skills/gh-review-workflow/scripts/fetch_review_state.py +222 -0
  41. package/skills/gh-review-workflow/scripts/resolve_review_threads.py +83 -0
  42. package/skills/github/SKILL.md +74 -0
  43. package/skills/github/agents/openai.yaml +6 -0
  44. package/skills/github/assets/github-small.svg +3 -0
  45. package/skills/github/assets/github.png +0 -0
  46. package/skills/gws-calendar/SKILL.md +126 -0
  47. package/skills/gws-calendar-agenda/SKILL.md +52 -0
  48. package/skills/gws-calendar-insert/SKILL.md +66 -0
  49. package/skills/gws-docs/SKILL.md +48 -0
  50. package/skills/gws-docs-write/SKILL.md +49 -0
  51. package/skills/gws-drive/SKILL.md +137 -0
  52. package/skills/gws-drive-upload/SKILL.md +52 -0
  53. package/skills/gws-gmail/SKILL.md +62 -0
  54. package/skills/gws-gmail-forward/SKILL.md +55 -0
  55. package/skills/gws-gmail-reply/SKILL.md +58 -0
  56. package/skills/gws-gmail-reply-all/SKILL.md +62 -0
  57. package/skills/gws-gmail-send/SKILL.md +57 -0
  58. package/skills/gws-gmail-triage/SKILL.md +50 -0
  59. package/skills/gws-gmail-watch/SKILL.md +58 -0
  60. package/skills/gws-shared/SKILL.md +27 -0
  61. package/skills/helium-browser-mcp/SKILL.md +137 -0
  62. package/skills/helium-browser-mcp/agents/openai.yaml +4 -0
  63. package/skills/helium-browser-mcp/scripts/obmcp.mjs +92 -0
  64. package/skills/helium-browser-mcp/scripts/openbrowsermcp-stdio-proxy.mjs +170 -0
  65. package/skills/learn/SKILL.md +122 -0
  66. package/skills/learn/agents/openai.yaml +7 -0
  67. package/skills/learn/assets/AGENTS.template.md +33 -0
  68. package/skills/learn/assets/errorlog.template.typ +61 -0
  69. package/skills/learn/assets/reading-sequence.template.md +23 -0
  70. package/skills/learn/assets/source-index.template.md +17 -0
  71. package/skills/learn/assets/tasklog.template.typ +57 -0
  72. package/skills/learn/assets/workbook.template.typ +60 -0
  73. package/skills/learn/references/learning-science.md +103 -0
  74. package/skills/learn/scripts/init_learning_workspace.py +70 -0
  75. package/skills/macos-messages/SKILL.md +258 -0
  76. package/skills/memory/SKILL.md +33 -0
  77. package/skills/memory/codex.md +186 -0
  78. package/skills/memory/opencode.md +164 -0
  79. package/skills/mimestreamctl/SKILL.md +170 -0
  80. package/skills/mimestreamctl/agents/openai.yaml +4 -0
  81. package/skills/mimestreamctl/scripts/mimestreamctl +33 -0
  82. package/skills/mon/SKILL.md +51 -0
  83. package/skills/mon/scripts/mon_spend_review.py +458 -0
  84. package/skills/ocr/SKILL.md +136 -0
  85. package/skills/ocr/agents/openai.yaml +4 -0
  86. package/skills/ocr/references/local-ocr-best-practices.md +297 -0
  87. package/skills/ocr/references/mineru-api.md +159 -0
  88. package/skills/ocr/scripts/ocr-router +22 -0
  89. package/skills/ocr/scripts/ocr_router.py +741 -0
  90. package/skills/panopto-mp4-bulk-download/SKILL.md +57 -0
  91. package/skills/panopto-mp4-bulk-download/agents/openai.yaml +4 -0
  92. package/skills/panopto-mp4-bulk-download/references/url-patterns.md +26 -0
  93. package/skills/panopto-mp4-bulk-download/scripts/panopto_bulk_mp4.sh +213 -0
  94. package/skills/rust-systems-style/SKILL.md +109 -0
  95. package/skills/rust-systems-style/agents/openai.yaml +4 -0
  96. package/skills/rust-systems-style/references/rust-review-checklist.md +77 -0
  97. package/skills/rust-systems-style/references/style-sources.md +68 -0
  98. package/skills/ship-ai-native-cli/SKILL.md +76 -0
  99. package/skills/ship-ai-native-cli/agents/openai.yaml +4 -0
  100. package/skills/ship-ai-native-cli/references/case-notes.md +83 -0
  101. package/skills/ship-ai-native-cli/references/product-method.md +82 -0
  102. package/skills/ship-ai-native-cli/references/release-checklist.md +147 -0
  103. package/skills/ship-ai-native-cli/references/rust-cli-shape.md +111 -0
  104. package/skills/telegram-mtproto-session/SKILL.md +125 -0
  105. package/skills/telegram-mtproto-session/agents/openai.yaml +4 -0
  106. package/skills/telegram-mtproto-session/scripts/telegram_session.py +687 -0
  107. package/skills/tg/SKILL.md +173 -0
  108. package/skills/things3-manager/SKILL.md +116 -0
  109. package/skills/things3-manager/scripts/things +42 -0
  110. package/skills/things3-manager/scripts/things_cli.py +514 -0
  111. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  112. package/skills/web-artifacts-builder/SKILL.md +74 -0
  113. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
  114. package/skills/web-artifacts-builder/scripts/init-artifact.sh +379 -0
  115. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  116. package/skills/yeet/LICENSE.txt +201 -0
  117. package/skills/yeet/SKILL.md +71 -0
  118. package/skills/yeet/agents/openai.yaml +6 -0
  119. package/skills/yeet/assets/yeet-small.svg +3 -0
  120. package/skills/yeet/assets/yeet.png +0 -0
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: "gh-fix-ci"
3
+ description: "Use when a user asks to debug or fix failing GitHub PR checks that run in GitHub Actions. Use GitHub MCP tools for PR metadata and patch context, and use `gh` for Actions check and log inspection before implementing any approved fix."
4
+ ---
5
+
6
+
7
+ # GitHub Actions CI Fix
8
+
9
+ ## Overview
10
+
11
+ Use this skill when the task is specifically about failing GitHub Actions checks on a pull request. This workflow is hybrid by design:
12
+
13
+ - Use GitHub MCP tools for PR metadata, changed files, and review context.
14
+ - Use `gh` for GitHub Actions checks and logs because the connector does not expose that workflow end to end.
15
+ - Summarize the root cause first, propose a focused fix plan, and implement only after explicit approval.
16
+
17
+ Prereq: authenticate with GitHub CLI once, then confirm with `gh auth status`. Repo and workflow scopes are typically required for Actions inspection.
18
+
19
+ ## Inputs
20
+
21
+ - `repo`: path inside the repo (default `.`)
22
+ - `pr`: PR number or URL (optional; defaults to current branch PR)
23
+ - `gh` authentication for the repo host
24
+
25
+ ## Quick start
26
+
27
+ - `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "<number-or-url>"`
28
+ - Add `--json` if you want machine-friendly output for summarization.
29
+
30
+ ## Workflow
31
+
32
+ 1. Verify gh authentication.
33
+ - Run `gh auth status` in the repo.
34
+ - If unauthenticated, ask the user to run `gh auth login` (ensuring repo + workflow scopes) before proceeding.
35
+ 2. Resolve the PR.
36
+ - If the user provides a PR number or URL, use that directly.
37
+ - Otherwise prefer the current branch PR with `gh pr view --json number,url`.
38
+ - When repo and PR are known, fetch PR metadata and patch context through GitHub MCP tools.
39
+ 3. Inspect failing checks (GitHub Actions only).
40
+ - Preferred: run the bundled script (handles gh field drift and job-log fallbacks):
41
+ - `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "<number-or-url>"`
42
+ - Add `--json` for machine-friendly output.
43
+ - Manual fallback:
44
+ - `gh pr checks <pr> --json name,state,bucket,link,startedAt,completedAt,workflow`
45
+ - If a field is rejected, rerun with the available fields reported by `gh`.
46
+ - For each failing check, extract the run id from `detailsUrl` and run:
47
+ - `gh run view <run_id> --json name,workflowName,conclusion,status,url,event,headBranch,headSha`
48
+ - `gh run view <run_id> --log`
49
+ - If the run log says it is still in progress, fetch job logs directly:
50
+ - `gh api "/repos/<owner>/<repo>/actions/jobs/<job_id>/logs" > "<path>"`
51
+ 4. Scope non-GitHub Actions checks.
52
+ - If `detailsUrl` is not a GitHub Actions run, label it as external and only report the URL.
53
+ - Do not attempt Buildkite or other providers; keep the workflow lean.
54
+ 5. Summarize failures for the user.
55
+ - Provide the failing check name, run URL (if any), and a concise log snippet.
56
+ - Call out missing logs explicitly and do not over-claim certainty.
57
+ 6. Propose a focused fix plan and wait for approval.
58
+ - Keep the plan tied directly to the failing checks and the observed root cause.
59
+ 7. Implement after approval.
60
+ - Apply the approved fix locally.
61
+ - Run the most relevant local verification available.
62
+ 8. Recheck status and summarize residual risk.
63
+ - Suggest re-running the relevant tests and `gh pr checks`.
64
+ - Report what is still unverified, what may still be flaky, and whether any failing checks were external and therefore not actionable here.
65
+
66
+ ## Bundled Resources
67
+
68
+ ### scripts/inspect_pr_checks.py
69
+
70
+ Fetch failing PR checks, pull GitHub Actions logs, and extract a failure snippet. Exits non-zero when failures remain so it can be used in automation.
71
+
72
+ Usage examples:
73
+ - `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "123"`
74
+ - `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --pr "https://github.com/org/repo/pull/123" --json`
75
+ - `python "<path-to-skill>/scripts/inspect_pr_checks.py" --repo "." --max-lines 200 --context 40`
76
+
77
+ ## Guardrails
78
+
79
+ - Do not imply that GitHub MCP can replace `gh` for Actions log retrieval.
80
+ - Treat non-GitHub Actions providers as report-only unless the user explicitly wants a separate investigation path.
81
+ - If the failure is clearly unrelated to the local diff, say so before proposing code changes.
@@ -0,0 +1,6 @@
1
+ interface:
2
+ display_name: "CI Debug"
3
+ short_description: "Debug failing GitHub Actions checks"
4
+ icon_small: "./assets/github-small.svg"
5
+ icon_large: "./assets/github.png"
6
+ default_prompt: "Use $gh-fix-ci to inspect the failing GitHub Actions checks on this PR, summarize the root cause, and propose the smallest viable fix."
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
2
+ <path fill="currentColor" d="M8 1.3a6.665 6.665 0 0 1 5.413 10.56 6.677 6.677 0 0 1-3.288 2.432c-.333.067-.458-.142-.458-.316 0-.226.008-.942.008-1.834 0-.625-.208-1.025-.45-1.233 1.483-.167 3.042-.734 3.042-3.292a2.58 2.58 0 0 0-.684-1.792c.067-.166.3-.85-.066-1.766 0 0-.559-.184-1.834.683a6.186 6.186 0 0 0-1.666-.225c-.567 0-1.134.075-1.667.225-1.275-.858-1.833-.683-1.833-.683-.367.916-.134 1.6-.067 1.766a2.594 2.594 0 0 0-.683 1.792c0 2.55 1.55 3.125 3.033 3.292-.192.166-.367.458-.425.891-.383.175-1.342.459-1.942-.55-.125-.2-.5-.691-1.025-.683-.558.008-.225.317.009.442.283.158.608.75.683.941.133.376.567 1.092 2.242.784 0 .558.008 1.083.008 1.242 0 .174-.125.374-.458.316a6.662 6.662 0 0 1-4.559-6.325A6.665 6.665 0 0 1 8 1.3Z"/>
3
+ </svg>
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from shutil import which
11
+ from typing import Any, Iterable, Sequence
12
+
13
+ FAILURE_CONCLUSIONS = {
14
+ "failure",
15
+ "cancelled",
16
+ "timed_out",
17
+ "action_required",
18
+ }
19
+
20
+ FAILURE_STATES = {
21
+ "failure",
22
+ "error",
23
+ "cancelled",
24
+ "timed_out",
25
+ "action_required",
26
+ }
27
+
28
+ FAILURE_BUCKETS = {"fail"}
29
+
30
+ FAILURE_MARKERS = (
31
+ "error",
32
+ "fail",
33
+ "failed",
34
+ "traceback",
35
+ "exception",
36
+ "assert",
37
+ "panic",
38
+ "fatal",
39
+ "timeout",
40
+ "segmentation fault",
41
+ )
42
+
43
+ DEFAULT_MAX_LINES = 160
44
+ DEFAULT_CONTEXT_LINES = 30
45
+ PENDING_LOG_MARKERS = (
46
+ "still in progress",
47
+ "log will be available when it is complete",
48
+ )
49
+
50
+
51
+ class GhResult:
52
+ def __init__(self, returncode: int, stdout: str, stderr: str):
53
+ self.returncode = returncode
54
+ self.stdout = stdout
55
+ self.stderr = stderr
56
+
57
+
58
+ def run_gh_command(args: Sequence[str], cwd: Path) -> GhResult:
59
+ process = subprocess.run(
60
+ ["gh", *args],
61
+ cwd=cwd,
62
+ text=True,
63
+ capture_output=True,
64
+ )
65
+ return GhResult(process.returncode, process.stdout, process.stderr)
66
+
67
+
68
+ def run_gh_command_raw(args: Sequence[str], cwd: Path) -> tuple[int, bytes, str]:
69
+ process = subprocess.run(
70
+ ["gh", *args],
71
+ cwd=cwd,
72
+ capture_output=True,
73
+ )
74
+ stderr = process.stderr.decode(errors="replace")
75
+ return process.returncode, process.stdout, stderr
76
+
77
+
78
+ def parse_args() -> argparse.Namespace:
79
+ parser = argparse.ArgumentParser(
80
+ description=(
81
+ "Inspect failing GitHub PR checks, fetch GitHub Actions logs, and extract a "
82
+ "failure snippet."
83
+ ),
84
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
85
+ )
86
+ parser.add_argument("--repo", default=".", help="Path inside the target Git repository.")
87
+ parser.add_argument(
88
+ "--pr", default=None, help="PR number or URL (defaults to current branch PR)."
89
+ )
90
+ parser.add_argument("--max-lines", type=int, default=DEFAULT_MAX_LINES)
91
+ parser.add_argument("--context", type=int, default=DEFAULT_CONTEXT_LINES)
92
+ parser.add_argument("--json", action="store_true", help="Emit JSON instead of text output.")
93
+ return parser.parse_args()
94
+
95
+
96
+ def main() -> int:
97
+ args = parse_args()
98
+ repo_root = find_git_root(Path(args.repo))
99
+ if repo_root is None:
100
+ print("Error: not inside a Git repository.", file=sys.stderr)
101
+ return 1
102
+
103
+ if not ensure_gh_available(repo_root):
104
+ return 1
105
+
106
+ pr_value = resolve_pr(args.pr, repo_root)
107
+ if pr_value is None:
108
+ return 1
109
+
110
+ checks = fetch_checks(pr_value, repo_root)
111
+ if checks is None:
112
+ return 1
113
+
114
+ failing = [c for c in checks if is_failing(c)]
115
+ if not failing:
116
+ print(f"PR #{pr_value}: no failing checks detected.")
117
+ return 0
118
+
119
+ results = []
120
+ for check in failing:
121
+ results.append(
122
+ analyze_check(
123
+ check,
124
+ repo_root=repo_root,
125
+ max_lines=max(1, args.max_lines),
126
+ context=max(1, args.context),
127
+ )
128
+ )
129
+
130
+ if args.json:
131
+ print(json.dumps({"pr": pr_value, "results": results}, indent=2))
132
+ else:
133
+ render_results(pr_value, results)
134
+
135
+ return 1
136
+
137
+
138
+ def find_git_root(start: Path) -> Path | None:
139
+ result = subprocess.run(
140
+ ["git", "rev-parse", "--show-toplevel"],
141
+ cwd=start,
142
+ text=True,
143
+ capture_output=True,
144
+ )
145
+ if result.returncode != 0:
146
+ return None
147
+ return Path(result.stdout.strip())
148
+
149
+
150
+ def ensure_gh_available(repo_root: Path) -> bool:
151
+ if which("gh") is None:
152
+ print("Error: gh is not installed or not on PATH.", file=sys.stderr)
153
+ return False
154
+ result = run_gh_command(["auth", "status"], cwd=repo_root)
155
+ if result.returncode == 0:
156
+ return True
157
+ message = (result.stderr or result.stdout or "").strip()
158
+ print(message or "Error: gh not authenticated.", file=sys.stderr)
159
+ return False
160
+
161
+
162
+ def resolve_pr(pr_value: str | None, repo_root: Path) -> str | None:
163
+ if pr_value:
164
+ return pr_value
165
+ result = run_gh_command(["pr", "view", "--json", "number"], cwd=repo_root)
166
+ if result.returncode != 0:
167
+ message = (result.stderr or result.stdout or "").strip()
168
+ print(message or "Error: unable to resolve PR.", file=sys.stderr)
169
+ return None
170
+ try:
171
+ data = json.loads(result.stdout or "{}")
172
+ except json.JSONDecodeError:
173
+ print("Error: unable to parse PR JSON.", file=sys.stderr)
174
+ return None
175
+ number = data.get("number")
176
+ if not number:
177
+ print("Error: no PR number found.", file=sys.stderr)
178
+ return None
179
+ return str(number)
180
+
181
+
182
+ def fetch_checks(pr_value: str, repo_root: Path) -> list[dict[str, Any]] | None:
183
+ primary_fields = ["name", "state", "conclusion", "detailsUrl", "startedAt", "completedAt"]
184
+ result = run_gh_command(
185
+ ["pr", "checks", pr_value, "--json", ",".join(primary_fields)],
186
+ cwd=repo_root,
187
+ )
188
+ if result.returncode != 0:
189
+ message = "\n".join(filter(None, [result.stderr, result.stdout])).strip()
190
+ available_fields = parse_available_fields(message)
191
+ if available_fields:
192
+ fallback_fields = [
193
+ "name",
194
+ "state",
195
+ "bucket",
196
+ "link",
197
+ "startedAt",
198
+ "completedAt",
199
+ "workflow",
200
+ ]
201
+ selected_fields = [field for field in fallback_fields if field in available_fields]
202
+ if not selected_fields:
203
+ print("Error: no usable fields available for gh pr checks.", file=sys.stderr)
204
+ return None
205
+ result = run_gh_command(
206
+ ["pr", "checks", pr_value, "--json", ",".join(selected_fields)],
207
+ cwd=repo_root,
208
+ )
209
+ if result.returncode != 0:
210
+ message = (result.stderr or result.stdout or "").strip()
211
+ print(message or "Error: gh pr checks failed.", file=sys.stderr)
212
+ return None
213
+ else:
214
+ print(message or "Error: gh pr checks failed.", file=sys.stderr)
215
+ return None
216
+ try:
217
+ data = json.loads(result.stdout or "[]")
218
+ except json.JSONDecodeError:
219
+ print("Error: unable to parse checks JSON.", file=sys.stderr)
220
+ return None
221
+ if not isinstance(data, list):
222
+ print("Error: unexpected checks JSON shape.", file=sys.stderr)
223
+ return None
224
+ return data
225
+
226
+
227
+ def is_failing(check: dict[str, Any]) -> bool:
228
+ conclusion = normalize_field(check.get("conclusion"))
229
+ if conclusion in FAILURE_CONCLUSIONS:
230
+ return True
231
+ state = normalize_field(check.get("state") or check.get("status"))
232
+ if state in FAILURE_STATES:
233
+ return True
234
+ bucket = normalize_field(check.get("bucket"))
235
+ return bucket in FAILURE_BUCKETS
236
+
237
+
238
+ def analyze_check(
239
+ check: dict[str, Any],
240
+ repo_root: Path,
241
+ max_lines: int,
242
+ context: int,
243
+ ) -> dict[str, Any]:
244
+ url = check.get("detailsUrl") or check.get("link") or ""
245
+ run_id = extract_run_id(url)
246
+ job_id = extract_job_id(url)
247
+ base: dict[str, Any] = {
248
+ "name": check.get("name", ""),
249
+ "detailsUrl": url,
250
+ "runId": run_id,
251
+ "jobId": job_id,
252
+ }
253
+
254
+ if run_id is None:
255
+ base["status"] = "external"
256
+ base["note"] = "No GitHub Actions run id detected in detailsUrl."
257
+ return base
258
+
259
+ metadata = fetch_run_metadata(run_id, repo_root)
260
+ log_text, log_error, log_status = fetch_check_log(
261
+ run_id=run_id,
262
+ job_id=job_id,
263
+ repo_root=repo_root,
264
+ )
265
+
266
+ if log_status == "pending":
267
+ base["status"] = "log_pending"
268
+ base["note"] = log_error or "Logs are not available yet."
269
+ if metadata:
270
+ base["run"] = metadata
271
+ return base
272
+
273
+ if log_error:
274
+ base["status"] = "log_unavailable"
275
+ base["error"] = log_error
276
+ if metadata:
277
+ base["run"] = metadata
278
+ return base
279
+
280
+ snippet = extract_failure_snippet(log_text, max_lines=max_lines, context=context)
281
+ base["status"] = "ok"
282
+ base["run"] = metadata or {}
283
+ base["logSnippet"] = snippet
284
+ base["logTail"] = tail_lines(log_text, max_lines)
285
+ return base
286
+
287
+
288
+ def extract_run_id(url: str) -> str | None:
289
+ if not url:
290
+ return None
291
+ for pattern in (r"/actions/runs/(\d+)", r"/runs/(\d+)"):
292
+ match = re.search(pattern, url)
293
+ if match:
294
+ return match.group(1)
295
+ return None
296
+
297
+
298
+ def extract_job_id(url: str) -> str | None:
299
+ if not url:
300
+ return None
301
+ match = re.search(r"/actions/runs/\d+/job/(\d+)", url)
302
+ if match:
303
+ return match.group(1)
304
+ match = re.search(r"/job/(\d+)", url)
305
+ if match:
306
+ return match.group(1)
307
+ return None
308
+
309
+
310
+ def fetch_run_metadata(run_id: str, repo_root: Path) -> dict[str, Any] | None:
311
+ fields = [
312
+ "conclusion",
313
+ "status",
314
+ "workflowName",
315
+ "name",
316
+ "event",
317
+ "headBranch",
318
+ "headSha",
319
+ "url",
320
+ ]
321
+ result = run_gh_command(["run", "view", run_id, "--json", ",".join(fields)], cwd=repo_root)
322
+ if result.returncode != 0:
323
+ return None
324
+ try:
325
+ data = json.loads(result.stdout or "{}")
326
+ except json.JSONDecodeError:
327
+ return None
328
+ if not isinstance(data, dict):
329
+ return None
330
+ return data
331
+
332
+
333
+ def fetch_check_log(
334
+ run_id: str,
335
+ job_id: str | None,
336
+ repo_root: Path,
337
+ ) -> tuple[str, str, str]:
338
+ log_text, log_error = fetch_run_log(run_id, repo_root)
339
+ if not log_error:
340
+ return log_text, "", "ok"
341
+
342
+ if is_log_pending_message(log_error) and job_id:
343
+ job_log, job_error = fetch_job_log(job_id, repo_root)
344
+ if job_log:
345
+ return job_log, "", "ok"
346
+ if job_error and is_log_pending_message(job_error):
347
+ return "", job_error, "pending"
348
+ if job_error:
349
+ return "", job_error, "error"
350
+ return "", log_error, "pending"
351
+
352
+ if is_log_pending_message(log_error):
353
+ return "", log_error, "pending"
354
+
355
+ return "", log_error, "error"
356
+
357
+
358
+ def fetch_run_log(run_id: str, repo_root: Path) -> tuple[str, str]:
359
+ result = run_gh_command(["run", "view", run_id, "--log"], cwd=repo_root)
360
+ if result.returncode != 0:
361
+ error = (result.stderr or result.stdout or "").strip()
362
+ return "", error or "gh run view failed"
363
+ return result.stdout, ""
364
+
365
+
366
+ def fetch_job_log(job_id: str, repo_root: Path) -> tuple[str, str]:
367
+ repo_slug = fetch_repo_slug(repo_root)
368
+ if not repo_slug:
369
+ return "", "Error: unable to resolve repository name for job logs."
370
+ endpoint = f"/repos/{repo_slug}/actions/jobs/{job_id}/logs"
371
+ returncode, stdout_bytes, stderr = run_gh_command_raw(["api", endpoint], cwd=repo_root)
372
+ if returncode != 0:
373
+ message = (stderr or stdout_bytes.decode(errors="replace")).strip()
374
+ return "", message or "gh api job logs failed"
375
+ if is_zip_payload(stdout_bytes):
376
+ return "", "Job logs returned a zip archive; unable to parse."
377
+ return stdout_bytes.decode(errors="replace"), ""
378
+
379
+
380
+ def fetch_repo_slug(repo_root: Path) -> str | None:
381
+ result = run_gh_command(["repo", "view", "--json", "nameWithOwner"], cwd=repo_root)
382
+ if result.returncode != 0:
383
+ return None
384
+ try:
385
+ data = json.loads(result.stdout or "{}")
386
+ except json.JSONDecodeError:
387
+ return None
388
+ name_with_owner = data.get("nameWithOwner")
389
+ if not name_with_owner:
390
+ return None
391
+ return str(name_with_owner)
392
+
393
+
394
+ def normalize_field(value: Any) -> str:
395
+ if value is None:
396
+ return ""
397
+ return str(value).strip().lower()
398
+
399
+
400
+ def parse_available_fields(message: str) -> list[str]:
401
+ if "Available fields:" not in message:
402
+ return []
403
+ fields: list[str] = []
404
+ collecting = False
405
+ for line in message.splitlines():
406
+ if "Available fields:" in line:
407
+ collecting = True
408
+ continue
409
+ if not collecting:
410
+ continue
411
+ field = line.strip()
412
+ if not field:
413
+ continue
414
+ fields.append(field)
415
+ return fields
416
+
417
+
418
+ def is_log_pending_message(message: str) -> bool:
419
+ lowered = message.lower()
420
+ return any(marker in lowered for marker in PENDING_LOG_MARKERS)
421
+
422
+
423
+ def is_zip_payload(payload: bytes) -> bool:
424
+ return payload.startswith(b"PK")
425
+
426
+
427
+ def extract_failure_snippet(log_text: str, max_lines: int, context: int) -> str:
428
+ lines = log_text.splitlines()
429
+ if not lines:
430
+ return ""
431
+
432
+ marker_index = find_failure_index(lines)
433
+ if marker_index is None:
434
+ return "\n".join(lines[-max_lines:])
435
+
436
+ start = max(0, marker_index - context)
437
+ end = min(len(lines), marker_index + context)
438
+ window = lines[start:end]
439
+ if len(window) > max_lines:
440
+ window = window[-max_lines:]
441
+ return "\n".join(window)
442
+
443
+
444
+ def find_failure_index(lines: Sequence[str]) -> int | None:
445
+ for idx in range(len(lines) - 1, -1, -1):
446
+ lowered = lines[idx].lower()
447
+ if any(marker in lowered for marker in FAILURE_MARKERS):
448
+ return idx
449
+ return None
450
+
451
+
452
+ def tail_lines(text: str, max_lines: int) -> str:
453
+ if max_lines <= 0:
454
+ return ""
455
+ lines = text.splitlines()
456
+ return "\n".join(lines[-max_lines:])
457
+
458
+
459
+ def render_results(pr_number: str, results: Iterable[dict[str, Any]]) -> None:
460
+ results_list = list(results)
461
+ print(f"PR #{pr_number}: {len(results_list)} failing checks analyzed.")
462
+ for result in results_list:
463
+ print("-" * 60)
464
+ print(f"Check: {result.get('name', '')}")
465
+ if result.get("detailsUrl"):
466
+ print(f"Details: {result['detailsUrl']}")
467
+ run_id = result.get("runId")
468
+ if run_id:
469
+ print(f"Run ID: {run_id}")
470
+ job_id = result.get("jobId")
471
+ if job_id:
472
+ print(f"Job ID: {job_id}")
473
+ status = result.get("status", "unknown")
474
+ print(f"Status: {status}")
475
+
476
+ run_meta = result.get("run", {})
477
+ if run_meta:
478
+ branch = run_meta.get("headBranch", "")
479
+ sha = (run_meta.get("headSha") or "")[:12]
480
+ workflow = run_meta.get("workflowName") or run_meta.get("name") or ""
481
+ conclusion = run_meta.get("conclusion") or run_meta.get("status") or ""
482
+ print(f"Workflow: {workflow} ({conclusion})")
483
+ if branch or sha:
484
+ print(f"Branch/SHA: {branch} {sha}")
485
+ if run_meta.get("url"):
486
+ print(f"Run URL: {run_meta['url']}")
487
+
488
+ if result.get("note"):
489
+ print(f"Note: {result['note']}")
490
+
491
+ if result.get("error"):
492
+ print(f"Error fetching logs: {result['error']}")
493
+ continue
494
+
495
+ snippet = result.get("logSnippet") or ""
496
+ if snippet:
497
+ print("Failure snippet:")
498
+ print(indent_block(snippet, prefix=" "))
499
+ else:
500
+ print("No snippet available.")
501
+ print("-" * 60)
502
+
503
+
504
+ def indent_block(text: str, prefix: str = " ") -> str:
505
+ return "\n".join(f"{prefix}{line}" for line in text.splitlines())
506
+
507
+
508
+ if __name__ == "__main__":
509
+ raise SystemExit(main())
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: gh-review-workflow
3
+ description: Inspect and address GitHub pull request review state, including latest PR comments, review submissions, unresolved inline threads, actionable review feedback, and post-fix thread resolution. Use when Codex needs to check "latest PR comments", summarize or implement requested PR changes, inspect the current branch PR, or resolve review threads after fixes are pushed.
4
+ ---
5
+
6
+ # GH Review Workflow
7
+
8
+ Use this skill when the user wants the real review state of a GitHub PR, not just flat issue comments. Prefer GitHub connector/MCP tools for repository and PR metadata. Use the bundled scripts or `gh api graphql` when you need inline review threads, `isResolved`, `isOutdated`, or file and line anchors.
9
+
10
+ ## Workflow
11
+
12
+ 1. Resolve the target PR.
13
+ - If the user provides a PR URL or repo plus PR number, use that directly.
14
+ - If the task is about the current branch PR, run `gh auth status` and then use the bundled `scripts/fetch_review_state.py` with no arguments.
15
+ - If the task is about the latest PR in a repo, first use GitHub metadata tools to list PRs sorted by create time.
16
+
17
+ 2. Inspect metadata and flat comments.
18
+ - Use GitHub connector data for PR title, head/base branches, and top-level PR comments.
19
+ - Do not treat flat PR comments as complete review state.
20
+
21
+ 3. Inspect review threads.
22
+ - Run `python3 ~/.codex/skills/gh-review-workflow/scripts/fetch_review_state.py` for the current branch PR.
23
+ - Or run `python3 ~/.codex/skills/gh-review-workflow/scripts/fetch_review_state.py --owner OWNER --repo REPO --number 123`.
24
+ - Read `review_threads` first. That is the source of truth for inline review comments and resolution state.
25
+
26
+ 4. Summarize actionability.
27
+ - Group duplicate comments by behavior or file instead of answering each line independently.
28
+ - Separate real blockers from style suggestions and "reply only" items.
29
+ - Call out whether a thread is still current or already `isOutdated`.
30
+
31
+ 5. If the user asks to address review feedback.
32
+ - Confirm the intended scope unless the user explicitly says to fix all unresolved actionable threads.
33
+ - Keep each code change traceable to a feedback cluster.
34
+ - If a comment needs explanation rather than code, draft the response instead of forcing a code change.
35
+ - Verify locally before pushing or resolving any thread.
36
+
37
+ 6. After fixes.
38
+ - Verify code locally before pushing.
39
+ - Push the branch.
40
+ - If the user asked to resolve addressed threads, run `python3 ~/.codex/skills/gh-review-workflow/scripts/resolve_review_threads.py THREAD_ID...`.
41
+ - If the user wants to resolve all unresolved threads captured in a saved JSON dump, run `python3 ~/.codex/skills/gh-review-workflow/scripts/resolve_review_threads.py --from-json review_state.json`.
42
+
43
+ ## Interpretation Rules
44
+
45
+ - `conversation_comments` are top-level PR comments.
46
+ - `reviews` are submitted or draft review objects. `PENDING` means there is a draft review even if no formal review event was submitted yet.
47
+ - `review_threads` hold inline review comments, anchors, and resolution state.
48
+ - `isOutdated: true` means the original diff context moved or changed; decide whether the underlying concern is already addressed before ignoring it.
49
+ - Repeated comments like "can we reuse X?" often map to one design decision. Cluster them.
50
+
51
+ ## Write Safety
52
+
53
+ - Do not comment on GitHub or resolve review threads unless the user explicitly asked for a write.
54
+ - Do not reject a suggestion vaguely. If keeping the current design, explain the ABI, spec, or ownership reason concretely.
55
+ - For review requests, prioritize behavioral bugs, regressions, and missing validation over cosmetic style changes.
56
+
57
+ ## Resources
58
+
59
+ - Read [workflow.md](references/workflow.md) for the tool split and response template.
60
+ - Use `scripts/fetch_review_state.py` for thread-aware reads.
61
+ - Use `scripts/resolve_review_threads.py` for post-fix cleanup.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "GH Review Workflow"
3
+ short_description: "Inspect and close PR review threads"
4
+ default_prompt: "Inspect the latest PR review comments, summarize what is actionable, and help resolve the threads after fixes are pushed."