@minakoto00/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.
@@ -0,0 +1,106 @@
1
+ # Review PR Examples
2
+
3
+ ## Latest Open GitLab MR
4
+
5
+ ```bash
6
+ bash scripts/resolve_review_target.sh --repo /path/to/repo --latest
7
+ ```
8
+
9
+ Expected shape:
10
+
11
+ ```text
12
+ platform=gitlab
13
+ number=42
14
+ source_branch=feat/example
15
+ head_sha=<sha>
16
+ ```
17
+
18
+ ## Specific GitHub PR
19
+
20
+ ```bash
21
+ bash scripts/resolve_review_target.sh --repo /path/to/repo --number 128
22
+ ```
23
+
24
+ ## Fetch Review Comments
25
+
26
+ ```bash
27
+ bash scripts/fetch_review_comments.sh --repo /path/to/repo --number 128 --platform github --json
28
+ ```
29
+
30
+ If `code-review comments` are present, ask the user whether to include code-review comments in scope.
31
+
32
+ If `discussion comments` are present, ask the user whether to include discussion comments in scope.
33
+
34
+ Only the approved comment categories move forward.
35
+
36
+ Resolved code-review feedback is excluded by default.
37
+
38
+ Outdated threads are validated separately from unresolved threads.
39
+
40
+ Group approved unresolved and outdated code-review feedback into issue clusters before validation.
41
+
42
+ For approved comments, dispatch several subagents in parallel to validate whether the comments still make sense.
43
+
44
+ During that validation, subagents search only within changed files for same-pattern candidates.
45
+
46
+ Those same-pattern candidates are reported separately from the original issues.
47
+
48
+ Then confirm the verification report with the user before planning fixes.
49
+
50
+ ## Inspect Repo Policy
51
+
52
+ ```bash
53
+ bash scripts/repo_policy.sh --repo /path/to/repo
54
+ ```
55
+
56
+ This inspects both `AGENTS.md` and `CLAUDE.md` if they exist. If neither file defines a worktree rule, the fallback is `<repo>/.worktrees`.
57
+
58
+ ## Reuse Existing Worktree
59
+
60
+ ```bash
61
+ bash scripts/worktree_sync.sh \
62
+ --repo /path/to/repo \
63
+ --source-branch feat/example \
64
+ --head-sha abcdef1234567890
65
+ ```
66
+
67
+ If the worktree already exists and is clean, the script fetches and fast-forwards the source branch, then aligns it to the requested MR/PR head SHA.
68
+
69
+ ## Create Missing Worktree
70
+
71
+ ```bash
72
+ bash scripts/worktree_sync.sh \
73
+ --repo /path/to/repo \
74
+ --source-branch feat/example \
75
+ --head-sha abcdef1234567890
76
+ ```
77
+
78
+ If the worktree does not exist, the script creates it under the repo-approved worktree root and checks out the remote source branch.
79
+
80
+ ## Comment-Only Proposal
81
+
82
+ ```bash
83
+ bash scripts/post_review_comment.sh \
84
+ --repo /path/to/repo \
85
+ --number 42 \
86
+ --body-file /tmp/review-plan.md
87
+ ```
88
+
89
+ Suggested comment body structure:
90
+
91
+ ```md
92
+ ## Change Plan
93
+
94
+ - Summary of review findings
95
+ - Files or subsystems likely to change
96
+ - Patch outline
97
+ - Verification steps
98
+ ```
99
+
100
+ ## Installer Dry Run
101
+
102
+ Advanced manual installer:
103
+
104
+ ```bash
105
+ bash install-skill.sh --agent codex --scope user-global --method symlink --dry-run
106
+ ```
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
5
+ # shellcheck source=./common.sh
6
+ source "$SCRIPT_DIR/common.sh"
7
+
8
+ input_path=
9
+
10
+ usage() {
11
+ cat <<'USAGE'
12
+ Usage: cluster_review_issues.sh [--input <path>]
13
+
14
+ Read normalized review-comment JSON and group unresolved or outdated code-review comments into issue clusters.
15
+ USAGE
16
+ }
17
+
18
+ while [[ $# -gt 0 ]]; do
19
+ case "$1" in
20
+ --input)
21
+ input_path=$2
22
+ shift 2
23
+ ;;
24
+ --help|-h)
25
+ usage
26
+ exit 0
27
+ ;;
28
+ *)
29
+ die "unknown argument: $1"
30
+ ;;
31
+ esac
32
+ done
33
+
34
+ require_cmd jq
35
+
36
+ if [[ -n "$input_path" ]]; then
37
+ input_json=$(cat "$input_path")
38
+ else
39
+ input_json=$(cat)
40
+ fi
41
+
42
+ jq -cn --argjson payload "$input_json" '
43
+ def review_comments:
44
+ $payload.code_review_comments.items // [];
45
+
46
+ def comment_state($comment):
47
+ $comment.thread_state
48
+ // (if ($comment.thread_resolved // false) then "resolved" else "unresolved" end);
49
+
50
+ def cluster_key($comment):
51
+ $comment.cluster_key
52
+ // $comment.issue_key
53
+ // (
54
+ (($comment.path // "") + "::" + ($comment.body // ""))
55
+ | gsub("[[:space:]]+"; " ")
56
+ );
57
+
58
+ def resolved_comments:
59
+ review_comments
60
+ | map(select(comment_state(.) == "resolved"));
61
+
62
+ def active_comments:
63
+ review_comments
64
+ | map(select(comment_state(.) != "resolved"));
65
+
66
+ def issue_clusters:
67
+ active_comments
68
+ | sort_by(cluster_key(.))
69
+ | group_by(cluster_key(.))
70
+ | map(
71
+ . as $cluster_comments
72
+ | {
73
+ cluster_id: cluster_key($cluster_comments[0]),
74
+ comment_count: ($cluster_comments | length),
75
+ comment_ids: ($cluster_comments | map(.id | tostring)),
76
+ thread_ids: ($cluster_comments | map(.thread_id // null) | unique),
77
+ thread_states: ($cluster_comments | map(comment_state(.)) | unique | sort),
78
+ paths: ($cluster_comments | map(.path // "") | unique | sort),
79
+ comments: (
80
+ $cluster_comments
81
+ | map({
82
+ id: (.id | tostring),
83
+ thread_id: (.thread_id // null),
84
+ thread_state: comment_state(.),
85
+ path: (.path // ""),
86
+ body: (.body // "")
87
+ })
88
+ )
89
+ }
90
+ );
91
+
92
+ {
93
+ excluded_resolved_threads: {
94
+ count: (resolved_comments | map(.thread_id // .id) | unique | length),
95
+ comment_count: (resolved_comments | length),
96
+ thread_ids: (resolved_comments | map(.thread_id // .id) | unique | sort)
97
+ },
98
+ active_review_comments: {
99
+ count: (active_comments | length),
100
+ state_counts: {
101
+ unresolved: (active_comments | map(select(comment_state(.) == "unresolved")) | length),
102
+ outdated: (active_comments | map(select(comment_state(.) == "outdated")) | length)
103
+ }
104
+ },
105
+ issue_clusters: {
106
+ count: (issue_clusters | length),
107
+ items: issue_clusters
108
+ }
109
+ }
110
+ '
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir() {
5
+ cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
6
+ }
7
+
8
+ die() {
9
+ printf 'error: %s\n' "$*" >&2
10
+ exit 1
11
+ }
12
+
13
+ warn() {
14
+ printf 'warn: %s\n' "$*" >&2
15
+ }
16
+
17
+ require_cmd() {
18
+ command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
19
+ }
20
+
21
+ resolve_repo_root() {
22
+ local repo_arg="${1:-.}"
23
+ git -C "$repo_arg" rev-parse --show-toplevel 2>/dev/null || die "not a git repository: $repo_arg"
24
+ }
25
+
26
+ origin_url() {
27
+ local repo_root=$1
28
+ git -C "$repo_root" remote get-url origin 2>/dev/null || die "repository has no origin remote: $repo_root"
29
+ }
30
+
31
+ trim() {
32
+ local value=$1
33
+ value=${value#"${value%%[![:space:]]*}"}
34
+ value=${value%"${value##*[![:space:]]}"}
35
+ printf '%s' "$value"
36
+ }
37
+
38
+ print_kv() {
39
+ local key=$1
40
+ local value=${2-}
41
+ printf '%s=%q\n' "$key" "$value"
42
+ }
43
+
44
+ normalize_path() {
45
+ local raw=$1
46
+ python3 - "$raw" <<'PY'
47
+ import os
48
+ import sys
49
+ print(os.path.abspath(os.path.expanduser(sys.argv[1])))
50
+ PY
51
+ }
52
+
53
+ expand_path() {
54
+ local raw=$1
55
+ local repo_root=$2
56
+
57
+ if [[ -z "$raw" ]]; then
58
+ die "cannot expand empty path"
59
+ fi
60
+
61
+ if [[ "$raw" == ~* ]]; then
62
+ raw="${raw/#\~/$HOME}"
63
+ fi
64
+
65
+ case "$raw" in
66
+ /*)
67
+ normalize_path "$raw"
68
+ ;;
69
+ ./*|../*)
70
+ normalize_path "$repo_root/$raw"
71
+ ;;
72
+ *)
73
+ normalize_path "$raw"
74
+ ;;
75
+ esac
76
+ }
77
+
78
+ sanitize_branch_name() {
79
+ local branch=$1
80
+ branch=${branch//\//-}
81
+ branch=${branch//:/-}
82
+ branch=${branch// /-}
83
+ printf '%s' "$branch"
84
+ }
85
+
86
+ repository_slug_from_remote() {
87
+ local remote_url=$1
88
+ local slug
89
+
90
+ slug=$(printf '%s' "$remote_url" | sed -E 's#^[a-z]+://[^/]+/##; s#^git@[^:]+:##; s#\.git$##')
91
+ if [[ -z "$slug" || "$slug" == "$remote_url" ]]; then
92
+ die "unable to derive repository slug from remote: $remote_url"
93
+ fi
94
+
95
+ printf '%s' "$slug"
96
+ }
97
+
98
+ load_kv_output() {
99
+ local output=$1
100
+ eval "$output"
101
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
5
+ # shellcheck source=./common.sh
6
+ source "$SCRIPT_DIR/common.sh"
7
+
8
+ repo=.
9
+ remote_url=
10
+
11
+ usage() {
12
+ cat <<'USAGE'
13
+ Usage: detect_platform.sh [--repo <path>] [--remote-url <url>]
14
+
15
+ Detect the remote VCS platform from a repository or explicit remote URL.
16
+ Prints one of: github, gitlab
17
+ USAGE
18
+ }
19
+
20
+ while [[ $# -gt 0 ]]; do
21
+ case "$1" in
22
+ --repo)
23
+ repo=$2
24
+ shift 2
25
+ ;;
26
+ --remote-url)
27
+ remote_url=$2
28
+ shift 2
29
+ ;;
30
+ --help|-h)
31
+ usage
32
+ exit 0
33
+ ;;
34
+ *)
35
+ die "unknown argument: $1"
36
+ ;;
37
+ esac
38
+ done
39
+
40
+ if [[ -z "$remote_url" ]]; then
41
+ repo=$(resolve_repo_root "$repo")
42
+ remote_url=$(origin_url "$repo")
43
+ fi
44
+
45
+ case "$remote_url" in
46
+ *github.com* )
47
+ printf 'github\n'
48
+ ;;
49
+ *gitlab* )
50
+ printf 'gitlab\n'
51
+ ;;
52
+ * )
53
+ die "unsupported remote platform: $remote_url"
54
+ ;;
55
+ esac
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
5
+ # shellcheck source=./common.sh
6
+ source "$SCRIPT_DIR/common.sh"
7
+
8
+ repo=.
9
+ platform=
10
+ number=
11
+ json_output=false
12
+
13
+ usage() {
14
+ cat <<'USAGE'
15
+ Usage: fetch_review_comments.sh --repo <path> --number <id> [--platform <github|gitlab>] [--json]
16
+
17
+ Fetch MR/PR comments and normalize them into code-review comments and discussion comments.
18
+ USAGE
19
+ }
20
+
21
+ while [[ $# -gt 0 ]]; do
22
+ case "$1" in
23
+ --repo)
24
+ repo=$2
25
+ shift 2
26
+ ;;
27
+ --platform)
28
+ platform=$2
29
+ shift 2
30
+ ;;
31
+ --number)
32
+ number=$2
33
+ shift 2
34
+ ;;
35
+ --json)
36
+ json_output=true
37
+ shift
38
+ ;;
39
+ --help|-h)
40
+ usage
41
+ exit 0
42
+ ;;
43
+ *)
44
+ die "unknown argument: $1"
45
+ ;;
46
+ esac
47
+ done
48
+
49
+ [[ -n "$number" ]] || die "--number is required"
50
+
51
+ repo=$(resolve_repo_root "$repo")
52
+ require_cmd jq
53
+
54
+ if [[ -z "$platform" ]]; then
55
+ platform=$("$SCRIPT_DIR/detect_platform.sh" --repo "$repo")
56
+ fi
57
+
58
+ remote_url=$(origin_url "$repo")
59
+ repository=$(repository_slug_from_remote "$remote_url")
60
+
61
+ normalize_github_comments() {
62
+ local review_comments=$1
63
+ local discussion_comments=$2
64
+ local review_threads=$3
65
+
66
+ jq -cn \
67
+ --arg platform "$platform" \
68
+ --arg repository "$repository" \
69
+ --arg number "$number" \
70
+ --argjson review_comments "$review_comments" \
71
+ --argjson discussion_comments "$discussion_comments" \
72
+ --argjson review_threads "$review_threads" \
73
+ '
74
+ def github_thread_lookup:
75
+ (
76
+ $review_threads.data.repository.pullRequest.reviewThreads.nodes // []
77
+ )
78
+ | map(
79
+ . as $thread
80
+ | ($thread.comments.nodes // [])[]
81
+ | {
82
+ key: (.databaseId | tostring),
83
+ value: {
84
+ thread_id: ($thread.id // null),
85
+ thread_state: (
86
+ if ($thread.isResolved // false) then "resolved"
87
+ elif ($thread.isOutdated // false) then "outdated"
88
+ else "unresolved"
89
+ end
90
+ ),
91
+ thread_resolved: ($thread.isResolved // false),
92
+ thread_outdated: ($thread.isOutdated // false)
93
+ }
94
+ }
95
+ )
96
+ | from_entries;
97
+
98
+ {
99
+ platform: $platform,
100
+ repository: $repository,
101
+ number: $number,
102
+ code_review_comments: {
103
+ count: ($review_comments | length),
104
+ items: (
105
+ github_thread_lookup as $thread_lookup
106
+ | $review_comments
107
+ | map(
108
+ (.id | tostring) as $comment_id
109
+ | {
110
+ id: $comment_id,
111
+ author: (.user.login // .user.name // "unknown"),
112
+ body: (.body // ""),
113
+ path: (.path // ""),
114
+ start_line: (if .start_line == null then null else (.start_line | tostring) end),
115
+ line: (if .line == null then "" else (.line | tostring) end),
116
+ side: (.side // null),
117
+ start_side: (.start_side // null),
118
+ subject_type: (.subject_type // null),
119
+ original_line: (if .original_line == null then null else (.original_line | tostring) end),
120
+ original_start_line: (if .original_start_line == null then null else (.original_start_line | tostring) end),
121
+ original_position: (if .original_position == null then null else (.original_position | tostring) end),
122
+ commit_id: (.commit_id // null),
123
+ original_commit_id: (.original_commit_id // null),
124
+ diff_hunk: (.diff_hunk // null),
125
+ url: (.html_url // ""),
126
+ created_at: (.created_at // "")
127
+ }
128
+ + ($thread_lookup[$comment_id] // {})
129
+ )
130
+ )
131
+ },
132
+ discussion_comments: {
133
+ count: ($discussion_comments | length),
134
+ items: ($discussion_comments | map({
135
+ id: (.id | tostring),
136
+ author: (.user.login // .user.name // "unknown"),
137
+ body: (.body // ""),
138
+ url: (.html_url // ""),
139
+ created_at: (.created_at // "")
140
+ }))
141
+ }
142
+ }
143
+ '
144
+ }
145
+
146
+ normalize_gitlab_comments() {
147
+ local discussions=$1
148
+
149
+ jq -cn \
150
+ --arg platform "$platform" \
151
+ --arg repository "$repository" \
152
+ --arg number "$number" \
153
+ --argjson discussions "$discussions" \
154
+ '
155
+ def active_notes:
156
+ [
157
+ $discussions[]
158
+ | .notes[]?
159
+ | select((.system // false) | not)
160
+ ];
161
+
162
+ def review_notes:
163
+ [
164
+ active_notes[]
165
+ | . as $note
166
+ | $discussions[]
167
+ | select(any(.notes[]?; (.id // null) == ($note.id // null)))
168
+ | . as $discussion
169
+ | $note
170
+ | select(
171
+ (.position // null) != null
172
+ or (.line_code // null) != null
173
+ )
174
+ | . + {
175
+ thread_id: ($discussion.id // null),
176
+ thread_resolved: ($discussion.resolved // false),
177
+ thread_state: (
178
+ if ($discussion.resolved // false) then "resolved"
179
+ else "unresolved"
180
+ end
181
+ ),
182
+ resolved_by: ($discussion.resolved_by // null),
183
+ resolved_at: ($discussion.resolved_at // null)
184
+ }
185
+ ];
186
+
187
+ def discussion_notes:
188
+ [
189
+ active_notes[]
190
+ | select(
191
+ (.position // null) == null
192
+ and (.line_code // null) == null
193
+ )
194
+ ];
195
+
196
+ {
197
+ platform: $platform,
198
+ repository: $repository,
199
+ number: $number,
200
+ code_review_comments: {
201
+ count: (review_notes | length),
202
+ items: (review_notes | map({
203
+ id: (.id | tostring),
204
+ author: (.author.username // .author.name // "unknown"),
205
+ body: (.body // ""),
206
+ path: (.position.new_path // .position.old_path // ""),
207
+ line: (
208
+ if (.position.new_line // null) != null then (.position.new_line | tostring)
209
+ elif (.position.old_line // null) != null then (.position.old_line | tostring)
210
+ else ""
211
+ end
212
+ ),
213
+ new_path: (.position.new_path // null),
214
+ old_path: (.position.old_path // null),
215
+ new_line: (if (.position.new_line // null) != null then (.position.new_line | tostring) else null end),
216
+ old_line: (if (.position.old_line // null) != null then (.position.old_line | tostring) else null end),
217
+ base_sha: (.position.base_sha // null),
218
+ start_sha: (.position.start_sha // null),
219
+ head_sha: (.position.head_sha // null),
220
+ position_type: (.position.position_type // null),
221
+ line_range: (.position.line_range // null),
222
+ position: (.position // null),
223
+ thread_id: (.thread_id // null),
224
+ thread_state: (.thread_state // "unresolved"),
225
+ thread_resolved: (.thread_resolved // false),
226
+ resolved_by: (.resolved_by // null),
227
+ resolved_at: (.resolved_at // null),
228
+ url: (.url // .web_url // ""),
229
+ created_at: (.created_at // "")
230
+ }))
231
+ },
232
+ discussion_comments: {
233
+ count: (discussion_notes | length),
234
+ items: (discussion_notes | map({
235
+ id: (.id | tostring),
236
+ author: (.author.username // .author.name // "unknown"),
237
+ body: (.body // ""),
238
+ url: (.url // .web_url // ""),
239
+ created_at: (.created_at // "")
240
+ }))
241
+ }
242
+ }
243
+ '
244
+ }
245
+
246
+ emit_kv() {
247
+ local json=$1
248
+ print_kv platform "$(printf '%s' "$json" | jq -r '.platform')"
249
+ print_kv repository "$(printf '%s' "$json" | jq -r '.repository')"
250
+ print_kv number "$(printf '%s' "$json" | jq -r '.number')"
251
+ print_kv code_review_comment_count "$(printf '%s' "$json" | jq -r '.code_review_comments.count')"
252
+ print_kv discussion_comment_count "$(printf '%s' "$json" | jq -r '.discussion_comments.count')"
253
+ }
254
+
255
+ cd "$repo"
256
+
257
+ case "$platform" in
258
+ github)
259
+ require_cmd gh
260
+ review_comments=$(gh api "repos/$repository/pulls/$number/comments")
261
+ discussion_comments=$(gh api "repos/$repository/issues/$number/comments")
262
+ owner=${repository%%/*}
263
+ repo_name=${repository#*/}
264
+ review_threads=$(gh api graphql -f query='
265
+ query($owner: String!, $repo: String!, $number: Int!) {
266
+ repository(owner: $owner, name: $repo) {
267
+ pullRequest(number: $number) {
268
+ reviewThreads(first: 100) {
269
+ nodes {
270
+ id
271
+ isResolved
272
+ isOutdated
273
+ comments(first: 100) {
274
+ nodes {
275
+ databaseId
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+ ' -F owner="$owner" -F repo="$repo_name" -F number="$number")
284
+ normalized=$(normalize_github_comments "$review_comments" "$discussion_comments" "$review_threads")
285
+ ;;
286
+ gitlab)
287
+ require_cmd glab
288
+ encoded_repository=$(jq -nr --arg value "$repository" '$value | @uri')
289
+ discussions=$(glab api "projects/$encoded_repository/merge_requests/$number/discussions" --paginate)
290
+ normalized=$(normalize_gitlab_comments "$discussions")
291
+ ;;
292
+ *)
293
+ die "unsupported platform: $platform"
294
+ ;;
295
+ esac
296
+
297
+ if [[ "$json_output" == true ]]; then
298
+ printf '%s\n' "$normalized"
299
+ else
300
+ emit_kv "$normalized"
301
+ fi