diff --git a/.claude/skills/fix-issue-batch/SKILL.md b/.claude/skills/fix-issue-batch/SKILL.md deleted file mode 100644 index c35840de..00000000 --- a/.claude/skills/fix-issue-batch/SKILL.md +++ /dev/null @@ -1,242 +0,0 @@ ---- -name: fix-issue-batch -description: Batch-fix [Model] or [Rule] issues from the GitHub Project board Backlog column — calls /fix-issue on each one sequentially ---- - -# Fix Issue Batch - -Iterate through `[Model]` or `[Rule]` issues **from the Backlog column of the GitHub Project board** and call `/fix-issue` on each one sequentially. - -## Invocation - -``` -/fix-issue-batch -``` - -## Constants - -GitHub Project board IDs (same as fix-issue / project-pipeline): - -| Constant | Value | -|----------|-------| -| `PROJECT_NUMBER` | `8` | -| `PROJECT_OWNER` | `CodingThrust` | -| `PROJECT_ID` | `PVT_kwDOBrtarc4BRNVy` | -| `STATUS_FIELD_ID` | `PVTSSF_lADOBrtarc4BRNVyzg_GmQc` | -| `STATUS_READY` | `f37d0d80` | - -## Process - -```dot -digraph fix_batch { - rankdir=TB; - "Parse argument" [shape=box]; - "Fetch project board Backlog" [shape=box]; - "Filter & sort" [shape=box]; - "Print queue" [shape=box]; - "Pick next issue" [shape=diamond]; - "Call /fix-issue N" [shape=box]; - "Done" [shape=doublecircle]; - - "Parse argument" -> "Fetch project board Backlog"; - "Fetch project board Backlog" -> "Filter & sort"; - "Filter & sort" -> "Print queue"; - "Print queue" -> "Pick next issue"; - "Pick next issue" -> "Call /fix-issue N" [label="remaining"]; - "Pick next issue" -> "Done" [label="none left"]; - "Call /fix-issue N" -> "Pick next issue"; -} -``` - ---- - -## Step 1: Parse Argument - -Accept one argument: `model` or `rule` (case-insensitive). - -- `model` → filter for issues with `[Model]` in the title -- `rule` → filter for issues with `[Rule]` in the title - -If no argument or invalid argument → stop with: "Usage: `/fix-issue-batch `" - ---- - -## Step 2: Fetch Issues from Project Board Backlog - -Fetch all items from the GitHub Project board and filter to the **Backlog** column: - -```bash -gh project item-list 8 --owner CodingThrust --format json --limit 500 -``` - -From the JSON result: -1. Filter items where `status == "Backlog"` -2. Keep only items whose `content.title` starts with `[Model]` or `[Rule]` (matching the argument) -3. For each item, extract: issue number (`content.number`), title (`content.title`), and project item ID (`id`) - -Then fetch labels for each matching issue (needed for categorization): - -```bash -gh issue view --repo CodingThrust/problem-reductions --json labels -``` - -Or batch-fetch with a single search query to avoid N+1: - -```bash -gh issue list --repo CodingThrust/problem-reductions \ - --state open --search "[Model] in:title" \ - --json number,title,labels --limit 200 -``` - -Cross-reference the board items with the label data to build the final list. - ---- - -## Step 3: Filter and Sort - -From the Backlog issues: - -1. **Categorize by current label status:** - - `already-good` — has `Good` label - - `has-failures` — has at least one failure label (`PoorWritten`, `Wrong`, `Trivial`, `Useless`) - - `needs-check` — no check-issue comment yet (no `Good`/`PoorWritten`/`Wrong`/`Trivial`/`Useless` label) -2. **Sort by priority then issue number:** `already-good` first, then `has-failures`, then `needs-check`. Within each group, sort by issue number ascending (oldest first). This prioritizes issues that already have a check report and are closest to being ready. - ---- - -## Step 4: Print Queue - -Show the user what will be processed: - -``` -## Fix queue: [Model] issues (N total) - -| # | Issue | Title | Status | -|---|-------|-------|--------| -| 1 | #235 | [Model] SteinerTree | already-good | -| 2 | #233 | [Model] StrongConnectivityAugmentation | has-failures (PoorWritten) | -| 3 | #234 | [Model] FeedbackVertexSet | needs-check | -| ... | ... | ... | ... | - -Priority: already-good → has-failures → needs-check. -Processing N issues total. -``` - -Then ask the user to confirm before starting: - -> Ready to start? I'll process each issue with `/fix-issue`, one at a time. -> -> 1. **Start** — begin processing from the first issue -> 2. **Start from #N** — skip to a specific issue number -> 3. **Cancel** - ---- - -## Step 5: Process Each Issue - -**CRITICAL:** Each issue MUST be dispatched as a **subagent** for analysis, giving it a fresh context window to prevent cutting corners. The subagent does NOT interact with the human — it returns a structured report, then the main agent presents it for human decisions. - -For each issue in the queue (priority order: `already-good` → `has-failures` → `needs-check`): - -### 5a: Check prerequisite - -If no comment starting with `## Issue Quality Check` exists, run `/check-issue ` first. - -### 5b: Dispatch analysis subagent - -``` -Agent tool: - subagent_type: "general-purpose" - description: "Analyze issue #" - prompt: | - Analyze GitHub issue # for fix-issue. - 1. Fetch the issue: gh issue view --json title,body,labels,comments - 2. Find the most recent "## Issue Quality Check" comment - 3. Parse all Fail and Warn results (warnings are NOT ignorable) - 4. For each issue, classify as mechanical or substantive - 5. For mechanical issues: apply the fix to a draft body - 6. For substantive issues: prepare 2-3 concrete options with your recommendation - - Return EXACTLY this format: - - ## Analysis for #: - - ### Auto-fixes applied - | # | Section | Issue | Fix | - |---|---------|-------|-----| - | 1 | ... | ... | ... | - - ### Questions for human - **Q1: <topic>** - <description of the problem> - - (a) <option 1> ← recommended - - (b) <option 2> - - (c) <option 3> - - **Q2: ...** - - ### Draft body - <full updated issue body with mechanical fixes applied, substantive issues marked as `[PENDING Q1]`> -``` - -### 5c: Present to human - -Print the subagent's report and ask the human to answer all questions at once: - -``` -## Issue #<NUMBER> (<current>/<total>): <title> - -<auto-fixes table from subagent> - -<questions from subagent> - -Please answer the questions above (e.g. "Q1: a, Q2: b"), or type "skip" to skip this issue. -``` - -### 5d: Apply answers and finalize - -After human responds: -- If **"skip"**: move to next issue -- Otherwise: apply the human's choices to the draft body, re-check (run 4 quality checks inline), then finalize on GitHub (edit body, post changelog comment, update labels, move to Ready — see fix-issue Steps 6–8) - -### 5e: Continue - -Print progress and ask whether to continue: - -``` -Done #<NUMBER>. (<current>/<total> complete, <remaining> remaining) -Next: #<NEXT_NUMBER> <next_title> -``` - -> 1. **Continue** — process the next issue -> 2. **Skip next** — skip the next issue and continue to the one after -> 3. **Stop** — end the batch here and print summary - ---- - -## Step 6: Summary - -After all issues are processed, print a summary: - -``` -## Batch fix complete - -| Result | Count | Issues | -|--------|-------|--------| -| Fixed & moved to Ready | 7 | #233, #234, #235, #236, #237, #238, #240 | -| Skipped (by user) | 1 | #239 | -| Total | 8 | | -``` - ---- - -## Common Mistakes - -| Mistake | Fix | -|---------|-----| -| Fetching all open issues instead of board | Only process issues from the **Backlog** column of GitHub Project #8 | -| Skipping already-good issues | Process ALL Backlog issues; `already-good` are processed first (highest priority) | -| Not running check-issue first | If no check report exists, run `/check-issue` before `/fix-issue` | -| Processing in random order | Always sort by issue number ascending within priority groups | -| Continuing after user cancels | Respect cancel/skip requests immediately | -| Missing project scopes | Run `gh auth refresh -s read:project,project` if board access fails | diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index 8ad3ed9d..73a059d2 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -10,7 +10,7 @@ Fix errors and warnings from a `check-issue` report. Auto-fixes mechanical issue ## Invocation ``` -/fix-issue <issue-number> +/fix-issue <model|rule> ``` ## Constants @@ -21,6 +21,7 @@ GitHub Project board IDs: |----------|-------| | `PROJECT_ID` | `PVT_kwDOBrtarc4BRNVy` | | `STATUS_FIELD_ID` | `PVTSSF_lADOBrtarc4BRNVyzg_GmQc` | +| `STATUS_BACKLOG` | `ab337660` | | `STATUS_READY` | `f37d0d80` | ## Process @@ -28,6 +29,7 @@ GitHub Project board IDs: ```dot digraph fix_issue { rankdir=TB; + "Pick issue from Backlog" [shape=box]; "Fetch issue + check comment" [shape=box]; "Parse failures & warnings" [shape=box]; "Auto-fix mechanical issues" [shape=box]; @@ -39,6 +41,7 @@ digraph fix_issue { "Ask what to change (free-form)" [shape=box]; "Apply changes + re-check locally" [shape=box]; + "Pick issue from Backlog" -> "Fetch issue + check comment"; "Fetch issue + check comment" -> "Parse failures & warnings"; "Parse failures & warnings" -> "Auto-fix mechanical issues"; "Auto-fix mechanical issues" -> "Present auto-fixes to human"; @@ -54,15 +57,40 @@ digraph fix_issue { --- -## Step 1: Fetch Issue and Check Comment +## Step 1: Pick Next Issue from Backlog + +The argument is `model` or `rule` — determines which issue type (`[Model]` or `[Rule]`) to process. + +### 1a: Fetch candidate list from project board + +```bash +uv run --project scripts scripts/pipeline_board.py backlog <model|rule> --format json +``` + +Returns all Backlog issues of the requested type, sorted by `Good` label first then by issue number: + +```json +{ + "issue_type": "rule", + "items": [ + {"number": 246, "title": "[Rule] A → B", "has_good": true, "labels": ["Good", "rule"]}, + {"number": 91, "title": "[Rule] C to D", "has_good": false, "labels": ["rule"]} + ] +} +``` + +### 1b: Pick the top issue + +Pick the first item from the list. If the list is empty, STOP with message: "No `[Model]`/`[Rule]` issues in Backlog." + +### 1c: Fetch the chosen issue ```bash gh issue view <NUMBER> --json title,body,labels,comments ``` -- Detect issue type from title: `[Rule]` or `[Model]` - Find the **most recent** comment that starts with `## Issue Quality Check` — this is the check-issue report -- If no check comment found, STOP with message: "No check-issue report found. Run `/check-issue <NUMBER>` first." +- If no check comment found, run `/check-issue <NUMBER>` first, then re-fetch the issue --- @@ -99,7 +127,7 @@ Tag each issue as: | Incomplete `(TBD)` in fields derivable from other sections | Fill from context | | Incorrect DOI format | Reformat to `https://doi.org/...` | -**Substantive** (brainstorm with human): +**Substantive** (brainstorm with human, ask for human's input): | Issue pattern | Why human input needed | |--------------|----------------------| @@ -181,6 +209,8 @@ Print results to the human as a summary table (Check / Result / Details). ## Step 7: Ask Human for Decision +Show the human the draft issue body. + Use `AskUserQuestion` to present the options: > The issue has been re-checked locally. What would you like to do? @@ -198,18 +228,15 @@ Apply the requested changes to the draft issue body, re-check locally (Step 6), --- -## Step 8: Finalize (on "Looks good") +## Step 8: Finalize (If human picks 1 "Looks good") Only reached when the human approves. Now push everything to GitHub. ### 8a: Edit the issue body -```bash -# Write updated body to temp file -cat > /tmp/fix_issue_body.md <<'BODYEOF' -$UPDATED_BODY -BODYEOF +Use the Write tool to save the updated body to `/tmp/fix_issue_body.md`, then: +```bash gh issue edit <NUMBER> --body-file /tmp/fix_issue_body.md ``` @@ -238,16 +265,10 @@ gh issue edit <NUMBER> --add-label "Good" ### 8d: Move to Ready on project board +Use the `item_id` obtained from Step 1a: + ```bash -# Find the project item ID for this issue -ITEM_ID=$(gh project item-list 8 --owner CodingThrust --format json | \ - jq -r '.items[] | select(.content.number == <NUMBER>) | .id') - -gh project item-edit \ - --id "$ITEM_ID" \ - --project-id PVT_kwDOBrtarc4BRNVy \ - --field-id PVTSSF_lADOBrtarc4BRNVyzg_GmQc \ - --single-select-option-id f37d0d80 +uv run --project scripts scripts/pipeline_board.py move <ITEM_ID> Ready ``` ### 8e: Confirm diff --git a/scripts/pipeline_board.py b/scripts/pipeline_board.py index 2c13c05b..87b9aa67 100644 --- a/scripts/pipeline_board.py +++ b/scripts/pipeline_board.py @@ -1429,6 +1429,49 @@ def print_candidate_list( return 0 +def backlog_issues( + board_data: dict, + issue_type: str, +) -> list[dict]: + """List Backlog issues of the given type, sorted by Good label first then by number. + + Returns a list of dicts with: number, title, item_id, labels, has_good. + """ + prefix = "[Model]" if issue_type == "model" else "[Rule]" + + check_labels = FAILURE_LABELS | {"Good"} + + results = [] + for item in board_data.get("items", []): + if item.get("status") != STATUS_BACKLOG: + continue + content = item.get("content") or {} + if content.get("type") != "Issue": + continue + title = content.get("title") or "" + if not title.startswith(prefix): + continue + number = content.get("number") + if number is None: + continue + item_labels = set(item.get("labels") or []) + # Only include issues that have been through check-issue + if not (item_labels & check_labels): + continue + has_good = "Good" in item_labels + results.append({ + "number": int(number), + "title": title, + "item_id": item_identity(item), + "labels": sorted(item_labels), + "has_good": has_good, + }) + + # Good-labeled first, then by issue number + results.sort(key=lambda r: (not r["has_good"], r["number"])) + return results + + def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Project board automation helpers.") subparsers = parser.add_subparsers(dest="command", required=True) @@ -1486,6 +1529,13 @@ def parse_args(argv: list[str]) -> argparse.Namespace: move_parser.add_argument("--project-id", default=PROJECT_ID) move_parser.add_argument("--field-id", default=STATUS_FIELD_ID) + fix_parser = subparsers.add_parser("backlog") + fix_parser.add_argument("issue_type", choices=["model", "rule"]) + fix_parser.add_argument("--owner", default="CodingThrust") + fix_parser.add_argument("--project-number", type=int, default=8) + fix_parser.add_argument("--limit", type=int, default=500) + fix_parser.add_argument("--format", choices=["text", "json"], default="json") + return parser.parse_args(argv) @@ -1505,6 +1555,17 @@ def main(argv: list[str] | None = None) -> int: ) return 0 + if args.command == "backlog": + board_data = fetch_board_items(args.owner, args.project_number, args.limit) + results = backlog_issues(board_data, args.issue_type) + if args.format == "json": + print(json.dumps({"issue_type": args.issue_type, "items": results})) + else: + for r in results: + good = "Good" if r["has_good"] else "" + print(f"#{r['number']:<5} {good:5s} {r['title']}") + return 0 if results else 1 + if args.command == "claim-next": if args.mode == "review" and not args.repo: raise SystemExit("--repo is required in claim-next review mode")