From fabf494c30118326dbce457430e6443b33aac4b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:35:15 +0000 Subject: [PATCH 1/3] Initial plan From e9637177e139fd9680765921bf670c9d9870ba86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:43:42 +0000 Subject: [PATCH 2/3] Fix autoloop creating multiple PRs: enforce canonical branch name and single PR per program Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/f3ec8d59-d3d1-40e8-8589-efb4778445e2 Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- .github/workflows/autoloop.lock.yml | 10 ++-- .github/workflows/autoloop.md | 71 +++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/.github/workflows/autoloop.lock.yml b/.github/workflows/autoloop.lock.yml index fa5a28a9..9b062e7b 100644 --- a/.github/workflows/autoloop.lock.yml +++ b/.github/workflows/autoloop.lock.yml @@ -232,7 +232,7 @@ jobs: cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" cat << 'GH_AW_PROMPT_f2e31707300f9386_EOF' - Tools: add_comment(max:7), create_issue(max:2), update_issue(max:3), create_pull_request(max:2), add_labels(max:2), remove_labels(max:2), push_to_pull_request_branch(max:2), missing_tool, missing_data, noop + Tools: add_comment(max:7), create_issue(max:2), update_issue(max:3), create_pull_request(max:1), add_labels(max:2), remove_labels(max:2), push_to_pull_request_branch(max:1), missing_tool, missing_data, noop GH_AW_PROMPT_f2e31707300f9386_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" @@ -499,7 +499,7 @@ jobs: mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_96ab95b5533c909e_EOF' - {"add_comment":{"hide_older_comments":false,"max":7,"target":"*"},"add_labels":{"max":2,"target":"*"},"create_issue":{"labels":["automation","autoloop"],"max":2,"title_prefix":"[Autoloop] "},"create_pull_request":{"draft":true,"labels":["automation","autoloop"],"max":2,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Autoloop] "},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":2,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Autoloop] "},"remove_labels":{"max":2,"target":"*"},"update_issue":{"allow_body":true,"max":3,"target":"*","title_prefix":"[Autoloop] "}} + {"add_comment":{"hide_older_comments":false,"max":7,"target":"*"},"add_labels":{"max":2,"target":"*"},"create_issue":{"labels":["automation","autoloop"],"max":2,"title_prefix":"[Autoloop] "},"create_pull_request":{"draft":true,"labels":["automation","autoloop"],"max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Autoloop] "},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Autoloop] "},"remove_labels":{"max":2,"target":"*"},"update_issue":{"allow_body":true,"max":3,"target":"*","title_prefix":"[Autoloop] "}} GH_AW_SAFE_OUTPUTS_CONFIG_96ab95b5533c909e_EOF - name: Write Safe Outputs Tools run: | @@ -509,8 +509,8 @@ jobs: "add_comment": " CONSTRAINTS: Maximum 7 comment(s) can be added. Target: *.", "add_labels": " CONSTRAINTS: Maximum 2 label(s) can be added. Target: *.", "create_issue": " CONSTRAINTS: Maximum 2 issue(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added.", - "create_pull_request": " CONSTRAINTS: Maximum 2 pull request(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added. PRs will be created as drafts.", - "push_to_pull_request_branch": " CONSTRAINTS: Maximum 2 push(es) can be made. The target pull request title must start with \"[Autoloop] \".", + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added. PRs will be created as drafts.", + "push_to_pull_request_branch": " CONSTRAINTS: Maximum 1 push(es) can be made. The target pull request title must start with \"[Autoloop] \".", "remove_labels": " CONSTRAINTS: Maximum 2 label(s) can be removed. Target: *.", "update_issue": " CONSTRAINTS: Maximum 3 issue(s) can be updated. Target: *. The target issue title must start with \"[Autoloop] \"." }, @@ -1554,7 +1554,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.jsr.io,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":false,\"max\":7,\"target\":\"*\"},\"add_labels\":{\"max\":2,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"autoloop\"],\"max\":2,\"title_prefix\":\"[Autoloop] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"autoloop\"],\"max\":2,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Autoloop] \"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":2,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"},\"remove_labels\":{\"max\":2,\"target\":\"*\"},\"update_issue\":{\"allow_body\":true,\"max\":3,\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":false,\"max\":7,\"target\":\"*\"},\"add_labels\":{\"max\":2,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"autoloop\"],\"max\":2,\"title_prefix\":\"[Autoloop] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"autoloop\"],\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Autoloop] \"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"},\"remove_labels\":{\"max\":2,\"target\":\"*\"},\"update_issue\":{\"allow_body\":true,\"max\":3,\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/autoloop.md b/.github/workflows/autoloop.md index f086bfd4..ff18019b 100644 --- a/.github/workflows/autoloop.md +++ b/.github/workflows/autoloop.md @@ -45,11 +45,11 @@ safe-outputs: title-prefix: "[Autoloop] " labels: [automation, autoloop] protected-files: fallback-to-issue - max: 2 + max: 1 push-to-pull-request-branch: target: "*" title-prefix: "[Autoloop] " - max: 2 + max: 1 create-issue: title-prefix: "[Autoloop] " labels: [automation, autoloop] @@ -646,13 +646,24 @@ Examples: - `autoloop/signal_processing` - `autoloop/coverage` +> ⚠️ **CRITICAL — Branch Name Must Be Exact** +> +> The branch name is ALWAYS exactly `autoloop/{program-name}` — **no suffixes, no hashes, no run IDs, no iteration numbers, no random tokens**. Never create branches like: +> - ❌ `autoloop/coverage-abc123` +> - ❌ `autoloop/coverage-iter42-deadbeef` +> - ❌ `autoloop/coverage-1234567890` +> +> Always use the exact canonical name, for example `autoloop/coverage`. If you are implementing a program named `build-tsb-pandas-typescript-migration`, the branch is always `autoloop/build-tsb-pandas-typescript-migration` — nothing more. +> +> **Never let the gh-aw framework auto-generate a branch name.** You must explicitly name the branch when creating it. + ### How It Works -1. On the **first accepted iteration**, the branch is created from the default branch. +1. On the **first accepted iteration**, the branch is created from the default branch using `git checkout -b autoloop/{program-name}`. 2. On **subsequent iterations**, the agent checks out the existing branch and ensures it is up to date with the default branch (by merging the default branch into it). 3. **Accepted iterations** are committed and pushed to the branch. Each commit message references the GitHub Actions run URL. 4. **Rejected or errored iterations** do not commit — changes are discarded. -5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR. +5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR — **never create a new PR if one already exists**. 6. The branch may be **merged into the default branch** at any time (by a maintainer or CI). After merging, the branch continues to be used for future iterations — it is never deleted while the program is active. 7. A **sync workflow** automatically merges the default branch into all active `autoloop/*` branches whenever the default branch changes, keeping them up to date. @@ -682,6 +693,8 @@ Each run executes **one iteration for the single selected program**: If the state file does not yet exist, create it in the repo-memory folder using the template defined in the [Repo Memory](#repo-memory) section. +3. Note the `PR` field from the Machine State table. If it contains a PR number (e.g., `#42`), that is the **existing draft PR** for this program — you must update it, not create a new one. + ### Step 2: Analyze and Propose 1. Read the target files and understand the current state. @@ -696,7 +709,22 @@ Each run executes **one iteration for the single selected program**: ### Step 3: Implement -1. Check out the program's long-running branch `autoloop/{program-name}`. If the branch does not yet exist, create it from the default branch. If it does exist, ensure it is up to date with the default branch (merge the default branch into it). +1. Check out the program's long-running branch. The branch name is **exactly** `autoloop/{program-name}` — never with a suffix. For example: + - Program `coverage` → branch `autoloop/coverage` + - Program `build-tsb-pandas-typescript-migration` → branch `autoloop/build-tsb-pandas-typescript-migration` + + ```bash + git fetch origin + if git ls-remote --exit-code origin autoloop/{program-name}; then + # Branch exists — check it out and merge the default branch + git checkout -b autoloop/{program-name} origin/autoloop/{program-name} + git merge origin/main --no-edit -m "Merge main into autoloop/{program-name}" + else + # Branch does not exist — create it from the default branch + git checkout -b autoloop/{program-name} origin/main + fi + ``` + 2. Make the proposed changes to the target files only. 3. **Respect the program constraints**: do not modify files outside the target list. @@ -712,11 +740,17 @@ Each run executes **one iteration for the single selected program**: 1. Commit the changes to the long-running branch `autoloop/{program-name}` with a commit message referencing the actions run: - Commit message subject line: `[Autoloop: {program-name}] Iteration : ` - Commit message body (after a blank line): `Run: {run_url}` referencing the GitHub Actions run URL. -2. Push the commit to the long-running branch. -3. If a draft PR does not already exist for this branch, create one: - - Title: `[Autoloop: {program-name}]` - - Body includes: a summary of the program goal, link to the steering issue, the current best metric, and AI disclosure: `🤖 *This PR is maintained by Autoloop. Each accepted iteration adds a commit to this branch.*` - If a draft PR already exists, update the PR body with the latest metric and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what changed, old metric, new metric, improvement delta, and a link to the actions run. +2. Push the commit to the long-running branch `autoloop/{program-name}`. +3. **Find the existing PR or create one** — follow these steps in order: + a. Check the `PR` field in the state file's **⚙️ Machine State** table. If it contains a PR number (e.g., `#42`), that is the existing draft PR. + b. If the state file has no PR number, search GitHub for open PRs with head branch `autoloop/{program-name}`. Use the GitHub API: `GET /repos/{owner}/{repo}/pulls?state=open&head={owner}:autoloop/{program-name}`. + c. **If an existing PR is found** (from either step a or b): use `push-to-pull-request-branch` to push additional commits to the existing PR. Update the PR body with the latest metric and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what changed, old metric, new metric, improvement delta, and a link to the actions run. **Do NOT call `create-pull-request`.** + d. **If NO PR exists** for `autoloop/{program-name}`: create one using `create-pull-request`: + - Branch: `autoloop/{program-name}` (the branch you already created in Step 3 — do NOT let the framework auto-generate a branch name) + - Title: `[Autoloop: {program-name}]` + - Body includes: a summary of the program goal, link to the steering issue, the current best metric, and AI disclosure: `🤖 *This PR is maintained by Autoloop. Each accepted iteration adds a commit to this branch.*` + + > ⚠️ **Never create a new PR if one already exists for `autoloop/{program-name}`.** Each program must have exactly one draft PR at any time. If you are unsure whether a PR exists, check the GitHub API before calling `create-pull-request`. 4. Ensure the steering issue exists (see [Steering Issue](#steering-issue) below). Add a comment to the steering issue linking to the commit and actions run. 5. Add an entry to the experiment log issue. 6. Update the state file `{program-name}.md` in the repo-memory folder: @@ -1106,3 +1140,20 @@ After each iteration, prepend an entry to the **📊 Iteration History** section - **Safety.** Never modify files outside the target list. Never modify the evaluation script. Never modify the program definition (except via `/autoloop` command mode). - **Read AGENTS.md first**: before starting work, read the repository's `AGENTS.md` file (if present) to understand project-specific conventions. - **Build and test**: run any build/test commands before creating PRs. + +## Common Mistakes to Avoid + +> ❌ **Do NOT create a new branch with a suffix for each iteration.** +> Correct: `autoloop/coverage` +> Wrong: `autoloop/coverage-abc123`, `autoloop/coverage-iter42`, `autoloop/coverage-deadbeef1234` + +> ❌ **Do NOT create a new PR if one already exists for `autoloop/{program-name}`.** +> Always check the state file's `PR` field and the GitHub API before calling `create-pull-request`. If a PR exists, use `push-to-pull-request-branch` instead. + +> ❌ **Do NOT let the gh-aw framework auto-generate a branch name when creating a PR.** +> Always specify the branch explicitly as `autoloop/{program-name}` when calling `create-pull-request`. + +> ❌ **Do NOT push to the default branch (e.g., `main`).** +> All accepted changes go to `autoloop/{program-name}` only. + +> ✅ **One program = one long-running branch = one draft PR.** This is the invariant that must hold at all times. From 4103970abaf7f44fed3ed3f96e6717cc3016b46d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:14:02 +0000 Subject: [PATCH 3/3] Fix TypeScript errors in stats tests: avoid passing undefined for exactOptionalPropertyTypes Agent-Logs-Url: https://github.com/githubnext/tsessebe/sessions/4c38da0f-a871-4aa4-bf5d-7133398e72bd Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- tests/stats/nlargest.test.ts | 440 +++++++++++++++++++++++++++++++++++ tests/stats/rank.test.ts | 413 ++++++++++++++++++++++++++++++++ 2 files changed, 853 insertions(+) create mode 100644 tests/stats/nlargest.test.ts create mode 100644 tests/stats/rank.test.ts diff --git a/tests/stats/nlargest.test.ts b/tests/stats/nlargest.test.ts new file mode 100644 index 00000000..046fe0ab --- /dev/null +++ b/tests/stats/nlargest.test.ts @@ -0,0 +1,440 @@ +/** + * Tests for src/stats/nlargest.ts + * — nlargestSeries, nsmallestSeries, nlargestDataFrame, nsmallestDataFrame + */ +import { describe, expect, it } from "bun:test"; +import fc from "fast-check"; +import { + DataFrame, + Index, + Series, + nlargestDataFrame, + nlargestSeries, + nsmallestDataFrame, + nsmallestSeries, +} from "../../src/index.ts"; +import type { Label, Scalar } from "../../src/index.ts"; + +// ─── helpers ───────────────────────────────────────────────────────────────── + +function s(data: readonly Scalar[], name?: string): Series { + return new Series({ data: [...data], ...(name !== undefined ? { name } : {}) }); +} + +function vals(series: Series): Scalar[] { + return [...series.values]; +} + +function idxVals(series: Series): Scalar[] { + return [...series.index.values]; +} + +// ─── nlargestSeries ────────────────────────────────────────────────────────── + +describe("nlargestSeries", () => { + it("returns top n values in descending order", () => { + const result = nlargestSeries(s([3, 1, 4, 1, 5, 9, 2, 6]), 3); + expect(vals(result)).toEqual([9, 6, 5]); + }); + + it("preserves original index labels", () => { + const series = new Series({ data: [3, 1, 4, 1, 5], index: ["a", "b", "c", "d", "e"] }); + const result = nlargestSeries(series, 3); + expect(vals(result)).toEqual([5, 4, 3]); + expect(idxVals(result)).toEqual(["e", "c", "a"]); + }); + + it("returns all elements when n >= length", () => { + const result = nlargestSeries(s([3, 1, 2]), 10); + expect(vals(result)).toEqual([3, 2, 1]); + }); + + it("returns empty series when n = 0", () => { + const result = nlargestSeries(s([1, 2, 3]), 0); + expect(vals(result)).toEqual([]); + }); + + it("excludes NaN values", () => { + const result = nlargestSeries(s([3, NaN, 5, NaN, 1]), 3); + expect(vals(result)).toEqual([5, 3, 1]); + }); + + it("excludes null values", () => { + const result = nlargestSeries(s([3, null, 5, null, 1]), 3); + expect(vals(result)).toEqual([5, 3, 1]); + }); + + it("keep='first' selects first occurrence on tie at boundary", () => { + const series = new Series({ data: [5, 5, 5], index: [0, 1, 2] }); + const result = nlargestSeries(series, 2, { keep: "first" }); + expect(vals(result)).toEqual([5, 5]); + expect(idxVals(result)).toEqual([0, 1]); + }); + + it("keep='last' selects last occurrence on tie at boundary", () => { + const series = new Series({ data: [5, 5, 5], index: [0, 1, 2] }); + const result = nlargestSeries(series, 2, { keep: "last" }); + expect(vals(result)).toEqual([5, 5]); + expect(idxVals(result)).toEqual([1, 2]); + }); + + it("keep='all' returns all ties at boundary", () => { + const series = new Series({ data: [5, 5, 5], index: [0, 1, 2] }); + const result = nlargestSeries(series, 2, { keep: "all" }); + expect(vals(result)).toEqual([5, 5, 5]); + }); + + it("keep='all' includes ties at boundary when n is exact", () => { + const series = new Series({ data: [1, 3, 3, 5], index: [0, 1, 2, 3] }); + const result = nlargestSeries(series, 2, { keep: "all" }); + // top 2 would be 5 and one of the 3s — but all 3s tie at boundary → 5, 3, 3 + expect(vals(result)).toEqual([5, 3, 3]); + }); + + it("keep='first' breaks tie at boundary to keep first occurrence", () => { + const series = new Series({ data: [1, 3, 3, 5], index: [0, 1, 2, 3] }); + const result = nlargestSeries(series, 2, { keep: "first" }); + expect(vals(result)).toEqual([5, 3]); + expect(idxVals(result)).toEqual([3, 1]); + }); + + it("keep='last' breaks tie at boundary to keep last occurrence", () => { + const series = new Series({ data: [1, 3, 3, 5], index: [0, 1, 2, 3] }); + const result = nlargestSeries(series, 2, { keep: "last" }); + expect(vals(result)).toEqual([5, 3]); + expect(idxVals(result)).toEqual([3, 2]); + }); + + it("works with string values", () => { + const result = nlargestSeries(s(["banana", "apple", "cherry", "date"]), 2); + expect(vals(result)).toEqual(["date", "cherry"]); + }); + + it("works with single element", () => { + const result = nlargestSeries(s([42]), 1); + expect(vals(result)).toEqual([42]); + }); + + it("empty series returns empty", () => { + const result = nlargestSeries(s([]), 3); + expect(vals(result)).toEqual([]); + }); + + it("preserves series name", () => { + const result = nlargestSeries(s([1, 2, 3], "my_col"), 2); + expect(result.name).toBe("my_col"); + }); + + it("all NaN returns empty", () => { + const result = nlargestSeries(s([NaN, NaN]), 2); + expect(vals(result)).toEqual([]); + }); +}); + +// ─── nsmallestSeries ───────────────────────────────────────────────────────── + +describe("nsmallestSeries", () => { + it("returns top n values in ascending order", () => { + const result = nsmallestSeries(s([3, 1, 4, 1, 5, 9, 2, 6]), 3); + expect(vals(result)).toEqual([1, 1, 2]); + }); + + it("preserves original index labels", () => { + const series = new Series({ data: [3, 1, 4, 1, 5], index: ["a", "b", "c", "d", "e"] }); + const result = nsmallestSeries(series, 3); + expect(vals(result)).toEqual([1, 1, 3]); + expect(idxVals(result)).toEqual(["b", "d", "a"]); + }); + + it("returns all when n >= length", () => { + const result = nsmallestSeries(s([3, 1, 2]), 10); + expect(vals(result)).toEqual([1, 2, 3]); + }); + + it("returns empty when n = 0", () => { + const result = nsmallestSeries(s([1, 2, 3]), 0); + expect(vals(result)).toEqual([]); + }); + + it("excludes NaN values", () => { + const result = nsmallestSeries(s([3, NaN, 5, NaN, 1]), 2); + expect(vals(result)).toEqual([1, 3]); + }); + + it("keep='first' selects first on tie at boundary", () => { + const series = new Series({ data: [1, 1, 1, 5], index: [0, 1, 2, 3] }); + const result = nsmallestSeries(series, 2, { keep: "first" }); + expect(vals(result)).toEqual([1, 1]); + expect(idxVals(result)).toEqual([0, 1]); + }); + + it("keep='last' selects last on tie at boundary", () => { + const series = new Series({ data: [1, 1, 1, 5], index: [0, 1, 2, 3] }); + const result = nsmallestSeries(series, 2, { keep: "last" }); + expect(vals(result)).toEqual([1, 1]); + expect(idxVals(result)).toEqual([1, 2]); + }); + + it("keep='all' returns all ties at boundary", () => { + const series = new Series({ data: [1, 3, 1, 5], index: [0, 1, 2, 3] }); + const result = nsmallestSeries(series, 2, { keep: "all" }); + // n=2: smallest two are 1 (pos 0) and 1 (pos 2) — tie at boundary (3) → already 2 items + expect(vals(result)).toEqual([1, 1]); + }); + + it("keep='all' expands on tie at boundary", () => { + const series = new Series({ data: [5, 3, 3, 1], index: [0, 1, 2, 3] }); + const result = nsmallestSeries(series, 2, { keep: "all" }); + // smallest 2: pos3(1), pos1(3) — but 3 ties with pos2(3) → include all → [1, 3, 3] + expect(vals(result)).toEqual([1, 3, 3]); + }); + + it("works with string values", () => { + const result = nsmallestSeries(s(["banana", "apple", "cherry", "date"]), 2); + expect(vals(result)).toEqual(["apple", "banana"]); + }); + + it("empty series returns empty", () => { + const result = nsmallestSeries(s([]), 3); + expect(vals(result)).toEqual([]); + }); +}); + +// ─── nlargestDataFrame ──────────────────────────────────────────────────────── + +describe("nlargestDataFrame", () => { + it("returns n rows with largest values in a single column", () => { + const df = DataFrame.fromColumns({ a: [3, 1, 4, 1, 5], b: [10, 20, 30, 40, 50] }); + const result = nlargestDataFrame(df, 2, { columns: "a" }); + expect([...result.col("a").values]).toEqual([5, 4]); + expect([...result.col("b").values]).toEqual([50, 30]); + }); + + it("preserves original index labels", () => { + const idx = new Index