From a413cd67c299e67cfec84e3834e682d00092451c Mon Sep 17 00:00:00 2001 From: Michel Schanen Date: Wed, 14 Oct 2026 08:26:06 -0500 Subject: [PATCH] CI: keep only the Fable review of task PRs Remove the automation around the review: the implementer (claude-implement.yml), the review fix job, the pipeline's CI-fix rounds and auto-merge (claude-pipeline.yml), the task chaining after triage (claude-triage.yml) and every `needs-owner` labelling. The owner merges by hand and starts the next task. AGENTS.md describes the new workflow. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/DocPreviewCleanup.yml | 4 +- .github/workflows/Documentation.yml | 3 +- .github/workflows/claude-implement.yml | 208 ------------------- .github/workflows/claude-pipeline.yml | 263 ------------------------ .github/workflows/claude-review.yml | 92 +-------- .github/workflows/claude-triage.yml | 67 ------ AGENTS.md | 74 +++---- 7 files changed, 33 insertions(+), 678 deletions(-) delete mode 100644 .github/workflows/claude-implement.yml delete mode 100644 .github/workflows/claude-pipeline.yml delete mode 100644 .github/workflows/claude-triage.yml diff --git a/.github/workflows/DocPreviewCleanup.yml b/.github/workflows/DocPreviewCleanup.yml index 1e139f5..fc5261a 100644 --- a/.github/workflows/DocPreviewCleanup.yml +++ b/.github/workflows/DocPreviewCleanup.yml @@ -3,8 +3,8 @@ name: Doc Preview Cleanup on: pull_request: types: [closed] - # claude-pipeline.yml merges with github.token, whose PR close triggers nothing: - # it dispatches this workflow with the PR number instead + # by hand, for a PR whose close did not trigger the cleanup (e.g. one merged + # with github.token) workflow_dispatch: inputs: pr: diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 76be65b..28bfd37 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -20,8 +20,7 @@ concurrency: cancel-in-progress: true jobs: - # Not watched by claude-pipeline.yml and not a required check: a red docs - # build does not hold a task PR. The examples run on CUDA, hence the GPU runner. + # Not a required check: a red docs build does not hold a task PR. The examples run on CUDA, hence the GPU runner. docs: name: Build documentation runs-on: [self-hosted, linux, X64, gpu, cuda] diff --git a/.github/workflows/claude-implement.yml b/.github/workflows/claude-implement.yml deleted file mode 100644 index 4d8aaaa..0000000 --- a/.github/workflows/claude-implement.yml +++ /dev/null @@ -1,208 +0,0 @@ -name: Claude implement -run-name: "Claude implement: issue #${{ github.event.inputs.issue_number || github.event.issue.number }} ${{ github.event.issue.title || '' }}" - -# Implements one task issue (label `task`, title `TNN — …`) with Claude Opus 5.5 at -# effort "medium" and opens a PR. Started by adding the label `claude:implement` to -# the issue, or by the Claude pipeline (workflow_dispatch) after the previous task -# merged. See AGENTS.md, section "GitHub workflow". - -on: - issues: - types: [labeled] - workflow_dispatch: - inputs: - issue_number: - description: Number of the task issue to implement - required: true - type: string - -permissions: - contents: write - pull-requests: write - issues: write - actions: read - id-token: write - -jobs: - implement: - if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'claude:implement' - runs-on: ubuntu-latest - # Job-level, not workflow-level: every `labeled` event on any issue creates a - # run whose job is skipped by the `if` above, and a workflow-level group let - # those empty runs queue behind (and cancel each other around) a real one. - concurrency: - group: claude-implement - cancel-in-progress: false - timeout-minutes: 300 - env: - ISSUE: ${{ github.event.inputs.issue_number || github.event.issue.number }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check that the issue is the next task - id: guard - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - issue=$(gh issue view "$ISSUE" --json number,title,state,labels,milestone) - title=$(jq -r .title <<<"$issue") - state=$(jq -r .state <<<"$issue") - labels=$(jq -r '[.labels[].name] | join(",")' <<<"$issue") - milestone=$(jq -r '.milestone.title // ""' <<<"$issue") - fail() { gh issue comment "$ISSUE" --body "Claude implement did not start: $1"; echo "go=false" >> "$GITHUB_OUTPUT"; exit 0; } - task=$(sed -nE 's/^(T[0-9]{2}) — .*/\1/p' <<<"$title") - [ -n "$task" ] || fail "the title does not start with \`TNN — \`." - [ "$state" = "OPEN" ] || fail "the issue is not open." - case ",$labels," in *,task,*) ;; *) fail "the issue has no \`task\` label.";; esac - case ",$labels," in *,on-hold,*) fail "the issue is \`on-hold\`.";; esac - num=$((10#${task#T})) - if [ "$num" -gt 1 ]; then - prev=$(printf 'T%02d' $((num - 1))) - prev_state=$(gh issue list --state all --label task --limit 100 --json title,state \ - --jq ".[] | select(.title | startswith(\"$prev — \")) | .state") - [ "$prev_state" = "CLOSED" ] || fail "the previous task $prev is not closed (state: ${prev_state:-not found})." - fi - open_pr=$(gh pr list --state open --limit 100 --json headRefName --jq "[.[] | select(.headRefName | startswith(\"task/$task-\")) | .headRefName] | first // \"\"") - [ -z "$open_pr" ] || fail "an open PR already exists on branch \`$open_pr\`." - echo "go=true" >> "$GITHUB_OUTPUT" - echo "task=$task" >> "$GITHUB_OUTPUT" - echo "milestone=$milestone" >> "$GITHUB_OUTPUT" - echo "Implementing $task ($title), milestone '$milestone'" - - - uses: julia-actions/setup-julia@latest - if: steps.guard.outputs.go == 'true' - with: - version: '1' - - uses: julia-actions/cache@v2 - if: steps.guard.outputs.go == 'true' - - - name: Instantiate the project and clone the reference repositories - if: steps.guard.outputs.go == 'true' - run: | - set -euo pipefail - julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()' - git clone --quiet --depth 1 https://github.com/exanauts/CUDSS.jl ../CUDSS.jl - git clone --quiet --depth 1 https://github.com/JuliaSmoothOptimizers/KrylovPreconditioners.jl ../KrylovPreconditioners.jl - git config user.name "claude[bot]" - git config user.email "claude[bot]@users.noreply.github.com" - - - name: Implement the task with Claude - id: claude - if: steps.guard.outputs.go == 'true' - uses: anthropics/claude-code-action@v1 - env: - CLAUDE_CODE_EFFORT_LEVEL: medium - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_bots: "claude[bot],github-actions[bot]" - claude_args: | - --model claude-opus-5-5 - --effort medium - --allowedTools "Bash,Edit,MultiEdit,Write,Read,Glob,Grep,LS,WebFetch" - prompt: | - You are the implementing agent of exanauts/SparseDirectSolver.jl, running unattended in - GitHub Actions. Nobody can answer questions: decide, and document the decision in the Report. - - TASK: ${{ steps.guard.outputs.task }} — GitHub issue #${{ env.ISSUE }} (milestone "${{ steps.guard.outputs.milestone }}"). - - Read AGENTS.md first (all of it, including "GitHub workflow"), then the "Session protocol" - of TASKS.md, the task's own section, the PLAN.md sections it names, and the Report blocks - of the previous tasks. Do exactly this one task. - - Environment: ubuntu-latest, Julia 1 on PATH, the project is instantiated, only the - KernelAbstractions CPU backend exists here (no GPU, CUDA.jl is not installed). The - CUDA/AMDGPU results come from CI on the pull request. Reference checkouts (read-only): - ../CUDSS.jl and ../KrylovPreconditioners.jl. - - Steps: - 1. You are on `main`. Create the branch `task/${{ steps.guard.outputs.task }}-`. - 2. Implement the task. Iterate with - `SDS_TEST_GPU=0 SDS_TEST_ONLY="" julia --project=. -e 'using Pkg; Pkg.test()'` - and finish with the full `SDS_TEST_GPU=0 julia --project=. -e 'using Pkg; Pkg.test()'`. - Never weaken, skip or delete tests to get green. - 3. In TASKS.md set the task's status marker and fill in its Report block (template at the - top of the file). Under "Tests" give the CPU counts you measured and write - "CUDA/AMDGPU: pending CI on the PR". Do not edit PLAN.md or other tasks. - 4. Bugs or follow-ups outside the task's scope: open an issue with - `gh issue create --label found-by-agent --title "..." --body "..."` (fields in - .github/ISSUE_TEMPLATE/agent-finding.md) and cite it in the Report. Do not fix them silently. - 5. Commit with a message starting with `${{ steps.guard.outputs.task }}: ` (never commit - Manifest.toml), then `git push -u origin `. Push the branch early as well - (right after creating it, and after each green test subset): the GitHub App token - of this run expires after one hour, and an early push limits what a late failure - loses. If a push or `gh` call fails with 401/403, do not retry and do not print - patches into the log: commit locally, finish the Report, and stop; the workflow's - "Publish the branch" step pushes your commits and opens the PR with its own token. - 6. Open the pull request: - `gh pr create --title "${{ steps.guard.outputs.task }}: " --label claude:pr --milestone "${{ steps.guard.outputs.milestone }}" --body ""` - following .github/pull_request_template.md. The body must contain the exact line - `Closes #${{ env.ISSUE }}`. - 7. If you cannot finish, still push and open the PR as a draft (`--draft`) with the state - described honestly in the body and the Report, and label the issue `needs-owner` - (`gh issue edit ${{ env.ISSUE }} --add-label needs-owner`). - - A Claude reviewer reviews the PR and CI runs the GPU suites; the pipeline merges when both pass. - - - name: Save the Claude execution log - if: always() && steps.guard.outputs.go == 'true' && steps.claude.outputs.execution_file != '' - uses: actions/upload-artifact@v4 - with: - name: claude-execution-${{ steps.guard.outputs.task }}-${{ github.run_id }} - path: ${{ steps.claude.outputs.execution_file }} - retention-days: 14 - - # The Claude App token the action mints lives one hour; a run longer than that - # (T15 took 81 min) cannot push or open the PR at the end. This step publishes - # whatever Claude committed on its `task/TNN-*` branch. With the fine-grained - # PAT `CLAUDE_GH_PAT` (contents, pull requests, issues: write) the push and the - # PR trigger CI and the reviewer like Claude's own would; without it the - # `github.token` is used, which opens the PR but starts no workflow (the owner - # then closes and reopens the PR, or pushes to it, to start CI). - - name: Publish the branch if Claude could not - if: always() && steps.guard.outputs.go == 'true' - env: - GH_TOKEN: ${{ secrets.CLAUDE_GH_PAT || github.token }} - TASK: ${{ steps.guard.outputs.task }} - MILESTONE: ${{ steps.guard.outputs.milestone }} - HAS_PAT: ${{ secrets.CLAUDE_GH_PAT != '' }} - run: | - set -euo pipefail - pr=$(gh pr list --state all --limit 100 --json number,headRefName,createdAt \ - --jq "[.[] | select(.headRefName | startswith(\"task/$TASK-\"))] | sort_by(.createdAt) | last | .number // empty") - if [ -n "$pr" ]; then echo "PR #$pr exists for $TASK; nothing to publish."; exit 0; fi - branch=$(git for-each-ref --format='%(refname:short)' "refs/heads/task/$TASK-*" | head -1) - if [ -z "$branch" ]; then echo "No local task/$TASK-* branch."; exit 0; fi - git checkout --quiet "$branch" - if [ -n "$(git status --porcelain --untracked-files=no)" ]; then - git add -u && git commit --quiet -m "$TASK: uncommitted changes at the end of the implementer run" - fi - if [ "$(git rev-list --count origin/main.."$branch")" = "0" ]; then echo "Branch $branch has no commits."; exit 0; fi - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" - git push --quiet -u origin "$branch" - subject=$(git log -1 --format=%s "$branch") - body=$(printf 'Closes #%s\n\n**Task**: %s — opened by the workflow after the implementer run %s/%s/actions/runs/%s ended without a pull request (the App token had expired). The Report block in TASKS.md and the commit message are the implementer'"'"'s; the body of the PR template is not filled in.\n\n%s\n' \ - "$ISSUE" "$TASK" "${{ github.server_url }}" "${{ github.repository }}" "${{ github.run_id }}" "$(git log -1 --format=%b "$branch")") - gh pr create --title "$subject" --head "$branch" --base main --label claude:pr --milestone "$MILESTONE" --body "$body" - if [ "$HAS_PAT" != "true" ]; then - gh issue comment "$ISSUE" --body "The implementer's branch was published with the workflow token, so CI and the reviewer did not start on its PR: close and reopen the PR (or push to it) to start them. Set the repository secret \`CLAUDE_GH_PAT\` to avoid this." - fi - - - name: Check that a pull request was opened - if: always() && steps.guard.outputs.go == 'true' - env: - GH_TOKEN: ${{ github.token }} - TASK: ${{ steps.guard.outputs.task }} - run: | - set -euo pipefail - pr=$(gh pr list --state all --limit 100 --json number,headRefName,createdAt \ - --jq "[.[] | select(.headRefName | startswith(\"task/$TASK-\"))] | sort_by(.createdAt) | last | .number // empty") - if [ -n "$pr" ]; then echo "PR #$pr exists for $TASK."; exit 0; fi - branch=$(git ls-remote --heads origin "task/$TASK-*" | awk '{print $2}' | head -1) - gh issue edit "$ISSUE" --add-label needs-owner - gh issue comment "$ISSUE" --body "Claude implement finished without opening a pull request (run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}; pushed branch: \`${branch:-none}\`). The execution log is attached to the run as an artifact. Labelled \`needs-owner\`." - echo "::error::No pull request for $TASK after the Claude step" - exit 1 diff --git a/.github/workflows/claude-pipeline.yml b/.github/workflows/claude-pipeline.yml deleted file mode 100644 index bafff4f..0000000 --- a/.github/workflows/claude-pipeline.yml +++ /dev/null @@ -1,263 +0,0 @@ -name: Claude pipeline - -# Drives task PRs (label `claude:pr`) to merge, unattended: -# * CI failed on the PR head -> Claude Opus 5.5 fixes it (max 3 rounds, then `needs-owner`) -# * CI green + reviewer VERDICT: APPROVE -> squash-merge, then start the next task issue -# Pause everything with the repository variable CLAUDE_AUTOPILOT=false, or label the next task -# issue `on-hold`. See AGENTS.md, section "GitHub workflow". - -on: - workflow_run: - workflows: ["Run tests", "Aqua", "Claude review"] - types: [completed] - -permissions: - contents: write - pull-requests: write - issues: write - actions: write - id-token: write - -concurrency: - group: claude-pipeline-${{ github.event.workflow_run.head_sha }} - cancel-in-progress: false - -env: - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - MAX_ROUNDS: 3 - -jobs: - resolve: - if: github.event.workflow_run.event == 'pull_request' && vars.CLAUDE_AUTOPILOT != 'false' - runs-on: ubuntu-latest - outputs: - pr: ${{ steps.pr.outputs.pr }} - head_ref: ${{ steps.pr.outputs.head_ref }} - title: ${{ steps.pr.outputs.title }} - eligible: ${{ steps.pr.outputs.eligible }} - ci_failed: ${{ steps.ci.outputs.ci_failed }} - all_green: ${{ steps.ci.outputs.all_green }} - verdict: ${{ steps.verdict.outputs.verdict }} - steps: - - uses: actions/checkout@v4 - with: - sparse-checkout: .github/scripts - - - name: Find the task PR for this head - id: pr - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - run: | - set -euo pipefail - pr=$(gh api "repos/$GITHUB_REPOSITORY/commits/$HEAD_SHA/pulls" \ - --jq '[.[] | select(.state == "open" and .head.sha == "'"$HEAD_SHA"'" and (.labels | map(.name) | index("claude:pr")) and ((.labels | map(.name) | index("needs-owner")) | not))][0]') - if [ -z "$pr" ] || [ "$pr" = "null" ]; then - echo "No open, current task PR for $HEAD_SHA; nothing to do." - echo "eligible=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - echo "eligible=$(jq -r 'if .draft then "false" else "true" end' <<<"$pr")" >> "$GITHUB_OUTPUT" - echo "pr=$(jq -r .number <<<"$pr")" >> "$GITHUB_OUTPUT" - echo "head_ref=$(jq -r .head.ref <<<"$pr")" >> "$GITHUB_OUTPUT" - echo "title=$(jq -r .title <<<"$pr")" >> "$GITHUB_OUTPUT" - jq -r '"PR #\(.number) \(.title) on \(.head.ref) draft=\(.draft)"' <<<"$pr" - - - name: Inspect CI on this head - id: ci - if: steps.pr.outputs.eligible == 'true' - env: - GH_TOKEN: ${{ github.token }} - EVENT_WORKFLOW: ${{ github.event.workflow_run.name }} - EVENT_CONCLUSION: ${{ github.event.workflow_run.conclusion }} - run: | - set -euo pipefail - ci_failed=false - case "$EVENT_WORKFLOW" in "Run tests"|"Aqua") [ "$EVENT_CONCLUSION" = "failure" ] && ci_failed=true ;; esac - echo "ci_failed=$ci_failed" >> "$GITHUB_OUTPUT" - # Latest run of every CI workflow on this exact head must have concluded successfully. - all_green=true - for wf in "Run tests" "Aqua"; do - c=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$HEAD_SHA&event=pull_request&per_page=50" \ - --jq '[.workflow_runs[] | select(.name == "'"$wf"'")] | sort_by(.run_number) | last | "\(.status)/\(.conclusion)"') - echo "$wf: ${c:-no run}" - [ "$c" = "completed/success" ] || all_green=false - done - echo "all_green=$all_green" >> "$GITHUB_OUTPUT" - - - name: Read the reviewer's verdict for this head - id: verdict - if: steps.pr.outputs.eligible == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - verdict=$(bash .github/scripts/pr-verdict.sh "${{ steps.pr.outputs.pr }}" "$HEAD_SHA") - echo "verdict=$verdict" >> "$GITHUB_OUTPUT" - echo "Verdict: ${verdict:-none yet}" - - ci-fix: - needs: resolve - if: needs.resolve.outputs.eligible == 'true' && needs.resolve.outputs.ci_failed == 'true' - runs-on: ubuntu-latest - # Shared with the review workflow's fix job: never two agents pushing to one branch. - concurrency: - group: claude-fix-${{ needs.resolve.outputs.head_ref }} - cancel-in-progress: false - timeout-minutes: 240 - env: - PR: ${{ needs.resolve.outputs.pr }} - HEAD_REF: ${{ needs.resolve.outputs.head_ref }} - RUN_URL: ${{ github.event.workflow_run.html_url }} - RUN_ID: ${{ github.event.workflow_run.id }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_REF }} - fetch-depth: 0 - - - name: Count CI fix rounds - id: rounds - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - n=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \ - --jq '[.[] | select(.body | startswith("CI-FIX round"))] | length') - n=$((n + 1)) - if [ "$n" -gt "$MAX_ROUNDS" ]; then - gh pr edit "$PR" --add-label needs-owner - gh pr comment "$PR" --body "CI failed again ($RUN_URL) after $MAX_ROUNDS automatic fix rounds; stopping. Labelled \`needs-owner\`." - echo "go=false" >> "$GITHUB_OUTPUT" - else - gh pr comment "$PR" --body "CI-FIX round $n of $MAX_ROUNDS: $RUN_URL failed on $HEAD_SHA; Claude is diagnosing it." - echo "go=true" >> "$GITHUB_OUTPUT" - echo "round=$n" >> "$GITHUB_OUTPUT" - fi - - - uses: julia-actions/setup-julia@latest - if: steps.rounds.outputs.go == 'true' - with: - version: '1' - - uses: julia-actions/cache@v2 - if: steps.rounds.outputs.go == 'true' - - - name: Instantiate the project and clone the reference repositories - if: steps.rounds.outputs.go == 'true' - run: | - set -euo pipefail - julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()' - git clone --quiet --depth 1 https://github.com/exanauts/CUDSS.jl ../CUDSS.jl - git clone --quiet --depth 1 https://github.com/JuliaSmoothOptimizers/KrylovPreconditioners.jl ../KrylovPreconditioners.jl - git config user.name "claude[bot]" - git config user.email "claude[bot]@users.noreply.github.com" - - - name: Fix the CI failure with Claude - if: steps.rounds.outputs.go == 'true' - uses: anthropics/claude-code-action@v1 - env: - CLAUDE_CODE_EFFORT_LEVEL: medium - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_bots: "claude[bot],github-actions[bot]" - claude_args: | - --model claude-opus-5-5 - --effort medium - --allowedTools "Bash,Edit,MultiEdit,Write,Read,Glob,Grep,LS,WebFetch" - prompt: | - You are the implementing agent of exanauts/SparseDirectSolver.jl, running unattended in - GitHub Actions. CI failed on pull request #${{ env.PR }} (branch `${{ env.HEAD_REF }}`, - checked out): ${{ env.RUN_URL }}. This is CI fix round ${{ steps.rounds.outputs.round }} of ${{ env.MAX_ROUNDS }}. - - Read AGENTS.md. Get the failure with `gh run view ${{ env.RUN_ID }} --log-failed` (and - `gh run view ${{ env.RUN_ID }} --json jobs` for the job names: `test-github-cpuonly` is the - KA CPU backend on GitHub runners, `test-gpu (… cuda)` and `test-gpu (… amdgpu)` are the - self-hosted GPU runners). Root-cause it. Only the KA CPU backend exists here, so a GPU-only - failure must be reasoned about from the log and the code; prefer fixes that keep the - CPU and GPU paths identical. Never skip, weaken or delete a test to get green; if a test - cannot pass for a documented reason, mark it `@test_broken` with a comment and list it in - the Report block. If the failure is in the runner itself (no test body ran: checkout, - install, runner lost), change nothing and say so in a PR comment. - Run `SDS_TEST_GPU=0 julia --project=. -e 'using Pkg; Pkg.test()'` until green, update the - Report block in TASKS.md if the change affects it, commit as - `: fix CI round ${{ steps.rounds.outputs.round }}` (never commit Manifest.toml) and - `git push origin ${{ env.HEAD_REF }}`. No rebase, no force-push, no new PR. Finish with one - PR comment (`gh pr comment ${{ env.PR }} --body ...`) stating the root cause and the fix. - - merge: - needs: resolve - if: >- - needs.resolve.outputs.eligible == 'true' && - needs.resolve.outputs.all_green == 'true' && - needs.resolve.outputs.verdict == 'APPROVE' - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - PR: ${{ needs.resolve.outputs.pr }} - TITLE: ${{ needs.resolve.outputs.title }} - steps: - # gh resolves the repository and runs git for `pr merge --delete-branch`, - # so the job needs a checkout (PR #35 failed here without one). - - uses: actions/checkout@v4 - with: - sparse-checkout: .github/scripts - - - name: Squash-merge the PR - id: merge - run: | - set -euo pipefail - if gh pr merge "$PR" --squash --delete-branch --match-head-commit "$HEAD_SHA"; then - echo "merged=true" >> "$GITHUB_OUTPUT" - # The merge pushes to main with github.token, which triggers no `push` - # workflows: build and deploy the dev docs by hand (Documentation.yml). - gh workflow run Documentation.yml --ref main || true - gh workflow run DocPreviewCleanup.yml --ref main -f pr="$PR" || true - # Close the task issue ourselves: GitHub's `Closes #N` auto-close did not - # fire on #42, and the next task's guard requires the previous issue closed. - for n in $(gh pr view "$PR" --json body --jq .body | grep -oiE '(close[sd]?|fix(e[sd])?|resolve[sd]?) #[0-9]+' | grep -oE '[0-9]+' | sort -u); do - gh issue close "$n" --reason completed --comment "Merged in #$PR." || true - done - else - gh pr edit "$PR" --add-label needs-owner - gh pr comment "$PR" --body "CI is green and the reviewer approved, but the merge failed (conflict with \`main\` or a branch rule). Labelled \`needs-owner\`." - echo "merged=false" >> "$GITHUB_OUTPUT" - fi - - - name: Start the next task - if: steps.merge.outputs.merged == 'true' - run: | - set -euo pipefail - task=$(sed -nE 's/^(T[0-9]{2}):.*/\1/p' <<<"$TITLE") - if [ -z "$task" ]; then - # Not a task PR (an owner PR closing found-by-agent issues): the issue - # closes above were made with github.token and trigger nothing, so run - # the triage workflow by hand; it starts the next task if nothing holds it. - echo "PR title has no task id; dispatching claude-triage.yml." - gh workflow run claude-triage.yml || true - exit 0 - fi - # next task: the lowest open TNN issue above the merged one. Usually T(NN+1); when - # that id is already closed (a task done out of order, or tasks renumbered), the - # chain continues with the next open id instead of stopping. - issue=$(gh issue list --state open --label task --limit 100 --json number,title,labels \ - --jq --argjson nn "$((10#${task#T}))" \ - '[.[] | select(.title | test("^T[0-9]{2} — ")) | . + {id: (.title[1:3] | tonumber)} | select(.id > $nn)] | sort_by(.id) | .[0]') - if [ -z "$issue" ] || [ "$issue" = "null" ]; then echo "No open task issue after $task."; exit 0; fi - n=$(jq -r .number <<<"$issue") - next=$(printf 'T%02d' "$(jq -r .id <<<"$issue")") - if jq -e '.labels | map(.name) | index("on-hold") or index("external")' <<<"$issue" >/dev/null; then - echo "Issue #$n ($next) is on-hold or external; not chaining."; exit 0 - fi - # triage gate: every open found-by-agent issue must carry `triaged` (or be closed) - untriaged=$(gh issue list --state open --label found-by-agent --limit 100 --json number,labels \ - --jq '[.[] | select(.labels | map(.name) | index("triaged") | not) | "#\(.number)"] | join(" ")') - if [ -n "$untriaged" ]; then - gh issue comment "$n" --body "Previous task merged in #$PR, but $next is **held**: open \`found-by-agent\` issues without the \`triaged\` label: $untriaged. Label them \`triaged\` (or close them) and the pipeline starts this task automatically (claude-triage.yml)." - echo "Held $next (#$n): untriaged issues $untriaged."; exit 0 - fi - gh issue edit "$n" --add-label claude:implement - gh workflow run claude-implement.yml -f issue_number="$n" - gh issue comment "$n" --body "Previous task merged in #$PR; the Claude implementer was started for this issue by the pipeline." - echo "Started $next (#$n)." diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index fa506d8..acfe8e9 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -1,8 +1,8 @@ name: Claude review # Reviews every task PR with Claude Fable 5.1 at effort "high". The review ends with a -# `VERDICT:` line. On CHANGES_REQUESTED, Claude Opus 5.5 (effort "medium") addresses the -# review on the same branch, at most 3 rounds, then the PR is labelled `needs-owner`. +# `VERDICT:` line. On CHANGES_REQUESTED nothing is fixed automatically: the owner decides +# how the findings are addressed (a push to the PR starts a new review). # See AGENTS.md, section "GitHub workflow". on: @@ -27,7 +27,6 @@ env: PR: ${{ github.event.pull_request.number }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} HEAD_REF: ${{ github.event.pull_request.head.ref }} - MAX_ROUNDS: 3 jobs: review: @@ -39,8 +38,6 @@ jobs: contains(github.event.pull_request.labels.*.name, 'claude:pr')) runs-on: ubuntu-latest timeout-minutes: 90 - outputs: - verdict: ${{ steps.verdict.outputs.verdict }} steps: - uses: actions/checkout@v4 with: @@ -66,7 +63,7 @@ jobs: The PR implements one task of TASKS.md (its id `TNN` is the title prefix). Read AGENTS.md, the task's section in TASKS.md on `main` (`git show origin/main:TASKS.md`), the PLAN.md sections it names, and the diff (`git diff origin/main...HEAD`, `gh pr diff ${{ env.PR }}`). - The implementer could only run the KA CPU backend; CUDA/AMDGPU run in CI on this PR. + CUDA/AMDGPU run in CI on this PR. Review for, in this order: 1. Scope: exactly the task's deliverables, nothing from later tasks; PLAN.md untouched; @@ -90,7 +87,7 @@ jobs: 5. The Report block: every claim matches the diff; test counts are plausible; deviations and follow-ups are listed and follow-ups have issues. 6. CI (`gh pr checks ${{ env.PR }}`): pending or failing checks are handled by the - pipeline and are not by themselves a reason to request changes, but a Report that + owner and are not by themselves a reason to request changes, but a Report that claims green tests which CI contradicts is. Output: @@ -116,84 +113,3 @@ jobs: verdict=$(bash .github/scripts/pr-verdict.sh "$PR" "$HEAD_SHA") echo "verdict=$verdict" >> "$GITHUB_OUTPUT" echo "Verdict for $HEAD_SHA: ${verdict:-none}" - - fix: - needs: review - if: needs.review.outputs.verdict == 'CHANGES_REQUESTED' - runs-on: ubuntu-latest - # Shared with the pipeline's ci-fix job: never two agents pushing to one branch. - concurrency: - group: claude-fix-${{ github.event.pull_request.head.ref }} - cancel-in-progress: false - timeout-minutes: 240 - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_REF }} - fetch-depth: 0 - - - name: Count review rounds - id: rounds - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - n=$(bash .github/scripts/pr-verdict.sh "$PR" --count-changes-requested) - echo "Review rounds with CHANGES_REQUESTED so far: $n (max $MAX_ROUNDS)" - if [ "$n" -gt "$MAX_ROUNDS" ]; then - gh pr edit "$PR" --add-label needs-owner - gh pr comment "$PR" --body "The Claude reviewer requested changes $n times; the automatic fix loop stops here (max $MAX_ROUNDS). Labelled \`needs-owner\`." - echo "go=false" >> "$GITHUB_OUTPUT" - else - echo "go=true" >> "$GITHUB_OUTPUT" - echo "round=$n" >> "$GITHUB_OUTPUT" - fi - - - uses: julia-actions/setup-julia@latest - if: steps.rounds.outputs.go == 'true' - with: - version: '1' - - uses: julia-actions/cache@v2 - if: steps.rounds.outputs.go == 'true' - - - name: Instantiate the project and clone the reference repositories - if: steps.rounds.outputs.go == 'true' - run: | - set -euo pipefail - julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()' - git clone --quiet --depth 1 https://github.com/exanauts/CUDSS.jl ../CUDSS.jl - git clone --quiet --depth 1 https://github.com/JuliaSmoothOptimizers/KrylovPreconditioners.jl ../KrylovPreconditioners.jl - git config user.name "claude[bot]" - git config user.email "claude[bot]@users.noreply.github.com" - - - name: Address the review with Claude - if: steps.rounds.outputs.go == 'true' - uses: anthropics/claude-code-action@v1 - env: - CLAUDE_CODE_EFFORT_LEVEL: medium - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_bots: "claude[bot],github-actions[bot]" - claude_args: | - --model claude-opus-5-5 - --effort medium - --allowedTools "Bash,Edit,MultiEdit,Write,Read,Glob,Grep,LS,WebFetch" - prompt: | - You are the implementing agent of exanauts/SparseDirectSolver.jl, running unattended in - GitHub Actions. Pull request #${{ env.PR }} (branch `${{ env.HEAD_REF }}`, checked out) received - `VERDICT: CHANGES_REQUESTED` from the Claude reviewer; this is fix round ${{ steps.rounds.outputs.round }} of ${{ env.MAX_ROUNDS }}. - - Read AGENTS.md, then the review: `gh pr view ${{ env.PR }} --comments`, - `gh api repos/${{ github.repository }}/pulls/${{ env.PR }}/reviews`, - `gh api repos/${{ github.repository }}/pulls/${{ env.PR }}/comments` (inline threads). - Address every blocking finding for the current head (${{ env.HEAD_SHA }}) in code, tests or - the Report block of TASKS.md. Where a finding is wrong, leave the code and reply on that - thread (`gh api -X POST repos/${{ github.repository }}/pulls/${{ env.PR }}/comments//replies -f body=...`) - explaining why. Never weaken, skip or delete tests. Only the KA CPU backend exists here: - run `SDS_TEST_GPU=0 julia --project=. -e 'using Pkg; Pkg.test()'` until green. - Update the Report block if the changes affect it. Commit with the message - `: address review round ${{ steps.rounds.outputs.round }}` (never commit Manifest.toml) - and `git push origin ${{ env.HEAD_REF }}`. No rebase, no force-push, no new PR. - Finish with one PR comment (`gh pr comment ${{ env.PR }} --body ...`) listing what changed - and which findings you rejected and why. diff --git a/.github/workflows/claude-triage.yml b/.github/workflows/claude-triage.yml deleted file mode 100644 index b7bf6ed..0000000 --- a/.github/workflows/claude-triage.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Claude triage - -# Resumes the task chain after the owner triages the found-by-agent issues that -# held it: when a found-by-agent issue gets the `triaged` label or is closed and -# no untriaged one remains, the next task issue (lowest TNN whose predecessor is -# closed) is started unless a task is already running or has an open PR. -# See AGENTS.md, section "GitHub workflow". - -on: - issues: - types: [labeled, closed] - # Dispatched by the pipeline after it merges a PR that is not a task PR: such a - # merge closes issues with `github.token`, whose events never trigger this - # workflow, so #66's close by #72 left T15 unstarted. - workflow_dispatch: - -permissions: - issues: write - pull-requests: read - actions: write - -jobs: - resume: - if: >- - vars.CLAUDE_AUTOPILOT != 'false' && - (github.event_name == 'workflow_dispatch' || - (contains(github.event.issue.labels.*.name, 'found-by-agent') && - (github.event.action == 'closed' || github.event.label.name == 'triaged'))) - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - steps: - - name: Start the next task if nothing holds it - run: | - set -euo pipefail - untriaged=$(gh issue list --state open --label found-by-agent --limit 100 --json number,labels \ - --jq '[.[] | select(.labels | map(.name) | index("triaged") | not) | "#\(.number)"] | join(" ")') - if [ -n "$untriaged" ]; then echo "Still untriaged: $untriaged."; exit 0; fi - running=$(gh run list --workflow claude-implement.yml --status in_progress --json databaseId --jq 'length') - queued=$(gh run list --workflow claude-implement.yml --status queued --json databaseId --jq 'length') - if [ "$running" != "0" ] || [ "$queued" != "0" ]; then echo "An implementer run is active."; exit 0; fi - open_pr=$(gh pr list --state open --limit 100 --json headRefName --jq '[.[] | select(.headRefName | startswith("task/"))] | length') - if [ "$open_pr" != "0" ]; then echo "A task PR is open."; exit 0; fi - # next task: the lowest open TNN issue whose predecessor is closed - n=$(gh issue list --state all --label task --limit 100 --json number,title,state,labels | python3 -c ' - import json, re, sys - issues = json.load(sys.stdin) - byid = {} - for i in issues: - m = re.match(r"^T(\d\d) — ", i["title"]) - if m: - byid[int(m.group(1))] = i - for k in sorted(byid): - i = byid[k] - labels = {l["name"] for l in i["labels"]} - if i["state"] != "OPEN" or "on-hold" in labels or "external" in labels: - continue - prev = byid.get(k - 1) - if prev is None or prev["state"] == "CLOSED": - print(i["number"]); break - ') - if [ -z "$n" ]; then echo "No startable task issue."; exit 0; fi - gh issue edit "$n" --add-label claude:implement - gh workflow run claude-implement.yml -f issue_number="$n" - gh issue comment "$n" --body "All \`found-by-agent\` issues are triaged; the Claude implementer was started for this issue by the pipeline." - echo "Started #$n." diff --git a/AGENTS.md b/AGENTS.md index 3f72b93..95a05aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,76 +49,54 @@ Three documents define the work. Read them in this order before touching code: * The repository is `github.com/exanauts/SparseDirectSolver.jl` (remote `origin`, branch `main`). Commit at the end of the task with a message starting with the task id, e.g. `T05: symbolic pattern, ordering, etree, - column counts`. Do not commit `Manifest.toml`. In a GitHub Actions session - push and open the PR as described under "GitHub workflow"; in a local - session do not push unless asked. + column counts`. Do not commit `Manifest.toml`. Do not push or open the PR + (as described under "GitHub workflow") unless asked. ## GitHub workflow Every task is tracked on GitHub and lands through a pull request that CI and a -reviewing agent must pass. The moving parts: +reviewing agent check; the owner merges by hand. The moving parts: * **Milestones** are `PLAN.md` §5 (`M0 — …` to `M13 — …`). **Issues**: one per task, title `TNN — ` (`External — …` for the MadNLP task), label `task`, milestone set. The issue only points at `TASKS.md`; the task text there stays the single source of truth. Tasks are strictly sequential: the previous task's issue must be closed before the next one starts. -* **Implementer** (`.github/workflows/claude-implement.yml`, Claude Opus 5.5, - effort medium) starts when a task issue gets the label `claude:implement` - (the owner, or the pipeline after the previous merge). It works on - `ubuntu-latest` with Julia and the KA CPU backend only; CUDA is verified - by CI on the PR. Branch `task/TNN-` from `main`; commits - `TNN: …`; PR titled `TNN: …`, labelled `claude:pr`, milestone set, body from - `.github/pull_request_template.md` with the exact line `Closes #`. The - Report block is filled in before the PR is opened ("CUDA: pending CI on - the PR" under Tests). If the task cannot be finished, the PR is opened as - a draft and the issue labelled `needs-owner`; never a silent stop. +* **Task PRs**: branch `task/TNN-` from `main`; commits `TNN: …`; PR + titled `TNN: …`, labelled `claude:pr`, milestone set, body from + `.github/pull_request_template.md` with the exact line `Closes #`. + The Report block is filled in before the PR is opened. If the task cannot + be finished, the PR is opened as a draft with the state described in the + body and the Report; never a silent stop. * **Reviewer** (`.github/workflows/claude-review.yml`, Claude Fable 5.1, effort high) reviews every push on a `task/**` or `claude:pr` PR against the task text, this file and `PLAN.md`, posts inline comments and a verdict (a neutral review comment, so it never blocks a merge by itself) whose first line is `VERDICT: APPROVE (head )` or - `VERDICT: CHANGES_REQUESTED (head )`. On changes requested the - implementer (same workflow, job `fix`) addresses every finding on the same - branch, replies on threads it rejects, and pushes; at most 3 rounds, then the - PR is labelled `needs-owner`. -* **Pipeline** (`.github/workflows/claude-pipeline.yml`) reacts to completed CI - and review runs. A red `Run tests`/`Aqua` run on a task PR triggers a CI-fix - round by the implementer (at most 3, comments `CI-FIX round k`). Green CI on - the current head plus `VERDICT: APPROVE` for that head squash-merges the PR - (which closes the issue) and starts the next task issue unless it is labelled - `on-hold` or `external`. The repository variable `CLAUDE_AUTOPILOT=false` - pauses merging and chaining. Only the status checks are required by the - branch rules; approval is the verdict line, since a bot cannot approve its - own PR. + `VERDICT: CHANGES_REQUESTED (head )`. On changes requested nothing is + fixed automatically: the owner decides how the findings are addressed, and + a push to the branch starts a new review. +* **Merging** is done by the owner. No workflow implements a task, fixes + failing CI or review findings, merges a PR or starts the next task: the + owner squash-merges (which closes the issue) and starts the next task. * **Issues opened by agents**: anything found outside the task's scope (a bug in earlier code, a limit hit, a plan problem) becomes an issue with label `found-by-agent` using `.github/ISSUE_TEMPLATE/agent-finding.md`, cited in the Report. Do not fix it in the task's PR and do not edit `PLAN.md`; the - owner decides what becomes a task. **Such issues hold the chain**: after a - task PR merges, the pipeline starts the next task only when every open - `found-by-agent` issue carries the label `triaged` or is closed; otherwise it - comments on the next task issue and waits. Labelling the last untriaged issue - `triaged` (or closing it) resumes the chain (`claude-triage.yml`). -* The owner re-evaluates `PLAN.md` and `TASKS.md` between tasks by pausing the - chain (`CLAUDE_AUTOPILOT=false` or `on-hold` on the next issue), editing on - `main`, and relabelling. + owner decides what becomes a task. +* The owner re-evaluates `PLAN.md` and `TASKS.md` between tasks, editing on + `main`, before starting the next task. ## Environment -### GitHub Actions (where the agents run) +### GitHub Actions (where the reviewer runs) -* `ubuntu-latest`, Julia `1` from `julia-actions/setup-julia`, the project - instantiated, `gh` authenticated as the Claude GitHub App. **CPU backend - only**: no GPU, CUDA.jl is not installed; GPU results come from the - self-hosted `cuda` runner through `ci.yml` on the PR, and from the `amdgpu` - leg of `ci.yml` on the self-hosted AMD runner (the AMDGPU extension, T23; - no longer continue-on-error, a required check once the owner adds it to - the branch ruleset). -* Reference code is cloned next to the checkout, read-only: `../CUDSS.jl` and - `../KrylovPreconditioners.jl` (same relative paths as below). MadNLPGPU's - cuDSS integration is not checked out; when a task needs it, read it from the - `MadNLP/MadNLP.jl` repository on GitHub (`lib/MadNLPGPU`). +* `ubuntu-latest`, `gh` authenticated as the Claude GitHub App, no Julia and + no GPU. GPU results come from the self-hosted `cuda` runner through + `ci.yml` on the PR, and from the `amdgpu` leg of `ci.yml` on the + self-hosted AMD runner (the AMDGPU extension, T23; no longer + continue-on-error, a required check once the owner adds it to the branch + ruleset). ### The owner's machine @@ -261,7 +239,7 @@ name their types). ```text PLAN.md TASKS.md RESEARCH.md AGENTS.md Project.toml src/ ext/ test/ bench/ docs/ -.github/workflows/ ci.yml Aqua.yml (checks) Documentation.yml DocPreviewCleanup.yml (docs) claude-implement.yml claude-review.yml claude-pipeline.yml (agents) +.github/workflows/ ci.yml Aqua.yml (checks) Documentation.yml DocPreviewCleanup.yml (docs) claude-review.yml (agent) .github/scripts/ pr-verdict.sh (reads the reviewer's verdict) .github/ISSUE_TEMPLATE/agent-finding.md .github/pull_request_template.md ```