diff --git a/.claude/commands/implement-unblocked.md b/.claude/commands/implement-unblocked.md new file mode 100644 index 0000000..51b993f --- /dev/null +++ b/.claude/commands/implement-unblocked.md @@ -0,0 +1,193 @@ +--- +description: Assign every unblocked, unassigned issue not awaiting a decision or a major version to the gh user, move it to In Progress, and implement each one as a PR from its own worktree via sub-agents. +argument-hint: "[issue numbers to restrict to] [--dry-run]" +allowed-tools: Bash(gh:*), Bash(git:*), Agent +--- + +# Implement unblocked issues + +You are the **fanning agent** described under *Worktrees* in `AGENTS.md`. You provision one worktree +per issue up front, then hand each sub-agent a path that already exists. Sub-agents only do the +work. They never provision. + +Arguments: `$ARGUMENTS` + +* Bare numbers restrict the run to those issues. They must still pass the filter in step 2. +* `--dry-run` stops after step 2. Print the issues that would be picked up and change nothing. + +The repository is `PaulTrampert/PTrampert.SimplePatch`. + +## 1. Preflight + +If any check fails, stop and tell the user how to fix it. Don't work around a failure. + +* `gh auth status` must list the `project` scope, which moving an issue on a project board needs. + If it's missing, ask the user to run `! gh auth refresh -s project`. +* Bring `main` up to date so every worktree starts from the latest `main`, not from whatever this + clone last fetched. Find the primary checkout from the shared git directory so this works from + any worktree. Then fetch and pin the commit the whole batch branches from: + + ```bash + PRIMARY="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")" + git -C "$PRIMARY" fetch origin main + BASE="$(git -C "$PRIMARY" rev-parse origin/main)" + git -C "$PRIMARY" fetch . origin/main:main # fast-forward local main; refuses if it has diverged + ``` + + If the fetch fails, stop. If git refuses the fast-forward because local `main` has commits that + aren't on `origin/main`, stop and report those commits. Never reset `main`. If git refuses only + because `main` is checked out in a worktree, and that tree is clean, run + `git merge --ff-only origin/main` there. If it isn't clean, leave it and note it for the final + report. Either way, the worktrees branch from `$BASE`. + +## 2. Find the issues + +An issue qualifies when all of the following hold: + +* It is **open**. +* It has **no assignees**. +* It has **no open blocking issue** (GitHub issue dependencies). A closed blocker no longer blocks. +* It has **no open pull request** that will close it. Someone is already working on it. +* It isn't labelled **`needs decision`**, meaning it isn't defined well enough to implement yet. +* It isn't labelled **`Breaking Change`**, meaning it has to wait for a major version. + +Naming an issue in the arguments doesn't override the label rules. The label has to be removed +first. + +```bash +gh api graphql --paginate -f query=' +query($endCursor: String) { + repository(owner: "PaulTrampert", name: "PTrampert.SimplePatch") { + issues(states: OPEN, first: 100, after: $endCursor) { + pageInfo { hasNextPage endCursor } + nodes { + number + title + labels(first: 20) { nodes { name } } + assignees(first: 1) { totalCount } + blockedBy(first: 50) { nodes { number state } } + closedByPullRequestsReferences(first: 10, includeClosedPrs: false) { nodes { number state } } + } + } + } +}' --jq '.data.repository.issues.nodes[] + | select(.assignees.totalCount == 0) + | select([.labels.nodes[].name] | (index("needs decision") or index("Breaking Change")) | not) + | select([.blockedBy.nodes[] | select(.state == "OPEN")] | length == 0) + | select([.closedByPullRequestsReferences.nodes[] | select(.state == "OPEN")] | length == 0) + | {number, title, labels: [.labels.nodes[].name]}' +``` + +Print the list with each issue's number and title. If the list is empty, say so and stop. If +`--dry-run` was given, stop here. + +## 3. Claim them + +For each issue: + +1. Assign it to the account `gh` is logged in as: + `gh issue edit --repo PaulTrampert/PTrampert.SimplePatch --add-assignee @me`. +2. Move it to **In Progress** on every project board it belongs to. Look up the issue's project + items and each project's `Status` field: + + ```bash + gh api graphql -f query=' + query($n: Int!) { + repository(owner: "PaulTrampert", name: "PTrampert.SimplePatch") { + issue(number: $n) { + projectItems(first: 10) { + nodes { + id + project { + id + title + field(name: "Status") { + ... on ProjectV2SingleSelectField { id options { id name } } + } + } + } + } + } + } + }' -F n= + ``` + + Then set the option whose name is `In Progress`, compared case-insensitively: + + ```bash + gh project item-edit --id --project-id \ + --field-id --single-select-option-id