Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .claude/skills/codetrial-contribute/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: codetrial-contribute
description: Help CodeTrial contributors turn observations into clear English GitHub issues, small contribution plans, and pull request descriptions, and see them through. Use to draft, improve or file a CodeTrial issue, answer what a maintainer asked on it (a revision, a log, an initial analysis, a retest), prepare a PR description, respond to review requests such as rebase, squash or commit message fixes, check which of your own issues and PRs are waiting on you, ask a focused maintainer question, or choose a bounded first contribution with an AI agent. The deliverable is copy-ready text; judging an existing backlog for duplicates is codetrial-triage. Opening a PR goes through gh-submit when it is installed; this skill covers it otherwise.
description: Help CodeTrial contributors turn observations into clear English GitHub issues, small contribution plans, and pull request descriptions, and see them through. Use to draft, improve or file a CodeTrial issue, answer what a maintainer asked on it (a revision, a log, an initial analysis, a retest), prepare a PR description, retrieve and address PR review comments, respond to requests such as rebase, squash or commit message fixes, check which of your own issues and PRs are waiting on you, ask a focused maintainer question, or choose a bounded first contribution with an AI agent. The deliverable is copy-ready text, or verified fixes when asked to address review feedback; judging an existing backlog for duplicates is codetrial-triage. Opening a PR goes through gh-submit when it is installed; this skill covers it otherwise.
---

# Contribute to CodeTrial
Expand Down Expand Up @@ -235,6 +235,10 @@ everyone's list.

## Answer a review

Fetching every thread, including long ones, and rechecking the head after a
push, and where failed checks are diagnosed, are in
[review-feedback.md](review-feedback.md).

Git and commit-message requests recur alongside substantive code review. Read
the current inline threads, unanswered cubic findings and failing checks as
well as the review summary. The one-line requests are decoded in
Expand Down
56 changes: 56 additions & 0 deletions .claude/skills/codetrial-contribute/review-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Retrieve and close out review feedback

What a request means, how to answer it and the local list of open requests are
in [Answer a review](SKILL.md#answer-a-review); this file only covers fetching
every thread and confirming the result. A request only to review or summarize a
PR stays read-only. When the user names a subset of comments, address that
subset; "address all the feedback" is the whole list, not a prompt to ask which.

Run `gh auth status` first, so an access failure is reported as unreviewed
feedback rather than as a PR with none. Top-level comments and reviews come from
`gh pr view`; inline threads need their own query, with the ids and URLs that
replying and resolving need:

```sh
gh pr view "$pr" --repo sysprog21/codetrial \
--json number,url,headRefOid,comments,reviews
gh api graphql --paginate -F n="$pr" -f query='query($n: Int!, $endCursor: String) {
repository(owner: "sysprog21", name: "codetrial") { pullRequest(number: $n) {
reviewThreads(first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes { id isResolved isOutdated path line originalLine
comments(first: 100) {
pageInfo { hasNextPage }
nodes { id author { login } body url } } } } } } }'
```

`--paginate` follows only the outer `pageInfo` and prints one JSON document per
page. A thread whose `comments.pageInfo.hasNextPage` is true is incomplete;
refetch all of its comments, where the thread's own `pageInfo` is the only one,
before reading its last reply:

```sh
gh api graphql --paginate -F thread="$thread" -f query='query($thread: ID!, $endCursor: String) {
node(id: $thread) { ... on PullRequestReviewThread {
comments(first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes { id author { login } body url } } } } }'
```
An outdated position does not mean fixed; compare the request with the current
code. Verify a bot finding by the same standard as any other
([collaboration.md](collaboration.md#review-evidence-including-bot-findings)).

Fix each request with the smallest change and run the affected check, then the
full gate with its skips read; browser behavior goes through
[Browser investigation](../codetrial-verify/SKILL.md#browser-investigation)
and red checks through
[Failed GitHub Actions checks](../codetrial-verify/SKILL.md#failed-github-actions-checks).
If the scope changed, update the PR description to describe the final result.

A push does not prove every request addressed. After it, rerun both queries
above and the checks, so a new top-level review or comment is read as well as
a new thread, and reassess anything a new head or new comment touches.
Replies and resolving follow
[codetrial-conventions](../codetrial-conventions/SKILL.md#pull-request-branches-and-review-replies)
and `CONTRIBUTING.md`, which closes an addressed thread with "Resolve
conversation"; never resolve a disputed or unverified request.
16 changes: 14 additions & 2 deletions .claude/skills/codetrial-triage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ description: >
review, and draft the reminders, acceptances and closes that follow. Use for
backlog triage, checking new issues or PRs, asking what needs attention,
deciding whether a symptom already has an issue, explicit PR sweeps,
approving GitHub Actions runs held for first-time contributors, and asking
conflicting PRs to rebase. Default to a read-only report; a PR sweep has
approving GitHub Actions runs held for first-time contributors, asking
conflicting PRs to rebase, and applying requested label, assignee, issue type,
sub-issue or blocked-by changes. Default to a read-only report; a PR sweep has
narrowly defined actions in pulls.md. Use codetrial-contribute for drafts on
a contributor's behalf, new issues or PRs, and focused maintainer questions.
---
Expand Down Expand Up @@ -403,6 +404,17 @@ that error, comment `Duplicate of #N` and close with `--reason "not planned"`,
which on its own reads as rejected and would take a reopen to correct, notifying
the reporter a second time for nothing.

A sub-issue, a blocked-by dependency and a duplicate are three different
records, and a cross-link in prose establishes none of them. `gh issue edit`
covers labels and assignees; for issue types, sub-issues and dependencies read
GitHub's current API documentation for that operation rather than guessing an
endpoint, check which issue is the parent or the blocker, and pass a node or
database ID where the endpoint wants one instead of an issue number. Put the
request body in a JSON file and send it with
`gh api <endpoint> --method <method> --input "$SCRATCH/payload.json"`, then read
the relationship back. If the operation is unavailable, say so and list the
intended relationship as a proposal, never as applied.

Post any preserved reproduction evidence to the canonical issue before closing.
Do not pair `--duplicate-of` with a `Duplicate of #N` comment; GitHub's marker
already says it. Report applied actions separately from proposals, with links.
50 changes: 38 additions & 12 deletions .claude/skills/codetrial-triage/issue-sweep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
# is the newest comment; stale is null when the history is cut,
# and idle_days too when none of their acts is in what was read
# truncated issues with more than 100 comments or cross-references, 10
# assignees or 20 assignments, whose lists above may miss what
# was cut
# assignees, 20 assignments or 20 body edits, whose lists above
# may miss what was cut

set -eu

Expand All @@ -48,8 +48,11 @@ query='query($owner: String!, $name: String!, $endCursor: String) {
issues(states: OPEN, first: 50, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes {
number title body createdAt lastEditedAt authorAssociation
author { login } editor { login } issueType { name }
number title body createdAt authorAssociation
author { login } issueType { name }
userContentEdits(last: 20) {
totalCount nodes { editedAt editor { login } }
}
assignees(first: 10) { totalCount nodes { login } }
assigned: timelineItems(last: 20, itemTypes: [ASSIGNED_EVENT]) {
totalCount
Expand Down Expand Up @@ -94,7 +97,9 @@ pages=$(gh api graphql --paginate -F owner="${repo%/*}" -F name="${repo#*/}" \
# collaborators with read or triage access, students among them, so the badge
# counts only when the push listing could not be read. A bot's comment never
# asks the reporter for anything. The reporter answering by editing the body
# counts, since the bug form and maintainers both ask for that.
# counts, since the bug form and maintainers both ask for that; every edit is
# read rather than only the last, which a maintainer tidying the body would
# otherwise hide.
printf '%s\n' "$pages" | jq -s --argjson days "$days" --arg repo "$repo" \
--argjson pushers "$pushers" --arg me "$me" '
def ts: if . == null then 0 else fromdateiso8601 end;
Expand All @@ -119,28 +124,48 @@ def ink: [limit(40; split("\n")[] | select(startswith("#") | not)
[.[].data.repository.issues.nodes[]] as $issues |
def maint: [.comments.nodes[] | select(human and staff)];
def cut: .comments.totalCount > 100 or .timelineItems.totalCount > 100
or .assignees.totalCount > 10 or .assigned.totalCount > 20;
or .assignees.totalCount > 10 or .assigned.totalCount > 20
or .userContentEdits.totalCount > 20;
def prs: [.timelineItems.nodes[] | select(.source.number != null
and .source.repository.nameWithOwner == $repo)];
def split: {open: [.[] | select(.state == "OPEN") | .number] | unique,
merged: [.[] | select(.mergedAt != null) | .number] | unique};
def mine: .author.login as $a | [.comments.nodes[]
| select($a != null and .author.login == $a) | .createdAt | ts]
+ [if .editor.login == $a then .lastEditedAt | ts else 0 end] | max;
+ [.userContentEdits.nodes[] | select($a != null and .editor.login == $a)
| .editedAt | ts] + [0] | max;
# Per line rather than one regex over the body, since "the same or a higher
# level" is not a backreference. A fence counts as content and what is inside
# it is skipped, so the comments in a pasted shell session are not headings; it
# closes only on its own character, at least as long, with nothing after it.
def empty_section: [foreach (split("\n")[] | select(test("\\S"))) as $l
({fence: null, level: null};
([$l | capture("^ {0,3}(?<m>`{3,}|~{3,})(?<rest>.*)$")] | first) as $f
| if .fence == null then
if $f != null and ($f.m[0:1] == "~" or ($f.rest | contains("`") | not))
then {fence: $f.m, level: 0}
else .level = (($l | capture("^(?<h>#{1,6}) +\\S") | .h | length) // 0)
end
elif $f != null and $f.m[0:1] == .fence[0:1]
and ($f.m | length) >= (.fence | length) and ($f.rest | test("^\\s*$"))
then {fence: null, level: 0}
else .level = null end;
.level | select(. != null))] as $lv
| any(range($lv | length); . as $k | $lv[$k] > 0
and ($lv[$k + 1] // -1 | . == -1 or (. > 0 and . <= $lv[$k])));
def gaps: (.body // "") as $b
| ($b + " " + ([.author.login as $a | .comments.nodes[]
| select($a != null and .author.login == $a) | .body] | join(" "))) as $said
| [ (select($b | ink < 40) | "empty_body"),
(select($b | test("\\[([Pp]lease )?[Ff]ill in|<[A-Z][a-z][^<>\\n]{15,}>"))
| "placeholder"),
(select($b | test("(?m)^(#{1,6}) [^\\n]+\\n\\s*(^\\1 |\\z)"))
| "empty_section"),
(select($b | empty_section) | "empty_section"),
(select(.title | test("^\\s*(\\[[^\\]]+\\]|[A-Za-z]+(\\([^)]*\\))?:)\\s"))
| "title_prefix"),
(select((.title | cjk > latin) or (($b | cjk) > ($b | latin)))
| "not_english"),
(select(.issueType.name == "Bug"
and ($said | test("codetrial [0-9.]+ \\([0-9a-f]{7,40}\\)|\\b(?=[0-9a-f]*[0-9])(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\\b") | not))
and ($said | test("codetrial [0-9.]+ \\([0-9a-f]{7,40}\\)|\\b(?=[0-9a-f]*[0-9])(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\\b"; "i") | not))
| "no_revision") ];
def claim: . as $i | maint as $m
| (($pushers // []) + [$m[].author.login]
Expand All @@ -150,8 +175,9 @@ def claim: . as $i | maint as $m
| ([$i.assigned.nodes[] | select(.assignee.login == $a) | .createdAt]
+ [$i.comments.nodes[] | select(.author.login == $a) | .createdAt]
+ [$xrefs[].createdAt] | map(ts) | max) as $last
| ([$m[] | select((.createdAt | ts) > ($last // 0)) | .createdAt]
| max) as $check
| (if $last == null then null
else [$m[] | select((.createdAt | ts) > $last) | .createdAt] | max end)
as $check
| (if $check then $check | ts else $last end) as $since
| ([$i.comments.nodes[] | select(human)] | max_by(.createdAt | ts)
| .author.login == $a) as $asked
Expand Down
14 changes: 7 additions & 7 deletions .claude/skills/codetrial-triage/pr-sweep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ known='["Rebase latest `main` and resolve conflicts",
# first check suite, created when the push reached a workflow or app, or the
# latest force push. The commit date, which the contributor's clock sets and
# which can run ahead and make every later request look stale, is used only for
# a head with no check suite.
# a head with no check suite. A PR force-pushed back to its base has no commit
# at all, and dates only its force push.
# shellcheck disable=SC2016
query='query($owner: String!, $name: String!, $endCursor: String) {
repository(owner: $owner, name: $name) {
Expand Down Expand Up @@ -131,7 +132,7 @@ printf '%s\n%s\n' "$prs" "$runs" | jq -s --argjson days "$days" --arg me "$me" \
--argjson pushers "$pushers" '
def human: .author != null and .author.__typename != "Bot";
def ts: if . == null then 0 else fromdateiso8601 end;
def pushed: .commits.nodes[0].commit as $c
def pushed: (.commits.nodes[0].commit // {checkSuites: {nodes: []}}) as $c
| [([$c.checkSuites.nodes[].createdAt | ts] | min) // ($c.committedDate | ts),
(.timelineItems.nodes[0].createdAt | ts)] | max;
def trim: gsub("^\\s+|\\s+$"; "");
Expand All @@ -155,11 +156,10 @@ def requests: .author.login as $a | .headRefOid as $head | pushed as $p |
def fixed_only: .inline == 0 and (.body | norm | IN($wordings[]));
def asks_rebase: [requests[] | select(fixed_only)] | sort_by(.at | ts);
def asked_here: any(requests[]; .current);
# A conflicting PR whose only request on its current head is hand-written:
# that request is the standing one, so no fixed one is kept beside it and it
# is never offered for hiding.
def hand_asked: .mergeable == "CONFLICTING"
and asked_here and all(asks_rebase[]; .current | not);
# A PR whose only request on its current head is hand-written, whether or not
# it still conflicts: that request is the standing one, so no fixed one is kept
# beside it and it is never offered for hiding.
def hand_asked: asked_here and all(asks_rebase[]; .current | not);
def needs_rebase: .mergeable == "CONFLICTING" and (.isDraft | not)
and .baseRefName == $base and $base == "main"
and nudgeable and whole
Expand Down
17 changes: 9 additions & 8 deletions .claude/skills/codetrial-triage/pulls.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,15 @@ gh pr view "$pr" --repo "$repo" --json mergeable,headRefOid \
Post it before hiding anything, so a failed post never leaves a conflicting PR
with no visible request. `hide` lists the earlier requests, reviews and comments
alike, in a known wording and with no inline comments: all of them on a PR in
`rebase`, or on a conflicting PR whose current request is hand-written (that
request stays and is kept out of `hide_ask`); on any other PR, all but one: the
newest on the current head, or the newest overall when none is, since a review
left on an older head can carry a later date. The entries for a PR in `rebase`
assume its new request landed, so hide them only after that PR's post succeeded;
when the check above skipped the post, or the post failed, keep the one that
rule would keep, which is still the current request. Hiding a review does not
dismiss it; the PR stays at changes requested until a reviewer re-reviews.
`rebase`, or on a PR, conflicting or not, whose current request is hand-written
(that request stays and is kept out of `hide_ask`); on any other PR, all but
one: the newest on the current head, or the newest overall when none is, since a
review left on an older head can carry a later date. The entries for a PR in
`rebase` assume its new request landed, so hide them only after that PR's post
succeeded; when the check above skipped the post, or the post failed, keep the
one that rule would keep, which is still the current request. Hiding a review
does not dismiss it; the PR stays at changes requested until a reviewer
re-reviews.

`hide_ask` lists the other requests that mention rebase on a PR that has, or is
about to get, the fixed request: hand-written ones, and known-wording reviews
Expand Down
7 changes: 3 additions & 4 deletions .claude/skills/codetrial-triage/waiting.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ The four sweep lists are the queue, and each one has a fixed next move:
disposition first. When a linked PR merges or the facts change while a
reminder is pending, rescope the request and keep its deadline. The closing
comment names the fact again and says a maintainer reopens the issue once it
is posted, since a comment alone does not reopen it and the sweep reads only
open issues. An
issue a maintainer opened is left out of `waiting`, because its author already
knows the rule.
is posted, since a comment alone does not reopen it and the sweep reads only
open issues. An issue a maintainer opened is left out of `waiting`, because
its author already knows the rule.
- `linked`, with `merged` set: a merged pull request referenced the issue after
it was filed, and GitHub did not close it, so that PR only touched it, or its
`Closes` line named another issue. Read the PR and the issue. When the PR does
Expand Down
Loading