diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index fcdee700..00000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "fantomas": { - "version": "7.0.5", - "commands": [ - "fantomas" - ] - }, - "fsdocs-tool": { - "version": "21.0.0", - "commands": [ - "fsdocs" - ], - "rollForward": false - } - } -} \ No newline at end of file diff --git a/.fantomasignore b/.fantomasignore deleted file mode 100644 index 12b05f3c..00000000 --- a/.fantomasignore +++ /dev/null @@ -1,5 +0,0 @@ -# Literate documentation scripts are not subject to Fantomas formatting -docs/ - -# Generated output from fsdocs -output/ diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index c1965c21..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json deleted file mode 100644 index c9e1cbac..00000000 --- a/.github/aw/actions-lock.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "entries": { - "actions/github-script@v9.0.0": { - "repo": "actions/github-script", - "version": "v9.0.0", - "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3" - }, - "github/gh-aw-actions/setup-cli@v0.79.6": { - "repo": "github/gh-aw-actions/setup-cli", - "version": "v0.79.6", - "sha": "5c2fe865bb4dc46e1450f6ee0d0541d759aea73a" - }, - "github/gh-aw-actions/setup@v0.79.6": { - "repo": "github/gh-aw-actions/setup", - "version": "v0.79.6", - "sha": "5c2fe865bb4dc46e1450f6ee0d0541d759aea73a" - }, - "github/gh-aw/actions/setup@v0.79.6": { - "repo": "github/gh-aw/actions/setup", - "version": "v0.79.6", - "sha": "9c481b8bc46dce8b92fce8ffc51781e5c330d37c" - } - }, - "containers": { - "ghcr.io/github/gh-aw-firewall/agent:0.25.20": { - "image": "ghcr.io/github/gh-aw-firewall/agent:0.25.20", - "digest": "sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682", - "pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682" - }, - "ghcr.io/github/gh-aw-firewall/agent:0.25.58": { - "image": "ghcr.io/github/gh-aw-firewall/agent:0.25.58", - "digest": "sha256:a316a2c021accba8a9ea194c75466b0c4a166be6ac783bf8c2d0afd73373dec2", - "pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.58@sha256:a316a2c021accba8a9ea194c75466b0c4a166be6ac783bf8c2d0afd73373dec2" - }, - "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20": { - "image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20", - "digest": "sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519", - "pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519" - }, - "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58": { - "image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58", - "digest": "sha256:43a5cdbe4e1156920dcdaab26d6c6761777d5c6bdc572d7d07b11739fb34c749", - "pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58@sha256:43a5cdbe4e1156920dcdaab26d6c6761777d5c6bdc572d7d07b11739fb34c749" - }, - "ghcr.io/github/gh-aw-firewall/squid:0.25.20": { - "image": "ghcr.io/github/gh-aw-firewall/squid:0.25.20", - "digest": "sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236", - "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236" - }, - "ghcr.io/github/gh-aw-firewall/squid:0.25.58": { - "image": "ghcr.io/github/gh-aw-firewall/squid:0.25.58", - "digest": "sha256:558682b7b6313a5443cbb3d702899823bd732f991c8b52db6b5b8066abefe7a1", - "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.58@sha256:558682b7b6313a5443cbb3d702899823bd732f991c8b52db6b5b8066abefe7a1" - }, - "ghcr.io/github/gh-aw-mcpg:v0.2.19": { - "image": "ghcr.io/github/gh-aw-mcpg:v0.2.19", - "digest": "sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd", - "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd" - }, - "ghcr.io/github/gh-aw-mcpg:v0.3.22": { - "image": "ghcr.io/github/gh-aw-mcpg:v0.3.22", - "digest": "sha256:ce5c6f5461b077af0d8e8eb1763436e85153f8e9531117d58a7bdb23de71f00a", - "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.3.22@sha256:ce5c6f5461b077af0d8e8eb1763436e85153f8e9531117d58a7bdb23de71f00a" - }, - "ghcr.io/github/github-mcp-server:v0.32.0": { - "image": "ghcr.io/github/github-mcp-server:v0.32.0", - "digest": "sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28", - "pinned_image": "ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28" - }, - "ghcr.io/github/github-mcp-server:v1.1.0": { - "image": "ghcr.io/github/github-mcp-server:v1.1.0", - "digest": "sha256:71b07d9abecb83b4a2595bcd8ccb35f9a0166361a12335f9e16da1ef07172029", - "pinned_image": "ghcr.io/github/github-mcp-server:v1.1.0@sha256:71b07d9abecb83b4a2595bcd8ccb35f9a0166361a12335f9e16da1ef07172029" - }, - "node:lts-alpine": { - "image": "node:lts-alpine", - "digest": "sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f", - "pinned_image": "node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f" - } - } -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 71dce5b0..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,21 +0,0 @@ -updates: -- directory: / - ignore: - - dependency-name: "*" - update-types: - - version-update:semver-patch - - dependency-name: "github/gh-aw-actions" # Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump. - open-pull-requests-limit: 10 - package-ecosystem: github-actions - schedule: - interval: daily -- directory: / - ignore: - - dependency-name: "*" - update-types: - - version-update:semver-patch - open-pull-requests-limit: 10 - package-ecosystem: nuget - schedule: - interval: daily -version: 2 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index ae28c8d6..00000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: ci-build - -on: [pull_request] - -jobs: - verify_formatting: - runs-on: ubuntu-latest - name: Verify code formatting - - steps: - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - - - name: tool restore - run: dotnet tool restore - - - name: validate formatting - run: dotnet fantomas . --check - - build: - name: Build - runs-on: windows-latest - steps: - # checkout the code - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - # setup dotnet based on global.json - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - - # cache NuGet packages to avoid re-downloading on every run - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - - # build it, test it, pack it - - name: Run dotnet build (release) - # see issue #105 - # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble - shell: cmd - run: ./build.cmd diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml deleted file mode 100644 index e547a024..00000000 --- a/.github/workflows/docs.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Build and Deploy Docs - -on: - push: - branches: - - main - -permissions: - contents: write # needed for peaceiris/actions-gh-pages - -jobs: - docs: - name: Build and deploy docs - runs-on: ubuntu-latest - steps: - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - - - name: Install tools - run: dotnet tool restore - - - name: Build library (Release) - run: dotnet build src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj -c Release - - - name: Build docs - run: dotnet fsdocs build --properties Configuration=Release --eval - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./output - publish_branch: gh-pages - force_orphan: true diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml deleted file mode 100644 index 002be210..00000000 --- a/.github/workflows/main.yaml +++ /dev/null @@ -1,99 +0,0 @@ -name: Build main (release) - -on: - push: - branches: - - main - -jobs: - build: - name: Build - runs-on: windows-latest - steps: - # checkout the code - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - # setup dotnet based on global.json - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - # cache NuGet packages to avoid re-downloading on every run - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - # build it, test it, pack it - - name: Run dotnet build (release) - # see issue #105 - # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble - shell: cmd - run: ./build.cmd - - test-release: - name: Test Release Build - runs-on: windows-latest - steps: - # checkout the code - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - # setup dotnet based on global.json - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - # cache NuGet packages to avoid re-downloading on every run - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - # build it, test it, pack it - - name: Run dotnet test - release - # see issue #105 - # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble - shell: cmd - run: ./build.cmd ci -release - - name: Publish test results - release - uses: dorny/test-reporter@v2 - if: always() - with: - name: Report release tests - # this path glob pattern requires forward slashes! - path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release.trx - reporter: dotnet-trx - - test-release-linux: - name: Test Release Build (Linux) - runs-on: ubuntu-latest - steps: - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - - # build.cmd is Windows-only; run dotnet test directly on Linux - - name: Run dotnet test - release (Linux) - run: dotnet test src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj -c Release --blame-hang-timeout 60000ms --logger "console;verbosity=detailed" --logger "trx;LogFileName=test-results-release-linux.trx" - - - name: Publish test results - release (Linux) - uses: dorny/test-reporter@v2 - if: always() - with: - name: Report release tests (Linux) - # this path glob pattern requires forward slashes! - path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release-linux.trx - reporter: dotnet-trx diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml deleted file mode 100644 index 19de9943..00000000 --- a/.github/workflows/publish.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Pack & Publish Nuget - -on: - push: - branches: - - main - -permissions: - #contents: write # for peaceiris/actions-gh-pages - id-token: write # for NuGet trusted publishing - -jobs: - publish: - name: Publish nuget (if new version) - runs-on: windows-latest - steps: - # checkout the code - - name: checkout-code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - # setup dotnet based on global.json - - name: setup-dotnet - uses: actions/setup-dotnet@v4 - # cache NuGet packages to avoid re-downloading on every run - - name: Cache NuGet packages - uses: actions/cache@v5 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} - restore-keys: nuget-${{ runner.os }}- - # build it, test it, pack it, publish it - - name: Run dotnet build (release, for nuget) - # see issue #105 and #243 - # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble - shell: cmd - run: ./build.cmd - - name: Obtain NuGet key - # this hash is v1.1.0 - uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 - id: login - with: - user: dsyme - - name: Publish NuGets (if this version not published before) - run: dotnet nuget push packages\FSharp.Control.TaskSeq.*.nupkg -s https://www.nuget.org/api/v2/package -k ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml deleted file mode 100644 index 209cddcc..00000000 --- a/.github/workflows/repo-assist.lock.yml +++ /dev/null @@ -1,2229 +0,0 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6a76201047085d4d979bac5f5ef946bf3d09185afdb49ad98b9d6b089d68a99d","body_hash":"422779c1cf00c63c80de694bfd2f865689ebccc15752ec4eb9bbd8a64df5318c","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"5c2fe865bb4dc46e1450f6ee0d0541d759aea73a","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]} -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.79.6). DO NOT EDIT. -# -# To update this file, edit githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75 and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# A friendly repository assistant that runs regularly (twice a day by default) to assist maintainers. -# Can also be triggered on-demand via '/repo-assist ' to perform specific tasks. -# - Labels and triages open issues -# - Comments helpfully on open issues to unblock contributors and onboard newcomers -# - Identifies issues that can be fixed and creates draft pull requests with fixes -# - Improves performance, testing, and code quality via PRs -# - Makes engineering investments: dependency updates, CI improvements, tooling -# - Updates its own PRs when CI fails or merge conflicts arise -# - Nudges stale PRs waiting for author response -# - Takes the repository forward with proactive improvements -# - Maintains a persistent memory of work done and what remains -# Always polite, constructive, and mindful of the project's goals. -# -# Source: githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75 -# -# Secrets used: -# - COPILOT_GITHUB_TOKEN -# - GH_AW_CI_TRIGGER_TOKEN -# - GH_AW_GITHUB_MCP_SERVER_TOKEN -# - GH_AW_GITHUB_TOKEN -# - GITHUB_TOKEN -# -# Custom actions used: -# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 -# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 -# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 -# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 -# -# Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 -# - ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 -# - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa -# - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - -name: "Repo Assist" -on: - discussion: - types: - - created - - edited - discussion_comment: - types: - - created - - edited - issue_comment: - types: - - created - - edited - issues: - types: - - opened - - edited - - reopened - # permissions: # Permissions applied to pre-activation job - # pull-requests: read - pull_request: - types: - - opened - - edited - - reopened - pull_request_review_comment: - types: - - created - - edited - schedule: - - cron: "39 13 * * 5" - # steps: # Steps injected into pre-activation job - # - id: check - # run: | - # MAX_OPEN_PRS=8 - # if [[ "$GITHUB_EVENT_NAME" != "schedule" ]]; then exit 0; fi - # COUNT=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --search 'in:title "[repo-assist]"' --json number --jq 'length') - # [[ "$COUNT" -lt "$MAX_OPEN_PRS" ]] - workflow_dispatch: - inputs: - aw_context: - default: "" - description: "Agent caller context (used internally by Agentic Workflows)." - required: false - type: string - command: - default: "" - description: "Optional command-mode instruction (for example: Run Task 9)" - required: false - type: string - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}" - -run-name: "Repo Assist" - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' && (needs.pre_activation.outputs.check_result == 'success') - runs-on: ubuntu-slim - permissions: - actions: read - contents: read - discussions: write - issues: write - pull-requests: write - env: - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} - outputs: - body: ${{ steps.sanitized.outputs.body }} - comment_id: ${{ steps.add-comment.outputs.comment-id }} - comment_repo: ${{ steps.add-comment.outputs.comment-repo }} - comment_url: ${{ steps.add-comment.outputs.comment-url }} - daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} - daily_effective_workflow_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_threshold || '' }} - daily_effective_workflow_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_total_effective_tokens || '' }} - engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} - lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} - setup-span-id: ${{ steps.setup.outputs.span-id }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - slash_command: ${{ needs.pre_activation.outputs.matched_command }} - stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} - text: ${{ steps.sanitized.outputs.text }} - title: ${{ steps.sanitized.outputs.title }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} - safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Generate agentic run info - id: generate_aw_info - env: - GH_AW_INFO_ENGINE_ID: "copilot" - GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AGENT_VERSION: "1.0.60" - GH_AW_INFO_CLI_VERSION: "v0.79.6" - GH_AW_INFO_WORKFLOW_NAME: "Repo Assist" - GH_AW_INFO_EXPERIMENTAL: "false" - GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" - GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","node","python","rust","java"]' - GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_AWMG_VERSION: "" - GH_AW_INFO_FIREWALL_TYPE: "squid" - GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); - await main(core, context); - - name: Check daily workflow token guardrail - id: daily-effective-workflow-guardrail - if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_ID: "repo-assist" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} - GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); - await main(); - - name: Add eyes reaction for immediate feedback - id: react - if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_REACTION: "eyes" - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs'); - await main(); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Checkout .github and .agents folders - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: | - .github - .agents - .antigravity - .claude - .codex - .crush - .gemini - .opencode - .pi - sparse-checkout-cone-mode: true - fetch-depth: 1 - - name: Save agent config folders for base branch restoration - env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - - name: Check workflow lock file - id: check-lock-file - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_WORKFLOW_FILE: "repo-assist.lock.yml" - GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - name: Check compile-agentic version - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_COMPILED_VERSION: "v0.79.6" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); - await main(); - - name: Compute current body text - id: sanitized - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.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" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); - await main(); - - name: Add comment with workflow run link - id: add-comment - if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/add_workflow_run_comment.cjs'); - await main(); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl - GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_1DD9F1B7: ${{ steps.sanitized.outputs.text || inputs.command }} - GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_INPUTS_COMMAND: ${{ inputs.command }} - GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} - GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} - GH_AW_WIKI_NOTE: ${{ '' }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_d056b923db64787d_EOF' - - GH_AW_PROMPT_d056b923db64787d_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_d056b923db64787d_EOF' - - Tools: add_comment(max:10), create_issue(max:4), update_issue, create_pull_request(max:4), add_labels(max:30), remove_labels(max:5), push_to_pull_request_branch(max:4), missing_tool, missing_data, noop - GH_AW_PROMPT_d056b923db64787d_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" - cat << 'GH_AW_PROMPT_d056b923db64787d_EOF' - - GH_AW_PROMPT_d056b923db64787d_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_d056b923db64787d_EOF' - - The following GitHub context information is available for this workflow: - {{#if github.actor}} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if github.repository}} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if github.workspace}} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - - **issue-number**: #__GH_AW_EXPR_802A9F6A__ - {{/if}} - {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ - {{/if}} - {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - - **pull-request-number**: #__GH_AW_EXPR_463A214A__ - {{/if}} - {{#if github.event.comment.id || github.aw.context.comment_id}} - - **comment-id**: __GH_AW_EXPR_FF1D34CE__ - {{/if}} - {{#if github.run_id}} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - **checkouts**: The following repositories have been checked out and are available in the workspace: - - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] - - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - - **Warning: No git credentials are available to the agent.** Credentials are - intentionally removed after the checkout step for security. This means any git - operation that needs to authenticate to the remote will fail. In private repositories, that includes: - - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools) - - Checking out or switching to a remote branch that is not already fetched - - Deepening a shallow clone (`git fetch --unshallow`) - - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout) - Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` — - authentication will not succeed. If you encounter credential prompts or authentication errors, - stop immediately and report the limitation rather than spending turns trying to work around it. - - - GH_AW_PROMPT_d056b923db64787d_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then - cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" - fi - if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then - cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" - fi - cat << 'GH_AW_PROMPT_d056b923db64787d_EOF' - - {{#runtime-import .github/workflows/repo-assist.md}} - GH_AW_PROMPT_d056b923db64787d_EOF - } > "$GH_AW_PROMPT" - - name: Interpolate variables and render templates - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_ENGINE_ID: "copilot" - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} - GH_AW_INPUTS_COMMAND: ${{ inputs.command }} - GH_AW_EXPR_1DD9F1B7: ${{ steps.sanitized.outputs.text || inputs.command }} - GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Substitute placeholders - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_1DD9F1B7: ${{ steps.sanitized.outputs.text || inputs.command }} - GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_INPUTS_COMMAND: ${{ inputs.command }} - GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} - GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' - GH_AW_MEMORY_BRANCH_NAME: 'memory/repo-assist' - GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Max File Size**: 102400 bytes (0.10 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 1024 KB)\n" - GH_AW_MEMORY_DESCRIPTION: '' - GH_AW_MEMORY_DIR: '/tmp/gh-aw/repo-memory/default/' - GH_AW_MEMORY_TARGET_REPO: ' of the current repository' - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} - GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} - GH_AW_WIKI_NOTE: '' - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, - GH_AW_EXPR_1DD9F1B7: process.env.GH_AW_EXPR_1DD9F1B7, - GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, - GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, - GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_SERVER_URL: process.env.GH_AW_GITHUB_SERVER_URL, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_COMMAND: process.env.GH_AW_INPUTS_COMMAND, - GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, - GH_AW_MEMORY_BRANCH_NAME: process.env.GH_AW_MEMORY_BRANCH_NAME, - GH_AW_MEMORY_CONSTRAINTS: process.env.GH_AW_MEMORY_CONSTRAINTS, - GH_AW_MEMORY_DESCRIPTION: process.env.GH_AW_MEMORY_DESCRIPTION, - GH_AW_MEMORY_DIR: process.env.GH_AW_MEMORY_DIR, - GH_AW_MEMORY_TARGET_REPO: process.env.GH_AW_MEMORY_TARGET_REPO, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND, - GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: process.env.GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT, - GH_AW_WIKI_NOTE: process.env.GH_AW_WIKI_NOTE - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - - name: Upload activation artifact - if: success() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: activation - include-hidden-files: true - path: | - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/model_multipliers.json - /tmp/gh-aw/models.json - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw-prompts/prompt-template.txt - /tmp/gh-aw/aw-prompts/prompt-import-tree.json - /tmp/gh-aw/github_rate_limits.jsonl - /tmp/gh-aw/base - /tmp/gh-aw/.github/agents - /tmp/gh-aw/.github/skills - if-no-files-found: ignore - retention-days: 1 - - agent: - needs: activation - if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' - runs-on: ubuntu-latest - permissions: read-all - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - GH_AW_ASSETS_ALLOWED_EXTS: "" - GH_AW_ASSETS_BRANCH: "" - GH_AW_ASSETS_MAX_SIZE_KB: 0 - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_WORKFLOW_ID_SANITIZED: repoassist - outputs: - agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} - ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} - aic: ${{ steps.parse-mcp-gateway.outputs.aic }} - ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} - inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} - mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} - model: ${{ needs.activation.outputs.model }} - model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} - setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} - setup-span-id: ${{ steps.setup.outputs.span-id }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Set runtime paths - id: set-runtime-paths - run: | - { - echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" - echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" - echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" - } >> "$GITHUB_OUTPUT" - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - fetch-depth: 0 - - name: Fetch additional refs - env: - GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) - git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' - - name: Create gh-aw temp directory - run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - - name: Configure gh CLI for GitHub Enterprise - run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" - env: - GH_TOKEN: ${{ github.token }} - - name: Start DIFC Proxy - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_SERVER_URL: ${{ github.server_url }} - DIFC_PROXY_POLICY: '{"allow-only":{"min-integrity":"none","repos":"all"}}' - DIFC_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.25' - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh" - - name: Fetch repo data for task weighting - run: | - mkdir -p /tmp/gh-aw - - # Fetch open issues with labels (up to 500) - gh issue list --state open --limit 500 --json number,labels > /tmp/gh-aw/issues.json - - # Fetch open PRs with titles (up to 200) - gh pr list --state open --limit 200 --json number,title > /tmp/gh-aw/prs.json - - # Compute task weights and select three tasks for this run - python3 - << 'EOF' - import json, random, os - - with open('/tmp/gh-aw/issues.json') as f: - issues = json.load(f) - with open('/tmp/gh-aw/prs.json') as f: - prs = json.load(f) - - open_issues = len(issues) - unlabelled = sum(1 for i in issues if not i.get('labels')) - repo_assist_prs = sum(1 for p in prs if p['title'].startswith('[repo-assist]')) - other_prs = sum(1 for p in prs if not p['title'].startswith('[repo-assist]')) - - task_names = { - 1: 'Issue Labelling', - 2: 'Issue Investigation and Comment', - 3: 'Issue Investigation and Fix', - 4: 'Engineering Investments', - 5: 'Coding Improvements', - 6: 'Maintain Repo Assist PRs', - 7: 'Stale PR Nudges', - 8: 'Performance Improvements', - 9: 'Testing Improvements', - 10: 'Take the Repository Forward', - } - - weights = { - 1: 1 + 3 * unlabelled, - 2: 3 + 1 * open_issues, - 3: 3 + 0.7 * open_issues, - 4: 5 + 0.2 * open_issues, - 5: 5 + 0.1 * open_issues, - 6: float(repo_assist_prs), - 7: 0.1 * other_prs, - 8: 3 + 0.05 * open_issues, - 9: 3 + 0.05 * open_issues, - 10: 3 + 0.05 * open_issues, - } - - # Seed with run ID for reproducibility within a run - run_id = int(os.environ.get('GITHUB_RUN_ID', '0')) - rng = random.Random(run_id) - - task_ids = list(weights.keys()) - task_weights = [weights[t] for t in task_ids] - - # Weighted sample without replacement (pick 3 distinct tasks) - NUM_TASKS_PER_RUN = 3 - chosen, seen = [], set() - for t in rng.choices(task_ids, weights=task_weights, k=30): - if t not in seen: - seen.add(t) - chosen.append(t) - if len(chosen) == NUM_TASKS_PER_RUN: - break - - print('=== Repo Assist Task Selection ===') - print(f'Open issues : {open_issues}') - print(f'Unlabelled issues : {unlabelled}') - print(f'Repo Assist PRs : {repo_assist_prs}') - print(f'Other open PRs : {other_prs}') - print() - print('Task weights:') - for t, w in weights.items(): - tag = ' <-- SELECTED' if t in chosen else '' - print(f' Task {t:2d} ({task_names[t]}): weight {w:6.1f}{tag}') - print() - print(f'Selected tasks for this run: ' + ', '.join(f'Task {c} ({task_names[c]})' for c in chosen)) - - result = { - 'open_issues': open_issues, 'unlabelled_issues': unlabelled, - 'repo_assist_prs': repo_assist_prs, 'other_prs': other_prs, - 'task_names': task_names, - 'weights': {str(k): round(v, 2) for k, v in weights.items()}, - 'selected_tasks': chosen, - } - with open('/tmp/gh-aw/task_selection.json', 'w') as f: - json.dump(result, f, indent=2) - EOF - env: - GH_HOST: ${{ env.GH_HOST || 'github.com' }} - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} - GITHUB_API_URL: https://localhost:18443/api/v3 - GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql - NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt - # Repo memory git-based storage configuration from frontmatter processed below - - name: Clone repo-memory branch (default) - env: - GH_TOKEN: ${{ github.token }} - GITHUB_SERVER_URL: ${{ github.server_url }} - BRANCH_NAME: memory/repo-assist - TARGET_REPO: ${{ github.repository }} - MEMORY_DIR: /tmp/gh-aw/repo-memory/default - CREATE_ORPHAN: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/clone_repo_memory_branch.sh" - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 - env: - GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - - name: Parse integrity filter lists - id: parse-guard-vars - env: - GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} - GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} - GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} - run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Stop DIFC Proxy - if: always() - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_difc_proxy.sh" - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - - name: Restore agent config folders from base branch - if: steps.checkout-pr.outcome == 'success' - env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" - run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - - name: Restore inline sub-agents from activation artifact - env: - GH_AW_SUB_AGENT_DIR: ".github/agents" - GH_AW_SUB_AGENT_EXT: ".agent.md" - run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" - - name: Restore inline skills from activation artifact - env: - GH_AW_SKILL_DIR: ".github/skills" - run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c - - name: Generate Safe Outputs Config - run: | - mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" - 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_eafecb60c3605cdc_EOF' - {"add_comment":{"hide_older_comments":true,"max":10,"target":"*"},"add_labels":{"allowed":["bug","enhancement","help wanted","good first issue","spam","off topic","documentation","question","duplicate","wontfix","needs triage","needs investigation","breaking change","performance","security","refactor"],"max":30,"target":"*"},"create_issue":{"labels":["automation","repo-assist"],"max":4,"title_prefix":"[repo-assist] "},"create_pull_request":{"draft":true,"labels":["automation","repo-assist"],"max":4,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"fallback-to-issue","title_prefix":"[repo-assist] "},"create_report_incomplete_issue":{},"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":102400,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":4,"max_patch_size":1024,"protect_top_level_dot_folders":true,"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","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"fallback-to-issue","target":"*","title_prefix":"[repo-assist] "},"remove_labels":{"allowed":["bug","enhancement","help wanted","good first issue","spam","off topic","documentation","question","duplicate","wontfix","needs triage","needs investigation","breaking change","performance","security","refactor"],"max":5,"target":"*"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*"}} - GH_AW_SAFE_OUTPUTS_CONFIG_eafecb60c3605cdc_EOF - - name: Generate Safe Outputs Tools - env: - GH_AW_TOOLS_META_JSON: | - { - "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", - "add_labels": " CONSTRAINTS: Maximum 30 label(s) can be added. Only these labels are allowed: [\"bug\" \"enhancement\" \"help wanted\" \"good first issue\" \"spam\" \"off topic\" \"documentation\" \"question\" \"duplicate\" \"wontfix\" \"needs triage\" \"needs investigation\" \"breaking change\" \"performance\" \"security\" \"refactor\"]. Target: *.", - "create_issue": " CONSTRAINTS: Maximum 4 issue(s) can be created. Title will be prefixed with \"[repo-assist] \". Labels [\"automation\" \"repo-assist\"] will be automatically added.", - "create_pull_request": " CONSTRAINTS: Maximum 4 pull request(s) can be created. Title will be prefixed with \"[repo-assist] \". Labels [\"automation\" \"repo-assist\"] will be automatically added. PRs will be created as drafts.", - "push_to_pull_request_branch": " CONSTRAINTS: Maximum 4 push(es) can be made. The target pull request title must start with \"[repo-assist] \".", - "remove_labels": " CONSTRAINTS: Maximum 5 label(s) can be removed. Only these labels can be removed: [bug enhancement help wanted good first issue spam off topic documentation question duplicate wontfix needs triage needs investigation breaking change performance security refactor]. Target: *.", - "update_issue": " CONSTRAINTS: Maximum 1 issue(s) can be updated. Target: *." - }, - "repo_params": {}, - "dynamic_tools": [] - } - GH_AW_VALIDATION_JSON: | - { - "add_comment": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "item_number": { - "issueOrPRNumber": true - }, - "reply_to_id": { - "type": "string", - "maxLength": 256 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, - "add_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, - "create_issue": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000, - "minLength": 20 - }, - "fields": { - "type": "array" - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "parent": { - "issueOrPRNumber": true - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "temporary_id": { - "type": "string" - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "create_pull_request": { - "defaultMax": 1, - "fields": { - "base": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "branch": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "draft": { - "type": "boolean" - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "missing_data": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "context": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "data_type": { - "type": "string", - "sanitize": true, - "maxLength": 128 - }, - "reason": { - "type": "string", - "sanitize": true, - "maxLength": 256 - } - } - }, - "missing_tool": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 512 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "tool": { - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "noop": { - "defaultMax": 1, - "fields": { - "message": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - } - } - }, - "push_to_pull_request_branch": { - "defaultMax": 1, - "fields": { - "branch": { - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "message": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "pull_request_number": { - "issueOrPRNumber": true - } - } - }, - "remove_labels": { - "defaultMax": 5, - "fields": { - "item_number": { - "issueNumberOrTemporaryId": true - }, - "labels": { - "required": true, - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "repo": { - "type": "string", - "maxLength": 256 - } - } - }, - "report_incomplete": { - "defaultMax": 5, - "fields": { - "details": { - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 1024 - } - } - }, - "update_issue": { - "defaultMax": 1, - "fields": { - "assignees": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 39 - }, - "body": { - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "issue_number": { - "issueOrPRNumber": true - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "milestone": { - "optionalPositiveInteger": true - }, - "operation": { - "type": "string", - "enum": [ - "replace", - "append", - "prepend", - "replace-island" - ] - }, - "repo": { - "type": "string", - "maxLength": 256 - }, - "status": { - "type": "string", - "enum": [ - "open", - "closed" - ] - }, - "title": { - "type": "string", - "sanitize": true, - "maxLength": 128 - } - }, - "customValidation": "requiresOneOf:status,title,body" - } - } - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); - await main(); - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" - - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="8080" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - export MCP_GATEWAY_HOST_DOMAIN="localhost" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" - export DEBUG="*" - - export GH_AW_ENGINE="copilot" - MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') - MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - case "${DOCKER_HOST:-}" in - unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; - /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; - * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; - esac - DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.25' - - mkdir -p /home/runner/.copilot - GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_57afb4da0f373e73_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.2", - "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "all" - }, - "guard-policies": { - "allow-only": { - "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, - "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, - "min-integrity": "none", - "repos": "all", - "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} - } - } - }, - "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" - }, - "guard-policies": { - "write-sink": { - "accept": [ - "*" - ] - } - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_57afb4da0f373e73_EOF - - name: Mount MCP servers as CLIs - id: mount-mcp-clis - continue-on-error: true - env: - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); - await main(); - - name: Clean credentials - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" - - name: Audit pre-agent workspace - id: pre_agent_audit - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 60 - run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/agent-stdio.log) - GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.gradle-enterprise.cloud\",\"*.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\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" - fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log - env: - AWF_REFLECT_ENABLED: 1 - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json - GH_AW_PHASE: agent - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.79.6 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - - name: Detect agent errors - if: always() - id: detect-agent-errors - continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Append agent step summary - if: always() - run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" - - name: Copy Safe Outputs - if: always() - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - run: | - mkdir -p /tmp/gh-aw - cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true - - name: Ingest agent output - id: collect_output - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.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_COMMANDS: "[\"repo-assist\"]" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); - await main(); - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - id: parse-mcp-gateway - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi - - name: Parse token usage for step summary - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); - await main(); - - name: Print AWF reflect summary - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); - await main(); - - name: Write agent output placeholder if missing - if: always() - run: | - if [ ! -f /tmp/gh-aw/agent_output.json ]; then - echo '{"items":[]}' > /tmp/gh-aw/agent_output.json - fi - # Upload repo memory as artifacts for push job - - name: Sanitize repo-memory filenames (default) - if: always() - continue-on-error: true - env: - MEMORY_DIR: /tmp/gh-aw/repo-memory/default - run: bash "${RUNNER_TEMP}/gh-aw/actions/sanitize_repo_memory_filenames.sh" - - name: Upload repo-memory artifact (default) - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: repo-memory-default - path: /tmp/gh-aw/repo-memory/default - retention-days: 1 - if-no-files-found: ignore - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: agent - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/proxy-logs/ - !/tmp/gh-aw/proxy-logs/proxy-tls/ - /tmp/gh-aw/agent_usage.json - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/pre-agent-audit.txt - /tmp/gh-aw/agent/ - /tmp/gh-aw/github_rate_limits.jsonl - /tmp/gh-aw/safeoutputs.jsonl - /tmp/gh-aw/agent_output.json - /tmp/gh-aw/aw-*.patch - /tmp/gh-aw/aw-*.bundle - /tmp/gh-aw/awf-config.json - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/sandbox/firewall/audit/ - /tmp/gh-aw/sandbox/firewall/awf-reflect.json - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - push_repo_memory - - safe_outputs - if: > - always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || - needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') - runs-on: ubuntu-slim - permissions: - contents: write - discussions: write - issues: write - pull-requests: write - concurrency: - group: "gh-aw-conclusion-repo-assist" - cancel-in-progress: false - queue: max - outputs: - incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} - noop_message: ${{ steps.noop.outputs.noop_message }} - tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} - total_count: ${{ steps.missing_tool.outputs.total_count }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Collect usage artifact files - if: always() - continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -f /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -f /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -f /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - find /tmp/gh-aw/usage -type f -print | sort - - name: Upload usage artifact - if: always() - continue-on-error: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: usage - path: | - /tmp/gh-aw/usage/aw-info.jsonl - /tmp/gh-aw/usage/agent_usage.jsonl - /tmp/gh-aw/usage/detection_usage.jsonl - /tmp/gh-aw/usage/agent/token_usage.jsonl - /tmp/gh-aw/usage/detection/token_usage.jsonl - if-no-files-found: ignore - - name: Process no-op messages - id: noop - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/e15e57b40918dbca11b350c55d02ab61934afa75/workflows/repo-assist.md" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} - GH_AW_WORKFLOW_ID: "repo-assist" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); - await main(); - - name: Log detection run - id: detection_runs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/e15e57b40918dbca11b350c55d02ab61934afa75/workflows/repo-assist.md" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); - await main(); - - name: Record missing tool - id: missing_tool - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/e15e57b40918dbca11b350c55d02ab61934afa75/workflows/repo-assist.md" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Record incomplete - id: report_incomplete - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/e15e57b40918dbca11b350c55d02ab61934afa75/workflows/repo-assist.md" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); - await main(); - - name: Handle agent failure - id: handle_agent_failure - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/e15e57b40918dbca11b350c55d02ab61934afa75/workflows/repo-assist.md" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "repo-assist" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" - GH_AW_ENGINE_ID: "copilot" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} - GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} - GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} - GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} - GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} - GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} - GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} - GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" - GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} - GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} - GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} - GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_EXCEEDED: ${{ needs.activation.outputs.daily_effective_workflow_exceeded }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_effective_workflow_total_effective_tokens }} - GH_AW_DAILY_EFFECTIVE_WORKFLOW_THRESHOLD: ${{ needs.activation.outputs.daily_effective_workflow_threshold }} - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" - GH_AW_PUSH_REPO_MEMORY_RESULT: ${{ needs.push_repo_memory.result }} - GH_AW_REPO_MEMORY_VALIDATION_FAILED_default: ${{ needs.push_repo_memory.outputs.validation_failed_default }} - GH_AW_REPO_MEMORY_VALIDATION_ERROR_default: ${{ needs.push_repo_memory.outputs.validation_error_default }} - GH_AW_REPO_MEMORY_PATCH_SIZE_EXCEEDED_default: ${{ needs.push_repo_memory.outputs.patch_size_exceeded_default }} - GH_AW_GROUP_REPORTS: "false" - GH_AW_FAILURE_REPORT_AS_ISSUE: "true" - GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" - GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "60" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Update reaction comment with completion status - id: conclusion - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_SAFE_OUTPUTS_RESULT: ${{ needs.safe_outputs.result }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/notify_comment_error.cjs'); - await main(); - - detection: - needs: - - activation - - agent - if: > - always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - aic: ${{ steps.parse_detection_token_usage.outputs.aic }} - detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} - detection_reason: ${{ steps.detection_conclusion.outputs.reason }} - detection_success: ${{ steps.detection_conclusion.outputs.success }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Checkout repository for patch context - if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - # --- Threat Detection --- - - name: Clean stale firewall files from agent artifact - run: | - rm -rf /tmp/gh-aw/sandbox/firewall/logs - rm -rf /tmp/gh-aw/sandbox/firewall/audit - - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4 ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591 - - name: Check if detection needed - id: detection_guard - if: always() - env: - OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - run: | - if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then - echo "run_detection=true" >> "$GITHUB_OUTPUT" - echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" - else - echo "run_detection=false" >> "$GITHUB_OUTPUT" - echo "Detection skipped: no agent outputs or patches to analyze" - fi - - name: Clear MCP Config for detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" - rm -f /home/runner/.copilot/mcp-config.json - rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - - name: Prepare threat detection files - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true - if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then - echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." - fi - cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true - for f in /tmp/gh-aw/aw-*.patch; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - for f in /tmp/gh-aw/aw-*.bundle; do - [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true - done - echo "Prepared threat detection files:" - ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true - - name: Setup threat detection - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - WORKFLOW_NAME: "Repo Assist" - WORKFLOW_DESCRIPTION: "A friendly repository assistant that runs regularly (twice a day by default) to assist maintainers.\nCan also be triggered on-demand via '/repo-assist ' to perform specific tasks.\n- Labels and triages open issues\n- Comments helpfully on open issues to unblock contributors and onboard newcomers\n- Identifies issues that can be fixed and creates draft pull requests with fixes\n- Improves performance, testing, and code quality via PRs\n- Makes engineering investments: dependency updates, CI improvements, tooling\n- Updates its own PRs when CI fails or merge conflicts arise\n- Nudges stale PRs waiting for author response\n- Takes the repository forward with proactive improvements\n- Maintains a persistent memory of work done and what remains\nAlways polite, constructive, and mindful of the project's goals." - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.60 - env: - GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.2 - - name: Execute GitHub Copilot CLI - if: always() && steps.detection_guard.outputs.run_detection == 'true' - continue-on-error: true - id: detection_agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 20 - run: | - set -o pipefail - printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'rm -f /home/runner/.copilot/settings.json' EXIT - mkdir -p /home/runner/.copilot - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > /home/runner/.copilot/settings.json - touch /tmp/gh-aw/agent-step-summary.md - GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) - export GH_AW_NODE_BIN - export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" - (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) - GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.2/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}},\"container\":{\"imageTag\":\"0.27.2,squid=sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591,agent=sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6,api-proxy=sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4,cli-proxy=sha256:02f3ec08f32dc26c5427920c6a2e2f3036238fce44802f2f11ef49ed8621b5d0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}" - if [ -d "$GH_AW_TOOL_CACHE" ]; then - if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then - GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" - fi - elif [ -d "/home/runner/work/_tool" ]; then - GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" - fi - # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log - env: - AWF_REFLECT_ENABLED: 1 - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_PHASE: detection - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.79.6 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - RUNNER_TEMP: ${{ runner.temp }} - XDG_CONFIG_HOME: /home/runner - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); - await main(); - - name: Upload threat detection log - if: always() && steps.detection_guard.outputs.run_detection == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: detection - path: /tmp/gh-aw/threat-detection/detection.log - if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion - if: always() - continue-on-error: true - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} - DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} - GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" - with: - script: | - try { - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - } catch (loadErr) { - const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; - const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; - const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); - core.error(msg); - core.setOutput('reason', 'parse_error'); - if (continueOnError && !detectionExecutionFailed) { - core.warning('\u26A0\uFE0F ' + msg); - core.setOutput('conclusion', 'warning'); - core.setOutput('success', 'false'); - } else { - core.setOutput('conclusion', 'failure'); - core.setOutput('success', 'false'); - core.setFailed(msg); - } - } - - pre_activation: - if: > - github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) - runs-on: ubuntu-slim - permissions: - pull-requests: read - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_command_position.outputs.command_position_ok == 'true' }} - check_result: ${{ steps.check.outcome }} - matched_command: ${{ steps.check_command_position.outputs.matched_command }} - setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} - setup-span-id: ${{ steps.setup.outputs.span-id }} - setup-trace-id: ${{ steps.setup.outputs.trace-id }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Check team membership for command workflow - id: check_membership - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); - await main(); - - name: Check command position - id: check_command_position - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_COMMANDS: "[\"repo-assist\"]" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_command_position.cjs'); - await main(); - - id: check - run: | - MAX_OPEN_PRS=8 - if [[ "$GITHUB_EVENT_NAME" != "schedule" ]]; then exit 0; fi - COUNT=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --search 'in:title "[repo-assist]"' --json number --jq 'length') - [[ "$COUNT" -lt "$MAX_OPEN_PRS" ]] - - push_repo_memory: - needs: - - activation - - agent - - detection - if: > - always() && (!cancelled()) && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && - needs.agent.result == 'success' - runs-on: ubuntu-slim - permissions: - contents: write - concurrency: - group: "push-repo-memory-${{ github.repository }}|memory/repo-assist" - cancel-in-progress: false - outputs: - patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} - validation_error_default: ${{ steps.push_repo_memory_default.outputs.validation_error }} - validation_failed_default: ${{ steps.push_repo_memory_default.outputs.validation_failed }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: . - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GITHUB_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Download repo-memory artifact (default) - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - continue-on-error: true - with: - name: repo-memory-default - path: /tmp/gh-aw/repo-memory/default - - name: Push repo-memory changes (default) - id: push_repo_memory_default - if: always() - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - GITHUB_SERVER_URL: ${{ github.server_url }} - ARTIFACT_DIR: /tmp/gh-aw/repo-memory/default - MEMORY_ID: default - TARGET_REPO: ${{ github.repository }} - BRANCH_NAME: memory/repo-assist - MAX_FILE_SIZE: 102400 - MAX_FILE_COUNT: 100 - MAX_PATCH_SIZE: 10240 - ALLOWED_EXTENSIONS: '[]' - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/push_repo_memory.cjs'); - await main(); - - safe_outputs: - needs: - - activation - - agent - - detection - if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' - runs-on: ubuntu-slim - permissions: - contents: write - discussions: write - issues: write - pull-requests: write - timeout-minutes: 45 - env: - GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_AIC: ${{ needs.agent.outputs.aic }} - GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} - GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/repo-assist" - GH_AW_COMMANDS: "[\"repo-assist\"]" - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} - GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} - GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} - GH_AW_ENGINE_ID: "copilot" - GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.60" - GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" - GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} - GH_AW_WORKFLOW_ID: "repo-assist" - GH_AW_WORKFLOW_NAME: "Repo Assist" - GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/e15e57b40918dbca11b350c55d02ab61934afa75/workflows/repo-assist.md" - outputs: - code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} - code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} - comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} - comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} - created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} - created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} - created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} - process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} - process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} - push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} - steps: - - name: Setup Scripts - id: setup - uses: github/gh-aw-actions/setup@5c2fe865bb4dc46e1450f6ee0d0541d759aea73a # v0.79.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - job-name: ${{ github.job }} - trace-id: ${{ needs.activation.outputs.setup-trace-id }} - parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} - env: - GH_AW_SETUP_WORKFLOW_NAME: "Repo Assist" - GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/repo-assist.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.60" - GH_AW_INFO_AWF_VERSION: "v0.27.2" - GH_AW_INFO_BODY_MODIFIED: "false" - GH_AW_INFO_ENGINE_ID: "copilot" - - name: Download agent output artifact - id: download-agent-output - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Setup agent output environment variable - id: setup-agent-output-env - if: steps.download-agent-output.outcome == 'success' - run: | - mkdir -p /tmp/gh-aw/ - find "/tmp/gh-aw/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: agent - path: /tmp/gh-aw/ - - name: Extract base branch from agent output - id: extract-base-branch - if: steps.download-agent-output.outcome == 'success' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/extract_base_branch_from_agent_output.cjs'); - await main(); - - name: Checkout repository (trusted default branch for comment events) - if: ((!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 0 - - name: Checkout repository - if: ((!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch')) && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} - token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 0 - - name: Configure Git credentials - if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Configure GH_HOST for enterprise compatibility - id: ghes-host-config - shell: bash - # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. - run: | - # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct - # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. - GH_HOST="${GITHUB_SERVER_URL#https://}" - GH_HOST="${GH_HOST#http://}" - echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.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\":true,\"max\":10,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"help wanted\",\"good first issue\",\"spam\",\"off topic\",\"documentation\",\"question\",\"duplicate\",\"wontfix\",\"needs triage\",\"needs investigation\",\"breaking change\",\"performance\",\"security\",\"refactor\"],\"max\":30,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"repo-assist\"],\"max\":4,\"title_prefix\":\"[repo-assist] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"repo-assist\"],\"max\":4,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"title_prefix\":\"[repo-assist] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":4,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"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\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"target\":\"*\",\"title_prefix\":\"[repo-assist] \"},\"remove_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"help wanted\",\"good first issue\",\"spam\",\"off topic\",\"documentation\",\"question\",\"duplicate\",\"wontfix\",\"needs triage\",\"needs investigation\",\"breaking change\",\"performance\",\"security\",\"refactor\"],\"max\":5,\"target\":\"*\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\"}}" - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); - await main(); - - name: Upload Safe Outputs Items - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: safe-outputs-items - path: | - /tmp/gh-aw/safe-output-items.jsonl - /tmp/gh-aw/temporary-id-map.json - if-no-files-found: ignore - diff --git a/.github/workflows/repo-assist.md b/.github/workflows/repo-assist.md deleted file mode 100644 index 3256337c..00000000 --- a/.github/workflows/repo-assist.md +++ /dev/null @@ -1,424 +0,0 @@ ---- -description: | - A friendly repository assistant that runs regularly (twice a day by default) to assist maintainers. - Can also be triggered on-demand via '/repo-assist ' to perform specific tasks. - - Labels and triages open issues - - Comments helpfully on open issues to unblock contributors and onboard newcomers - - Identifies issues that can be fixed and creates draft pull requests with fixes - - Improves performance, testing, and code quality via PRs - - Makes engineering investments: dependency updates, CI improvements, tooling - - Updates its own PRs when CI fails or merge conflicts arise - - Nudges stale PRs waiting for author response - - Takes the repository forward with proactive improvements - - Maintains a persistent memory of work done and what remains - Always polite, constructive, and mindful of the project's goals. - -on: - schedule: weekly - workflow_dispatch: - inputs: - command: - description: "Optional command-mode instruction (for example: Run Task 9)" - required: false - type: string - default: "" - slash_command: - name: repo-assist - reaction: "eyes" - permissions: - pull-requests: read - steps: - - id: check - run: | - MAX_OPEN_PRS=8 - if [[ "$GITHUB_EVENT_NAME" != "schedule" ]]; then exit 0; fi - COUNT=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --search 'in:title "[repo-assist]"' --json number --jq 'length') - [[ "$COUNT" -lt "$MAX_OPEN_PRS" ]] - # exits 0 if not scheduled or Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md)." - run-started: "{workflow_name} is processing {event_type}, see [workflow run]({run_url})..." - run-success: "✓ {workflow_name} completed successfully, see [workflow run]({run_url})." - run-failure: "✗ {workflow_name} encountered {status}, see [workflow run]({run_url})." - add-comment: - max: 10 - target: "*" - hide-older-comments: true - create-pull-request: - draft: true - title-prefix: "[repo-assist] " - labels: [automation, repo-assist] - protected-files: fallback-to-issue - max: 4 - push-to-pull-request-branch: - target: "*" - required-title-prefix: "[repo-assist] " - max: 4 - protected-files: fallback-to-issue - create-issue: - title-prefix: "[repo-assist] " - labels: [automation, repo-assist] - max: 4 - update-issue: - target: "*" - required-title-prefix: "[repo-assist] " - max: 1 - add-labels: - allowed: [bug, enhancement, "help wanted", "good first issue", "spam", "off topic", documentation, question, duplicate, wontfix, "needs triage", "needs investigation", "breaking change", performance, security, refactor] - max: 30 - target: "*" - remove-labels: - allowed: [bug, enhancement, "help wanted", "good first issue", "spam", "off topic", documentation, question, duplicate, wontfix, "needs triage", "needs investigation", "breaking change", performance, security, refactor] - max: 5 - target: "*" - -steps: - - name: Fetch repo data for task weighting - env: - GH_TOKEN: ${{ github.token }} - run: | - mkdir -p /tmp/gh-aw - - # Fetch open issues with labels (up to 500) - gh issue list --state open --limit 500 --json number,labels > /tmp/gh-aw/issues.json - - # Fetch open PRs with titles (up to 200) - gh pr list --state open --limit 200 --json number,title > /tmp/gh-aw/prs.json - - # Compute task weights and select three tasks for this run - python3 - << 'EOF' - import json, random, os - - with open('/tmp/gh-aw/issues.json') as f: - issues = json.load(f) - with open('/tmp/gh-aw/prs.json') as f: - prs = json.load(f) - - open_issues = len(issues) - unlabelled = sum(1 for i in issues if not i.get('labels')) - repo_assist_prs = sum(1 for p in prs if p['title'].startswith('[repo-assist]')) - other_prs = sum(1 for p in prs if not p['title'].startswith('[repo-assist]')) - - task_names = { - 1: 'Issue Labelling', - 2: 'Issue Investigation and Comment', - 3: 'Issue Investigation and Fix', - 4: 'Engineering Investments', - 5: 'Coding Improvements', - 6: 'Maintain Repo Assist PRs', - 7: 'Stale PR Nudges', - 8: 'Performance Improvements', - 9: 'Testing Improvements', - 10: 'Take the Repository Forward', - } - - weights = { - 1: 1 + 3 * unlabelled, - 2: 3 + 1 * open_issues, - 3: 3 + 0.7 * open_issues, - 4: 5 + 0.2 * open_issues, - 5: 5 + 0.1 * open_issues, - 6: float(repo_assist_prs), - 7: 0.1 * other_prs, - 8: 3 + 0.05 * open_issues, - 9: 3 + 0.05 * open_issues, - 10: 3 + 0.05 * open_issues, - } - - # Seed with run ID for reproducibility within a run - run_id = int(os.environ.get('GITHUB_RUN_ID', '0')) - rng = random.Random(run_id) - - task_ids = list(weights.keys()) - task_weights = [weights[t] for t in task_ids] - - # Weighted sample without replacement (pick 3 distinct tasks) - NUM_TASKS_PER_RUN = 3 - chosen, seen = [], set() - for t in rng.choices(task_ids, weights=task_weights, k=30): - if t not in seen: - seen.add(t) - chosen.append(t) - if len(chosen) == NUM_TASKS_PER_RUN: - break - - print('=== Repo Assist Task Selection ===') - print(f'Open issues : {open_issues}') - print(f'Unlabelled issues : {unlabelled}') - print(f'Repo Assist PRs : {repo_assist_prs}') - print(f'Other open PRs : {other_prs}') - print() - print('Task weights:') - for t, w in weights.items(): - tag = ' <-- SELECTED' if t in chosen else '' - print(f' Task {t:2d} ({task_names[t]}): weight {w:6.1f}{tag}') - print() - print(f'Selected tasks for this run: ' + ', '.join(f'Task {c} ({task_names[c]})' for c in chosen)) - - result = { - 'open_issues': open_issues, 'unlabelled_issues': unlabelled, - 'repo_assist_prs': repo_assist_prs, 'other_prs': other_prs, - 'task_names': task_names, - 'weights': {str(k): round(v, 2) for k, v in weights.items()}, - 'selected_tasks': chosen, - } - with open('/tmp/gh-aw/task_selection.json', 'w') as f: - json.dump(result, f, indent=2) - EOF - -source: githubnext/agentics/workflows/repo-assist.md@e15e57b40918dbca11b350c55d02ab61934afa75 ---- - -# Repo Assist - -## Command Mode - -Take heed of **instructions**: "${{ steps.sanitized.outputs.text || inputs.command }}" - -If these are non-empty (not ""), then you have been triggered via `/repo-assist ` (or by the user setting `inputs.command` in a manual `workflow_dispatch`). Follow the user's instructions instead of the normal scheduled workflow. Focus exclusively on those instructions. Apply all the same guidelines (read AGENTS.md, run formatters/linters/tests, be polite, use AI disclosure). Skip the weighted task selection and Task 11 reporting, and instead directly do what the user requested. If no specific instructions were provided (empty or blank), proceed with the normal scheduled workflow below. - -Then exit - do not run the normal workflow after completing the instructions. - -## Non-Command Mode - -You are Repo Assist for `${{ github.repository }}`. Your job is to support human contributors, help onboard newcomers, identify improvements, and fix bugs by creating pull requests. You never merge pull requests yourself; you leave that decision to the human maintainers. - -Always be: - -- **Polite and encouraging**: Every contributor deserves respect. Use warm, inclusive language. -- **Concise**: Keep comments focused and actionable. Avoid walls of text. -- **Mindful of project values**: Prioritize **stability**, **correctness**, and **minimal dependencies**. Do not introduce new dependencies without clear justification. -- **Transparent about your nature**: Always clearly identify yourself as Repo Assist, an automated AI assistant. Never pretend to be a human maintainer. -- **Restrained**: When in doubt, do nothing. It is always better to stay silent than to post a redundant, unhelpful, or spammy comment. Human maintainers' attention is precious - do not waste it. - -## Memory - -Use persistent repo memory to track: - -- issues already commented on (with timestamps to detect new human activity) -- fix attempts and outcomes, improvement ideas already submitted, a short to-do list -- a **backlog cursor** so each run continues where the previous one left off -- previously checked off items (checked off by maintainer) in the Monthly Activity Summary to maintain an accurate pending actions list for maintainers - -Read memory at the **start** of every run; update it at the **end**. - -**Important**: Memory may not be 100% accurate. Issues may have been created, closed, or commented on; PRs may have been created, merged, commented on, or closed since the last run. Always verify memory against current repository state — reviewing recent activity since your last run is wise before acting on stale assumptions. - -**Memory backlog tracking**: Your memory may contain notes about issues or PRs that still need attention (e.g., "issues #384, #336 have labels but no comments"). These are **action items for you**, not just informational notes. Each run, check your memory's `notes` field and other tracking fields for any explicitly flagged backlog work, and prioritise acting on it. - -## Workflow - -Each run, the deterministic pre-step collects live repo data (open issue count, unlabelled issue count, open Repo Assist PRs, other open PRs), computes a **weighted probability** for each task, and selects **three tasks** for this run using a seeded random draw. The weights and selected tasks are printed in the workflow logs. You will find the selection in `/tmp/gh-aw/task_selection.json`. - -**Read the task selection**: at the start of your run, read `/tmp/gh-aw/task_selection.json` and confirm the three selected tasks in your opening reasoning. Execute **those three tasks** (plus the mandatory Task 11). If a selected task is not applicable to the current repo state, substitute its fallback task rather than doing nothing. Record the substitution in the Task 11 run history entry. - -| Selected task | Not applicable when… | Fallback | -|---|---|---| -| Task 1 (Issue Labelling) | All open issues already labelled | Task 2 | -| Task 2 (Issue Comment) | All open issues already have a recent Repo Assist comment and no new human activity | Task 1 | -| Task 3 (Issue Fix) | No issues labelled `bug`, `help wanted`, or `good first issue` that are fixable | Task 2 | -| Task 4 (Engineering Investments) | No actionable dependency updates, CI gaps, or build improvements identifiable | Task 5 | -| Task 5 (Coding Improvements) | No clearly beneficial, low-risk improvements identifiable after reviewing the codebase | Task 9 | -| Task 6 (Maintain Repo Assist PRs) | No open Repo Assist PRs exist | Task 2 | -| Task 7 (Stale PR Nudges) | No non-Repo-Assist PRs stale 14+ days, or all already nudged | Task 2 | -| Task 8 (Performance Improvements) | No measurable performance opportunities identifiable | Task 9 | -| Task 9 (Testing Improvements) | Test coverage is already comprehensive and no gaps identified | Task 5 | -| Task 10 (Take Repo Forward) | In-progress work from memory is blocked or complete; no valuable next step | Task 2 | - -The weighting scheme naturally adapts to repo state: - -- When unlabelled issues pile up, Task 1 (labelling) dominates. -- When there are many open issues, Tasks 2 and 3 (commenting and fixing) get more weight. -- As the backlog clears, Tasks 4–10 (engineering, improvements, nudges, forward progress) draw more evenly. - -**Repeat-run mode**: When invoked via `gh aw run repo-assist --repeat`, runs occur every 5–10 minutes. Each run is independent — do not skip a run. Always check memory to avoid duplicate work across runs. - -**Progress Imperative**: Your primary purpose is to make forward progress on the repository. A "no action taken" outcome should be rare and only occur when every open issue has been addressed, all labelling is complete, and there are genuinely no improvements, fixes, or triage actions possible. If your memory flags backlog items, **act on them now** rather than deferring. - -Always do Task 11 (Update Monthly Activity Summary Issue) every run. In all comments and PR descriptions, identify yourself as "Repo Assist". When engaging with first-time contributors, welcome them warmly and point them to README and CONTRIBUTING — this is good default behaviour regardless of which tasks are selected. - -### Task 1: Issue Labelling - -Process as many unlabelled issues and PRs as possible each run. Resume from memory's backlog cursor. - -For each item, apply the best-fitting labels from: `bug`, `enhancement`, `help wanted`, `good first issue`, `documentation`, `question`, `duplicate`, `wontfix`, `spam`, `off topic`, `needs triage`, `needs investigation`, `breaking change`, `performance`, `security`, `refactor`. Remove misapplied labels. Apply multiple where appropriate; skip any you're not confident about. After labelling, post a brief comment if you have something genuinely useful to add. - -Update memory with labels applied and cursor position. - -### Task 2: Issue Investigation and Comment - -1. List open issues sorted by creation date ascending (oldest first). Resume from your memory's backlog cursor; reset when you reach the end. -2. **Prioritise issues that have never received a Repo Assist comment.** Read the issue comments and check memory's `comments_made` field. Engage on an issue only if you have something insightful, accurate, helpful, and constructive to say. Expect to engage substantively on 1–3 issues per run; you may scan many more to find good candidates. Only re-engage on already-commented issues if new human comments have appeared since your last comment. -3. Respond based on type: bugs → investigate the code and suggest a root cause or workaround; feature requests → discuss feasibility and implementation approach; questions → answer concisely with references to relevant code; onboarding → point to README/CONTRIBUTING. Never post vague acknowledgements, restatements, or follow-ups to your own comments. -4. Begin every comment with: `🤖 *This is an automated response from Repo Assist.*` -5. Update memory with comments made and the new cursor position. - -### Task 3: Issue Investigation and Fix - -**Only attempt fixes you are confident about.** It is fine to work on issues you have previously commented on. - -1. Review issues labelled `bug`, `help wanted`, or `good first issue`, plus any identified as fixable during investigation. -2. For each fixable issue: - a. Check memory — skip if you've already tried and the attempt is still open. Never create duplicate PRs. - b. Create a fresh branch off the default branch of the repository: `repo-assist/fix-issue--`. - c. Implement a minimal, surgical fix. Do not refactor unrelated code. - d. **Build and test (required)**: do not create a PR if the build fails or tests fail due to your changes. If tests fail due to infrastructure, create the PR but document it. - e. Add a test for the bug if feasible; re-run tests. - f. Create a draft PR with: AI disclosure, `Closes #N`, root cause, fix rationale, trade-offs, and a Test Status section showing build/test outcome. - g. Post a single brief comment on the issue linking to the PR. -3. Update memory with fix attempts and outcomes. - -### Task 4: Engineering Investments - -Improve the engineering foundations of the repository. Consider: - -- **Dependency updates**: Check for outdated dependencies. Prefer minor/patch updates; propose major bumps only with clear benefit. **Bundle Dependabot PRs**: If multiple open Dependabot PRs exist, create a single bundled PR applying all compatible updates. Reference the original PRs so maintainers can close them after merging. -- **CI improvements**: Speed up CI pipelines, fix flaky tests, improve caching, upgrade actions. -- **Tooling and SDK versions**: Update runtime versions, linters, formatters. -- **Build system**: Simplify or modernise the build configuration. - -For any change: create a fresh branch `repo-assist/eng--`, implement the change, build and test, then create a draft PR with AI disclosure and Test Status section. Update memory with what was checked and when. - -### Task 5: Coding Improvements - -Study the codebase and make clearly beneficial, low-risk improvements. **Be highly selective — only propose changes with obvious value.** - -Good candidates: code clarity and readability, removing dead code, API usability, documentation gaps, reducing duplication. - -Check memory for already-submitted ideas; do not re-propose them. Create a fresh branch `repo-assist/improve-` off the default branch of the repository, implement the improvement, build and test (same requirements as Task 3), then create a draft PR with AI disclosure, rationale, and Test Status section. If not ready to implement, file an issue instead. Update memory. - -### Task 6: Maintain Repo Assist PRs - -1. List all open PRs with the `[repo-assist]` title prefix. -2. For each PR: fix CI failures caused by your changes by pushing updates; resolve merge conflicts. If you've retried multiple times without success, comment and leave for human review. -3. Do not push updates for infrastructure-only failures — comment instead. -4. Update memory. - -### Task 7: Stale PR Nudges - -1. List open non-Repo-Assist PRs not updated in 14+ days. -2. For each (check memory — skip if already nudged): if the PR is waiting on the author, post a single polite comment asking if they need help or want to hand off. Do not comment if the PR is waiting on a maintainer. -3. **Maximum 3 nudges per run.** Update memory. - -### Task 8: Performance Improvements - -Identify and implement meaningful performance improvements. Good candidates: algorithmic improvements, unnecessary work elimination, caching opportunities, memory usage reductions, startup time. Only propose changes with a clear, measurable benefit. Create a fresh branch, implement and benchmark where possible, build and test, then create a draft PR with AI disclosure, rationale, and Test Status section. Update memory. - -### Task 9: Testing Improvements - -Improve the quality and coverage of the test suite. Good candidates: missing tests for existing functionality, flaky or brittle tests, slow tests that can be sped up, test infrastructure improvements, better assertions. Avoid adding low-value tests just to inflate coverage. Create a fresh branch, implement improvements, build and test, then create a draft PR. Update memory. - -### Task 10: Take the Repository Forward - -Proactively move the repository forward. Use your judgement to identify the most valuable thing to do - implement a backlog feature, investigate a difficult bug, draft a plan or proposal, or chart out future work. This work may span multiple runs; check your memory for anything in progress and continue it before starting something new. Record progress and next steps in memory at the end of each run. - -### Task 11: Update Monthly Activity Summary Issue (ALWAYS DO THIS TASK IN ADDITION TO OTHERS) - -Maintain a single open issue titled `[repo-assist] Monthly Activity {YYYY}-{MM}` as a rolling summary of all Repo Assist activity for the current month. - -1. Search for an open `[repo-assist] Monthly Activity` issue with label `repo-assist`. If it's for the current month, update it. If for a previous month, close it and create a new one. Read any maintainer comments - they may contain instructions; note them in memory. -2. **Issue body format** - use **exactly** this structure: - - ```markdown - 🤖 *Repo Assist here - I'm an automated AI assistant for this repository.* - - ## Activity for - - ## Suggested Actions for Maintainer - - **Comprehensive list** of all pending actions requiring maintainer attention (excludes items already actioned and checked off). - - Reread the issue you're updating before you update it - there may be new checkbox adjustments since your last update that require you to adjust the suggested actions. - - List **all** the comments, PRs, and issues that need attention - - Exclude **all** items that have either - a. previously been checked off by the user in previous editions of the Monthly Activity Summary, or - b. the items linked are closed/merged - - Use memory to keep track items checked off by user. - - Be concise - one line per item., repeating the format lines as necessary: - - * [ ] **Review PR** #: - [Review]() - * [ ] **Check comment** #: Repo Assist commented - verify guidance is helpful - [View]() - * [ ] **Merge PR** #: - [Review]() - * [ ] **Close issue** #: - [View]() - * [ ] **Close PR** #: - [View]() - * [ ] **Define goal**: - [Related issue]() - - *(If no actions needed, state "No suggested actions at this time.")* - - ## Future Work for Repo Assist - - {Very briefly list future work for Repo Assist} - - *(If nothing pending, skip this section.)* - - ## Run History - - ### - [Run](/actions/runs/>) - - 💬 Commented on #: - - 🔧 Created PR #: - - 🏷️ Labelled # with ` - /// - /// A function that updates the state with each element from the sequence. - /// The initial state. - /// The input sequence. - /// The state object after the folding function is applied to each element of the sequence. - /// Thrown when the input task sequence is null. - static member fold: folder: ('State -> 'T -> 'State) -> state: 'State -> source: TaskSeq<'T> -> Task<'State> - - /// - /// Applies the asynchronous function to each element in the task sequence, threading an accumulator - /// argument of type through the computation. If the input function is and the elements are - /// then computes . - /// If the accumulator function is synchronous, consider using . - /// - /// - /// A function that updates the state with each element from the sequence. - /// The initial state. - /// The input sequence. - /// The state object after the folding function is applied to each element of the sequence. - /// Thrown when the input task sequence is null. - static member foldAsync: - folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State> - - /// - /// Applies the function to each element in the task sequence, threading an - /// accumulator of type through the computation, for as long as - /// returns true. The predicate is evaluated against the current - /// state and next element before that element is folded in; once it returns false the element - /// is not folded, iteration stops, and no further elements of the input are enumerated. - /// If either function is asynchronous, consider using . - /// - /// - /// A function that, given the current state and next element, returns true to keep folding or false to stop. - /// A function that updates the state with each element from the sequence. - /// The initial state. - /// The input sequence. - /// The state object after iteration halted, or after the whole sequence was consumed. - /// Thrown when the input task sequence is null. - static member foldWhile: - predicate: ('State -> 'T -> bool) -> - folder: ('State -> 'T -> 'State) -> - state: 'State -> - source: TaskSeq<'T> -> - Task<'State> - - /// - /// Applies the asynchronous function to each element in the task sequence, - /// threading an accumulator of type through the computation, for as long as - /// the asynchronous returns true. The predicate is evaluated - /// against the current state and next element before that element is folded in; once it returns - /// false the element is not folded, iteration stops, and no further elements of the input are - /// enumerated. - /// If both functions are synchronous, consider using . - /// - /// - /// An async function that, given the current state and next element, returns true to keep folding or false to stop. - /// An async function that updates the state with each element from the sequence. - /// The initial state. - /// The input sequence. - /// The state object after iteration halted, or after the whole sequence was consumed. - /// Thrown when the input task sequence is null. - static member foldWhileAsync: - predicate: ('State -> 'T -> #Task) -> - folder: ('State -> 'T -> #Task<'State>) -> - state: 'State -> - source: TaskSeq<'T> -> - Task<'State> - - /// - /// Like , but returns the sequence of intermediate results and the final result. - /// The first element of the output sequence is always the initial state. If the input task sequence - /// has N elements, the output task sequence has N + 1 elements. - /// If the folder function is asynchronous, consider using . - /// - /// - /// A function that updates the state with each element from the sequence. - /// The initial state. - /// The input sequence. - /// A task sequence of states, starting with the initial state and applying the folder to each element. - /// Thrown when the input task sequence is null. - static member scan: folder: ('State -> 'T -> 'State) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'State> - - /// - /// Like , but returns the sequence of intermediate results and the final result. - /// The first element of the output sequence is always the initial state. If the input task sequence - /// has N elements, the output task sequence has N + 1 elements. - /// If the folder function is synchronous, consider using . - /// - /// - /// A function that updates the state with each element from the sequence. - /// The initial state. - /// The input sequence. - /// A task sequence of states, starting with the initial state and applying the folder to each element. - /// Thrown when the input task sequence is null. - static member scanAsync: - folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'State> - - /// - /// Applies the function to each element of the task sequence, threading an accumulator - /// argument through the computation, while also generating a new mapped element for each input element. - /// If the input function is and the elements are , then - /// computes both the mapped results and the final state in a single pass. - /// The result is a pair of an array of mapped values and the final state. - /// If the mapping function is asynchronous, consider using . - /// - /// - /// A function that maps each element to a result while also updating the state. - /// The initial state. - /// The input task sequence. - /// A task returning a pair of the array of mapped results and the final state. - /// Thrown when the input task sequence is null. - static member mapFold: - mapping: ('State -> 'T -> 'Result * 'State) -> state: 'State -> source: TaskSeq<'T> -> Task<'Result[] * 'State> - - /// - /// Applies the asynchronous function to each element of the task sequence, - /// threading an accumulator argument through the computation, while also generating a new mapped element for each input element. - /// If the input function is and the elements are , then - /// computes both the mapped results and the final state in a single pass. - /// The result is a pair of an array of mapped values and the final state. - /// If the mapping function is synchronous, consider using . - /// - /// - /// An asynchronous function that maps each element to a result while also updating the state. - /// The initial state. - /// The input task sequence. - /// A task returning a pair of the array of mapped results and the final state. - /// Thrown when the input task sequence is null. - static member mapFoldAsync: - mapping: ('State -> 'T -> #Task<'Result * 'State>) -> - state: 'State -> - source: TaskSeq<'T> -> - Task<'Result[] * 'State> - - /// - /// Applies the function to each element of the task sequence, threading a state - /// argument through the computation, and lazily yields each mapped result as a new task sequence. Unlike - /// , the results are streamed rather than collected into an array, and the - /// final state is not returned. - /// If the function is asynchronous, consider using . - /// - /// - /// A function that maps each element to a result while also updating the state. - /// The initial state. - /// The input task sequence. - /// A task sequence of mapped results, produced lazily in source order. - /// Thrown when the input task sequence is null. - static member threadState: - folder: ('State -> 'T -> 'Result * 'State) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'Result> - - /// - /// Applies the asynchronous function to each element of the task sequence, threading - /// a state argument through the computation, and lazily yields each mapped result as a new task sequence. Unlike - /// , the results are streamed rather than collected into an array, and the - /// final state is not returned. - /// If the function is synchronous, consider using . - /// - /// - /// An asynchronous function that maps each element to a result while also updating the state. - /// The initial state. - /// The input task sequence. - /// A task sequence of mapped results, produced lazily in source order. - /// Thrown when the input task sequence is null. - static member threadStateAsync: - folder: ('State -> 'T -> #Task<'Result * 'State>) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'Result> - - - /// - /// Applies the function to each element of the task sequence, threading - /// an accumulator argument through the computation. The first element is used as the initial state. If the input - /// function is and the elements are , then computes - /// . Raises when the - /// sequence is empty. - /// If the accumulator function is asynchronous, consider using . - /// - /// - /// A function that updates the state with each element from the sequence. - /// The input sequence. - /// The final state value after applying the reduction function to all elements. - /// Thrown when the input task sequence is null. - /// Thrown when the input task sequence is empty. - static member reduce: folder: ('T -> 'T -> 'T) -> source: TaskSeq<'T> -> Task<'T> - - /// - /// Applies the asynchronous function to each element of the task sequence, threading - /// an accumulator argument through the computation. The first element is used as the initial state. If the input - /// function is and the elements are , then computes - /// . Raises when the - /// sequence is empty. - /// If the accumulator function is synchronous, consider using . - /// - /// - /// A function that updates the state with each element from the sequence. - /// The input sequence. - /// The final state value after applying the reduction function to all elements. - /// Thrown when the input task sequence is null. - /// Thrown when the input task sequence is empty. - static member reduceAsync: folder: ('T -> 'T -> #Task<'T>) -> source: TaskSeq<'T> -> Task<'T> - - /// - /// Applies a key-generating function to each element of a task sequence and yields a sequence of unique keys - /// and arrays of all elements that have each key, in order of first occurrence of each key. - /// The returned array preserves the original order of elements within each group. - /// - /// - /// - /// This function consumes the entire source task sequence before returning. - /// If the projection function is asynchronous, consider using - /// . - /// - /// - /// A function that transforms each element into a key. - /// The input task sequence. - /// A task returning an array of (key, elements[]) pairs. - /// Thrown when the input task sequence is null. - static member groupBy: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> Task<('Key * 'T[])[]> when 'Key: equality - - /// - /// Applies an asynchronous key-generating function to each element of a task sequence and yields a sequence of - /// unique keys and arrays of all elements that have each key, in order of first occurrence of each key. - /// The returned array preserves the original order of elements within each group. - /// - /// - /// - /// This function consumes the entire source task sequence before returning. - /// If the projection function is synchronous, consider using - /// . - /// - /// - /// An asynchronous function that transforms each element into a key. - /// The input task sequence. - /// A task returning an array of (key, elements[]) pairs. - /// Thrown when the input task sequence is null. - static member groupByAsync: - projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> Task<('Key * 'T[])[]> when 'Key: equality - - /// - /// Applies a key-generating function to each element of a task sequence and returns a task with an array of - /// unique keys and their element counts, in order of first occurrence of each key. - /// - /// - /// - /// This function consumes the entire source task sequence before returning. - /// If the projection function is asynchronous, consider using - /// . - /// - /// - /// A function that transforms each element into a key. - /// The input task sequence. - /// A task returning an array of (key, count) pairs. - /// Thrown when the input task sequence is null. - static member countBy: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> Task<('Key * int)[]> when 'Key: equality - - /// - /// Applies an asynchronous key-generating function to each element of a task sequence and returns a task with - /// an array of unique keys and their element counts, in order of first occurrence of each key. - /// - /// - /// - /// This function consumes the entire source task sequence before returning. - /// If the projection function is synchronous, consider using - /// . - /// - /// - /// An asynchronous function that transforms each element into a key. - /// The input task sequence. - /// A task returning an array of (key, count) pairs. - /// Thrown when the input task sequence is null. - static member countByAsync: - projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> Task<('Key * int)[]> when 'Key: equality - - /// - /// Splits the task sequence into two arrays: those for which the given predicate returns true, - /// and those for which it returns false. The relative order of elements within each partition is preserved. - /// - /// - /// - /// This function consumes the entire source task sequence before returning. - /// If the predicate function is asynchronous, consider using - /// . - /// - /// - /// A function that returns true for elements to include in the first array. - /// The input task sequence. - /// A task returning a tuple of two arrays: (trueItems, falseItems). - /// Thrown when the input task sequence is null. - static member partition: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<'T[] * 'T[]> - - /// - /// Splits the task sequence into two arrays using an asynchronous predicate: those for which the predicate returns - /// true, and those for which it returns false. The relative order of elements within each partition - /// is preserved. - /// - /// - /// - /// This function consumes the entire source task sequence before returning. - /// If the predicate function is synchronous, consider using - /// . - /// - /// - /// An asynchronous function that returns true for elements to include in the first array. - /// The input task sequence. - /// A task returning a tuple of two arrays: (trueItems, falseItems). - /// Thrown when the input task sequence is null. - static member partitionAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task<'T[] * 'T[]> - - /// - /// Return a new task sequence with a new item inserted before the given index. - /// - /// - /// The index where the item should be inserted. - /// The value to insert. - /// The input task sequence. - /// The result task sequence. - /// Thrown when the input task sequence is null. - /// Thrown when index is below 0 or greater than source length. - static member insertAt: index: int -> value: 'T -> source: TaskSeq<'T> -> TaskSeq<'T> - - /// - /// Return a new task sequence with the new items inserted before the given index. - /// - /// - /// The index where the items should be inserted. - /// The values to insert. - /// The input task sequence. - /// The result task sequence. - /// Thrown when the input task sequence is null. - /// Thrown when index is below 0 or greater than source length. - static member insertManyAt: index: int -> values: TaskSeq<'T> -> source: TaskSeq<'T> -> TaskSeq<'T> - - /// - /// Return a new task sequence with the item at the given index removed. - /// - /// - /// The index where the item should be removed. - /// The input task sequence. - /// The result task sequence. - /// Thrown when the input task sequence is null. - /// Thrown when index is below 0 or greater than source length. - static member removeAt: index: int -> source: TaskSeq<'T> -> TaskSeq<'T> - - /// - /// Return a new task sequence with the number of items starting at a given index removed. - /// If is negative or zero, no items are removed. If - /// + is greater than source length, but is not, then - /// all items until end of sequence are removed. - /// - /// - /// The index where the items should be removed. - /// The number of items to remove. - /// The input task sequence. - /// The result task sequence. - /// Thrown when the input task sequence is null. - /// Thrown when index is below 0 or greater than source length. - static member removeManyAt: index: int -> count: int -> source: TaskSeq<'T> -> TaskSeq<'T> - - /// - /// Return a new task sequence with the item at a given index set to the new value. - /// - /// - /// The index of the item to be replaced. - /// The new value. - /// The input task sequence. - /// The result task sequence. - /// Thrown when the input task sequence is null. - /// Thrown when index is below 0 or greater than source length. - static member updateAt: index: int -> value: 'T -> source: TaskSeq<'T> -> TaskSeq<'T> diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs deleted file mode 100644 index 9a8fd826..00000000 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ /dev/null @@ -1,871 +0,0 @@ -namespace FSharp.Control - -open System.Diagnostics - -// note: this is *not* an experimental feature, but they forgot to switch off the flag -#nowarn "57" // Experimental library feature, requires '--langversion:preview'. -#nowarn "3513" // Resumable code invocation: intentionally calling ResumableCode as a regular delegate in the dynamic (FSI) fallback path. - -open System -open System.Collections.Generic -open System.Threading -open System.Threading.Tasks -open System.Runtime.CompilerServices -open System.Threading.Tasks.Sources - -open FSharp.Core.CompilerServices -open FSharp.Core.CompilerServices.StateMachineHelpers // raises warning FS0057 -open FSharp.Control - - -[] -module Internal = // cannot be marked with 'internal' scope - - let initVerbose () = - try - match Environment.GetEnvironmentVariable "TASKSEQ_LOG_VERBOSE" with - | null -> false - | x -> - match x.ToLowerInvariant().Trim() with - | "1" - | "true" - | "on" - | "yes" -> true - | _ -> false - - with _ -> - false - - - let inline moveNextRef (x: byref<'T> when 'T :> IAsyncStateMachine) = x.MoveNext() - - let inline raiseNotImpl () = - NotImplementedException "Abstract Class: method or property not implemented" - |> raise - -// deprecated from 0.4.0, see FSI file -[' is deprecated in favor of 'TaskSeq<_>'. It will be removed in an upcoming release.">] -type taskSeq<'T> = IAsyncEnumerable<'T> - -// the proper type from 0.4.0 onwards, see FSI file -type TaskSeq<'T> = IAsyncEnumerable<'T> - - -[] -type TaskSeqStateMachineData<'T>() = - - [] - val mutable cancellationToken: CancellationToken - - /// Keeps track of the objects that need to be disposed off on IAsyncDispose. - [] - val mutable disposalStack: ResizeArray<(unit -> Task)> - - [] - val mutable awaiter: ICriticalNotifyCompletion - - [] - val mutable promiseOfValueOrEnd: ManualResetValueTaskSourceCore - - /// Helper struct providing methods for awaiting 'next' in async iteration scenarios. - [] - val mutable builder: AsyncIteratorMethodBuilder - - /// Whether or not a full iteration through the IAsyncEnumerator has completed - [] - val mutable completed: bool - - /// Used by the AsyncEnumerator interface to return the Current value when - /// IAsyncEnumerator.Current is called - [] - val mutable current: ValueOption<'T> - - /// A reference to 'self', because otherwise we can't use byref in the resumable code. - [] - val mutable boxedSelf: TaskSeqBase<'T> - - member data.PushDispose(disposer: unit -> Task) = - if isNull data.disposalStack then - data.disposalStack <- ResizeArray() - - data.disposalStack.Add disposer - - member data.PopDispose() = - if not (isNull data.disposalStack) then - data.disposalStack.RemoveAt(data.disposalStack.Count - 1) - -and [] TaskSeqBase<'T>() = - - abstract MoveNextAsyncResult: unit -> ValueTask - - interface IAsyncEnumerator<'T> with - member _.Current = raiseNotImpl () - member _.MoveNextAsync() = raiseNotImpl () - member _.DisposeAsync() = raiseNotImpl () - - interface IAsyncEnumerable<'T> with - member _.GetAsyncEnumerator(ct) = raiseNotImpl () - - interface IAsyncStateMachine with - member _.MoveNext() = raiseNotImpl () - member _.SetStateMachine(_state) = raiseNotImpl () - - interface IValueTaskSource with - member _.GetResult(_token: int16) = raiseNotImpl () - member _.GetStatus(_token: int16) = raiseNotImpl () - member _.OnCompleted(_continuation, _state, _token, _flags) = raiseNotImpl () - - interface IValueTaskSource with - member _.GetStatus(_token: int16) = raiseNotImpl () - member _.GetResult(_token: int16) = raiseNotImpl () - member _.OnCompleted(_continuation, _state, _token, _flags) = raiseNotImpl () - -and [] TaskSeq<'Machine, 'T - when 'Machine :> IAsyncStateMachine and 'Machine :> IResumableStateMachine>>() = - inherit TaskSeqBase<'T>() - let initialThreadId = Environment.CurrentManagedThreadId - - /// Shadows the initial machine, just after it is initialized by the F# compiler-generated state. - /// Used on GetAsyncEnumerator, to ensure a clean state, and a ResumptionPoint of 0. - [] - val mutable _initialMachine: 'Machine - - /// Keeps the active state machine. - [] - val mutable _machine: 'Machine - - member this.InitMachineData(ct, machine: 'Machine byref) = - let data = TaskSeqStateMachineData() - data.boxedSelf <- this - data.cancellationToken <- ct - data.builder <- AsyncIteratorMethodBuilder.Create() - machine.Data <- data - - // Note: Not entirely clear if this is needed, everything still compiles without it - interface IValueTaskSource with - member this.GetResult token = - let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token - - if not canMoveNext then - // see below in generic version for explanation - this._machine.Data.completed <- true - - member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token - - member this.OnCompleted(continuation, state, token, flags) = - this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) - - // Needed for MoveNextAsync to return a ValueTask, this manages the source of the ValueTask - // in combination with the ManualResetValueTaskSourceCore (in promiseOfValueOrEnd). - interface IValueTaskSource with - member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token - - /// Returning the boolean value that is used as a result for MoveNextAsync() - member this.GetResult token = - let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token - - // This ensures that, esp. in cases where there's no actual iteration (i.e. empty seq) - // we can still detect completeness and prevent an incorrect jump in the resumable code. - // See https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/54 - if not canMoveNext then - // Signal we reached the end. - // DO NOT call Data.builder.Complete() here, ONLY do that in the Run method. - this._machine.Data.completed <- true - - canMoveNext - - member this.OnCompleted(continuation, state, token, flags) = - this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) - - interface IAsyncStateMachine with - /// The MoveNext method is called by builder.MoveNext() in the resumable code - member this.MoveNext() = moveNextRef &this._machine - - /// SetStatemachine is (currently) never called - member _.SetStateMachine(_state) = () // not needed for reference type - - interface IAsyncEnumerable<'T> with - member this.GetAsyncEnumerator(ct) = - // if this is null, it means it's the first time for this Enumerable to create an Enumerator - // so, to prevent extra allocations, we just return 'self', with the iterator vars set appropriately. - match this._machine.Data :> obj with - | null when initialThreadId = Environment.CurrentManagedThreadId -> - this.InitMachineData(ct, &this._machine) - this // just return 'self' here - - | _ -> - Debug.logInfo "GetAsyncEnumerator, start cloning..." - - // We need to reset state, but only to the "initial machine", resetting the _machine to - // Unchecked.defaultof<_> is wrong, as the compiler uses this to track state. However, - // we do need a zeroed ResumptionPoint, otherwise we would continue after the last iteration - // returning an empty sequence. - // - // Solution: we shadow the initial machine, which we then re-assign here: - // - let clone = TaskSeq<'Machine, 'T>() // we used MemberwiseClone, TODO: test difference in perf, but this should be faster - - // _machine will change, _initialMachine will not, which can be used in a new clone. - // we still need to copy _initialMachine, as it has been initialized by the F# compiler in AfterCode<_, _>. - clone._machine <- this._initialMachine - clone._initialMachine <- this._initialMachine // TODO: proof with a test that this is necessary: probably not - clone.InitMachineData(ct, &clone._machine) - Debug.logInfo "GetAsyncEnumerator, finished cloning..." - clone - - interface System.Collections.Generic.IAsyncEnumerator<'T> with - member this.Current = - match this._machine.Data.current with - | ValueSome x -> x - | ValueNone -> - // Returning a default value is similar to how F#'s seq<'T> behaves - // According to the docs, behavior is Unspecified in case of a call - // to Current, which means that this is certainly fine, and arguably - // better than raising an exception. - Unchecked.defaultof<'T> - - member this.MoveNextAsync() = - Debug.logInfo "MoveNextAsync..." - - if this._machine.ResumptionPoint = -1 then // can't use as IAsyncEnumerator before IAsyncEnumerable - Debug.logInfo "at MoveNextAsync: Resumption point = -1" - - ValueTask.False - - elif this._machine.Data.completed then - Debug.logInfo "at MoveNextAsync: completed = true" - - // return False when beyond the last item - this._machine.Data.promiseOfValueOrEnd.Reset() - ValueTask.False - - else - Debug.logInfo "at MoveNextAsync: normal resumption scenario" - - let data = this._machine.Data - - // Honor the cancellation token passed to GetAsyncEnumerator (fixes #179). - // ThrowIfCancellationRequested() is a no-op for CancellationToken.None. - data.cancellationToken.ThrowIfCancellationRequested() - - data.promiseOfValueOrEnd.Reset() - let mutable ts = this - - Debug.logInfo "at MoveNextAsync: start calling builder.MoveNext()" - - data.builder.MoveNext(&ts) - - Debug.logInfo "at MoveNextAsync: finished calling builder.MoveNext()" - - this.MoveNextAsyncResult() - - /// Disposes of the IAsyncEnumerator (*not* the IAsyncEnumerable!!!) - member this.DisposeAsync() = - task { - Debug.logInfo "DisposeAsync..." - - match this._machine.Data.disposalStack with - | null -> () - | _ -> - let mutable exn = None - - for d in Seq.rev this._machine.Data.disposalStack do - try - do! d () - with e -> - if exn.IsNone then - exn <- Some e - - match exn with - | None -> () - | Some e -> raise e - } - |> ValueTask - - - override this.MoveNextAsyncResult() = - let data = this._machine.Data - let version = data.promiseOfValueOrEnd.Version - let status = data.promiseOfValueOrEnd.GetStatus(version) - - match status with - | ValueTaskSourceStatus.Succeeded -> - Debug.logInfo "at MoveNextAsyncResult: case succeeded..." - - let result = data.promiseOfValueOrEnd.GetResult(version) - - if not result then - // if beyond the end of the stream, ensure we unset - // the Current value - data.current <- ValueNone - - ValueTask.fromResult result - - | ValueTaskSourceStatus.Faulted - | ValueTaskSourceStatus.Canceled - | ValueTaskSourceStatus.Pending as state -> - Debug.logInfo ("at MoveNextAsyncResult: case ", state) - - ValueTask.ofSource this version - | _ -> - Debug.logInfo "at MoveNextAsyncResult: Unexpected state" - // assume it's a possibly new, not yet supported case, treat as default - ValueTask.ofSource this version - -and ResumableTSC<'T> = ResumableCode, unit> -and TaskSeqStateMachine<'T> = ResumableStateMachine> -and TaskSeqResumptionFunc<'T> = ResumptionFunc> -and TaskSeqResumptionDynamicInfo<'T> = ResumptionDynamicInfo> - -/// Implements the dynamic (FSI) path for ResumptionDynamicInfo, used by TaskSeqDynamic. -/// Handles the state-machine transitions that the compiler-generated MoveNextMethodImpl handles in the static path. -and [] TaskSeqDynamicInfo<'T>(initialResumptionFunc: TaskSeqResumptionFunc<'T>) = - inherit TaskSeqResumptionDynamicInfo<'T>(initialResumptionFunc) - - override this.MoveNext(sm: byref>) = - try - Debug.logInfo "at TaskSeqDynamicInfo.MoveNext start" - - let __stack_code_fin = this.ResumptionFunc.Invoke(&sm) - - if __stack_code_fin then - Debug.logInfo "at TaskSeqDynamicInfo.MoveNext, done" - - sm.Data.promiseOfValueOrEnd.SetResult(false) - sm.Data.builder.Complete() - sm.Data.completed <- true - - elif sm.Data.current.IsSome then - Debug.logInfo "at TaskSeqDynamicInfo.MoveNext, still more items" - - sm.Data.promiseOfValueOrEnd.SetResult(true) - - else - Debug.logInfo "at TaskSeqDynamicInfo.MoveNext, await" - - let boxed = sm.Data.boxedSelf - - sm.Data.awaiter.UnsafeOnCompleted(fun () -> - let mutable boxed = boxed - moveNextRef &boxed) - - with exn -> - Debug.logInfo ("Setting exception of PromiseOfValueOrEnd to: ", exn.Message) - sm.Data.promiseOfValueOrEnd.SetException(exn) - sm.Data.builder.Complete() - - override _.SetStateMachine(_machine: byref>, _state: IAsyncStateMachine) = () - -/// Dynamic (FSI) implementation of IAsyncEnumerable for taskSeq computation expressions. -/// Used when the F# compiler cannot emit static resumable code (e.g., in F# Interactive). -and [] TaskSeqDynamic<'T>() = - inherit TaskSeqBase<'T>() - - let initialThreadId = Environment.CurrentManagedThreadId - - [] - val mutable _machine: TaskSeqStateMachine<'T> - - [] - val mutable _initialResumptionFunc: TaskSeqResumptionFunc<'T> - - member this.InitDynamicMachineData(ct: CancellationToken) = - let data = TaskSeqStateMachineData() - data.boxedSelf <- this - data.cancellationToken <- ct - data.builder <- AsyncIteratorMethodBuilder.Create() - this._machine.Data <- data - this._machine.ResumptionDynamicInfo <- TaskSeqDynamicInfo(this._initialResumptionFunc) - - interface IValueTaskSource with - member this.GetResult token = - let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token - - if not canMoveNext then - this._machine.Data.completed <- true - - member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token - - member this.OnCompleted(continuation, state, token, flags) = - this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) - - interface IValueTaskSource with - member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token - - member this.GetResult token = - let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token - - if not canMoveNext then - this._machine.Data.completed <- true - - canMoveNext - - member this.OnCompleted(continuation, state, token, flags) = - this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) - - interface IAsyncStateMachine with - member this.MoveNext() = moveNextRef &this._machine - member _.SetStateMachine(_state) = () - - interface IAsyncEnumerable<'T> with - member this.GetAsyncEnumerator(ct) = - match this._machine.Data :> obj with - | null when initialThreadId = Environment.CurrentManagedThreadId -> - this.InitDynamicMachineData(ct) - this - | _ -> - Debug.logInfo "TaskSeqDynamic.GetAsyncEnumerator, cloning..." - let clone = TaskSeqDynamic<'T>() - clone._initialResumptionFunc <- this._initialResumptionFunc - clone.InitDynamicMachineData(ct) - clone - - interface IAsyncEnumerator<'T> with - member this.Current = - match this._machine.Data.current with - | ValueSome x -> x - | ValueNone -> Unchecked.defaultof<'T> - - member this.MoveNextAsync() = - Debug.logInfo "TaskSeqDynamic.MoveNextAsync..." - - if this._machine.ResumptionPoint = -1 then - Debug.logInfo "at TaskSeqDynamic.MoveNextAsync: Resumption point = -1" - ValueTask.False - - elif this._machine.Data.completed then - Debug.logInfo "at TaskSeqDynamic.MoveNextAsync: completed = true" - this._machine.Data.promiseOfValueOrEnd.Reset() - ValueTask.False - - else - Debug.logInfo "at TaskSeqDynamic.MoveNextAsync: normal resumption" - let data = this._machine.Data - data.cancellationToken.ThrowIfCancellationRequested() - data.promiseOfValueOrEnd.Reset() - let mutable ts = this - data.builder.MoveNext(&ts) - this.MoveNextAsyncResult() - - member this.DisposeAsync() = - task { - match this._machine.Data.disposalStack with - | null -> () - | _ -> - let mutable exn = None - - for d in Seq.rev this._machine.Data.disposalStack do - try - do! d () - with e -> - if exn.IsNone then - exn <- Some e - - match exn with - | None -> () - | Some e -> raise e - } - |> ValueTask - - override this.MoveNextAsyncResult() = - let data = this._machine.Data - let version = data.promiseOfValueOrEnd.Version - let status = data.promiseOfValueOrEnd.GetStatus(version) - - match status with - | ValueTaskSourceStatus.Succeeded -> - Debug.logInfo "at TaskSeqDynamic MoveNextAsyncResult: case succeeded..." - - let result = data.promiseOfValueOrEnd.GetResult(version) - - if not result then - data.current <- ValueNone - - ValueTask.fromResult result - - | ValueTaskSourceStatus.Faulted - | ValueTaskSourceStatus.Canceled - | ValueTaskSourceStatus.Pending as state -> - Debug.logInfo ("at TaskSeqDynamic MoveNextAsyncResult: case ", state) - - ValueTask.ofSource this version - | _ -> - Debug.logInfo "at TaskSeqDynamic MoveNextAsyncResult: Unexpected state" - ValueTask.ofSource this version - -type TaskSeqBuilder() = - - member inline _.Delay(f: unit -> ResumableTSC<'T>) = ResumableTSC<'T>(fun sm -> f().Invoke(&sm)) - - member inline _.Run(code: ResumableTSC<'T>) : IAsyncEnumerable<'T> = - if __useResumableCode then - // This is the static implementation. A new struct type is created. - __stateMachine, IAsyncEnumerable<'T>> - // IAsyncStateMachine.MoveNext - (MoveNextMethodImpl<_>(fun sm -> - //-- RESUMABLE CODE START - __resumeAt sm.ResumptionPoint - - try - Debug.logInfo "at Run.MoveNext start" - - let __stack_code_fin = code.Invoke(&sm) - - if __stack_code_fin then - Debug.logInfo $"at Run.MoveNext, done" - - // Signal we're at the end - // NOTE: if we don't do it here, as well as in IValueTaskSource.GetResult - // we either end up in an endless loop, or we'll get NRE on empty sequences. - // see: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/54 - sm.Data.promiseOfValueOrEnd.SetResult(false) - sm.Data.builder.Complete() - sm.Data.completed <- true - - elif sm.Data.current.IsSome then - Debug.logInfo $"at Run.MoveNext, still more items in enumerator" - - // Signal there's more data: - sm.Data.promiseOfValueOrEnd.SetResult(true) - - else - // Goto request - Debug.logInfo $"at Run.MoveNext, await, MoveNextAsync has not completed yet" - - // don't capture the full object in the next closure (won't work because: byref) - // but only a reference to itself. - let boxed = sm.Data.boxedSelf - - sm.Data.awaiter.UnsafeOnCompleted(fun () -> - let mutable boxed = boxed - moveNextRef &boxed) - - with exn -> - Debug.logInfo ("Setting exception of PromiseOfValueOrEnd to: ", exn.Message) - sm.Data.promiseOfValueOrEnd.SetException(exn) - sm.Data.builder.Complete() - - //-- RESUMABLE CODE END - )) - (SetStateMachineMethodImpl<_>(fun sm state -> ())) // not used in reference impl - (AfterCode<_, _>(fun sm -> - Debug.logInfo "at AfterCode<_, _>, after F# inits the sm, and we can attach extra info" - - let ts = TaskSeq, 'T>() - ts._initialMachine <- sm - ts._machine <- sm - ts :> IAsyncEnumerable<'T>)) - else - // Dynamic path, used when __useResumableCode = false (e.g., in F# Interactive / FSI). - // Uses TaskSeqDynamic which drives the resumable code via ResumptionDynamicInfo. - let ts = TaskSeqDynamic<'T>() - ts._initialResumptionFunc <- TaskSeqResumptionFunc<'T>(fun sm -> code.Invoke(&sm)) - ts :> IAsyncEnumerable<'T> - - - member inline _.Zero() : ResumableTSC<'T> = - Debug.logInfo "at Zero()" - ResumableCode.Zero() - - member inline _.Combine(task1: ResumableTSC<'T>, task2: ResumableTSC<'T>) = - Debug.logInfo "at Combine(.., ..)" - - ResumableCode.Combine(task1, task2) - - /// Used by `For`. Unclear if the new `while!` (from F# 8.0) hits this - member inline _.WhileAsync([] condition: unit -> ValueTask, body: ResumableTSC<'T>) : ResumableTSC<'T> = - let mutable condition_res = true - - ResumableCode.While( - (fun () -> condition_res), - ResumableTSC<'T>(fun sm -> - let mutable __stack_condition_fin = true - let __stack_vtask = condition () - - if __stack_vtask.IsCompleted then - Debug.logInfo "at WhileAsync: returning completed task" - - __stack_condition_fin <- true - condition_res <- __stack_vtask.Result - else - Debug.logInfo "at WhileAsync: awaiting non-completed task" - - let task = __stack_vtask.AsTask() - let mutable awaiter = task.GetAwaiter() - - // This will yield with __stack_fin = false - // This will resume with __stack_fin = true - - // NOTE (AB): if this extra let-binding isn't here, we get NRE exceptions, infinite loops (end of seq not signaled) and warning FS3513 - let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm) - __stack_condition_fin <- __stack_yield_fin - - if __stack_condition_fin then - condition_res <- task.Result - else - sm.Data.awaiter <- awaiter - sm.Data.current <- ValueNone - - if __stack_condition_fin then - if condition_res then body.Invoke(&sm) else true - else - false) - ) - - member inline _.While([] condition: unit -> bool, body: ResumableTSC<'T>) = - Debug.logInfo "at While(...)" - ResumableCode.While(condition, body) - - member inline _.TryWith(body: ResumableTSC<'T>, catch: exn -> ResumableTSC<'T>) = ResumableCode.TryWith(body, catch) - - member inline _.TryFinallyAsync(body: ResumableTSC<'T>, compensationAction: unit -> Task) = - ResumableCode.TryFinallyAsync( - - ResumableTSC<'T>(fun sm -> - sm.Data.PushDispose compensationAction - body.Invoke(&sm)), - - ResumableTSC<'T>(fun sm -> - - sm.Data.PopDispose() - let mutable __stack_condition_fin = true - let __stack_vtask = compensationAction () - - if not __stack_vtask.IsCompleted then - let mutable awaiter = __stack_vtask.GetAwaiter() - let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm) - __stack_condition_fin <- __stack_yield_fin - - if not __stack_condition_fin then - sm.Data.awaiter <- awaiter - - __stack_condition_fin) - ) - - member inline _.TryFinally(body: ResumableTSC<'T>, compensationAction: unit -> unit) = - ResumableCode.TryFinally( - ResumableTSC<'T>(fun sm -> - sm.Data.PushDispose(compensationAction >> Task.get_CompletedTask) - body.Invoke(&sm)), - - ResumableTSC<'T>(fun sm -> - sm.Data.PopDispose() - compensationAction () - true) - ) - - member inline this.Using(disp: #IAsyncDisposable, body: #IAsyncDisposable -> ResumableTSC<'T>) = - - // A using statement is just a try/finally with the finally block disposing if non-null. - this.TryFinallyAsync( - (fun sm -> (body disp).Invoke(&sm)), - (fun () -> - if not (isNull (box disp)) then - disp.DisposeAsync().AsTask() - else - Task.CompletedTask) - ) - - member inline _.Yield(value: 'T) : ResumableTSC<'T> = - ResumableTSC<'T>(fun sm -> - // This will yield with __stack_fin = false - // This will resume with __stack_fin = true - Debug.logInfo "at Yield" - - let __stack_fin = ResumableCode.Yield().Invoke(&sm) - sm.Data.current <- ValueSome value - sm.Data.awaiter <- null - __stack_fin) - -// -// These "modules of priority" allow for an indecisive F# to resolve -// the proper overload if a single type implements more than one -// interface. For instance, a type implementing 'IDisposable' and -// 'IAsyncDisposable'. -// -// See for more info tasks.fs in F# Core. -// -// This section also includes the dependencies of such overloads -// (like For depending on Using etc). -// - -[] -module LowPriority = - type TaskSeqBuilder with - - // - // Note: we cannot place _.Bind directly on the type, as the NoEagerXXX attribute - // has no effect, and each use of `do!` will give an overload error (because the - // `TaskLike` type and the `Task<_>` type are partially interchangeable, see notes there). - // - // However, we cannot unify these two methods, because Task<_> inherits from Task (non-generic) - // and we need a way to distinguish these two methods. - // - // Types handled: - // - (non-generic) ValueTask (because it implements GetResult() -> unit) - // - ValueTask<'T> (because it implements GetResult() -> 'TResult) - // - (non-generic) Task (because it implements GetResult() -> unit) - // - any other type that implements GetAwaiter() - // - // Not handled: - // - Task<'T> (because it only implements GetResult() -> unit, not GetResult() -> 'TResult) - - [] - member inline _.Bind< ^TaskLike, 'T, 'U, ^Awaiter - when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) - and ^Awaiter :> ICriticalNotifyCompletion - and ^Awaiter: (member get_IsCompleted: unit -> bool) - and ^Awaiter: (member GetResult: unit -> 'T)> - (task: ^TaskLike, continuation: ('T -> ResumableTSC<'U>)) - = - - ResumableTSC<'U>(fun sm -> - let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task)) - let mutable __stack_fin = true - - Debug.logInfo "at TaskLike bind" - - if not (^Awaiter: (member get_IsCompleted: unit -> bool) awaiter) then - // This will yield with __stack_fin2 = false - // This will resume with __stack_fin2 = true - let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) - __stack_fin <- __stack_fin2 - - Debug.logInfo ("at TaskLike bind: this.completed = ", sm.Data.completed) - - if __stack_fin then - Debug.logInfo "at TaskLike bind!: finished awaiting, calling continuation" - let result = (^Awaiter: (member GetResult: unit -> 'T) awaiter) - (continuation result).Invoke(&sm) - - else - Debug.logInfo "at TaskLike bind: await further" - - sm.Data.awaiter <- awaiter - sm.Data.current <- ValueNone - false) - - -[] -module MediumPriority = - type TaskSeqBuilder with - - member inline this.Using(dispensation: #IDisposable, body: #IDisposable -> ResumableTSC<'T>) = - - // A using statement is just a try/finally with the finally block disposing if non-null. - this.TryFinally( - (fun sm -> (body dispensation).Invoke(&sm)), - (fun () -> - // yes, this can be null from time to time - if not (isNull (box dispensation)) then - dispensation.Dispose()) - ) - - member inline this.For(sequence: seq<'TElement>, body: 'TElement -> ResumableTSC<'T>) = - // A for loop is just a using statement on the sequence's enumerator... - this.Using( - sequence.GetEnumerator(), - // ... and its body is a while loop that advances the enumerator and runs the body on each element. - fun e -> this.While(e.MoveNext, (fun sm -> (body e.Current).Invoke(&sm))) - ) - - member inline this.YieldFrom(source: seq<'T>) : ResumableTSC<'T> = this.For(source, this.Yield) - - member inline this.For(source: #IAsyncEnumerable<'TElement>, body: 'TElement -> ResumableTSC<'T>) = - ResumableTSC<'T>(fun sm -> - this - .Using( - source.GetAsyncEnumerator(sm.Data.cancellationToken), - fun e -> this.WhileAsync(e.MoveNextAsync, (fun sm -> (body e.Current).Invoke(&sm))) - ) - .Invoke(&sm)) - - member inline this.YieldFrom(source: IAsyncEnumerable<'T>) = this.For(source, (fun v -> this.Yield(v))) - -[] -module HighPriority = - type TaskSeqBuilder with - - // - // Notes Task: - // - Task<_> implements GetAwaiter(), but TaskAwaiter does not implement GetResult() -> TResult - // - Instead, it has GetResult() -> unit, which is not '^TaskLike' - // - Conclusion: we need an extra high-prio overload to allow support for Task<_> - // - // Notes ValueTask: - // - In contrast, ValueTask<_> *does have* GetResult() -> 'TResult - // - Conclusion: we do not need an extra overload anymore for ValueTask - // - member inline _.Bind(task: Task<'T>, continuation: ('T -> ResumableTSC<'U>)) = - ResumableTSC<'U>(fun sm -> - let mutable awaiter = task.GetAwaiter() - let mutable __stack_fin = true - - Debug.logInfo "at Bind" - - if not awaiter.IsCompleted then - // This will yield with __stack_fin2 = false - // This will resume with __stack_fin2 = true - let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) - __stack_fin <- __stack_fin2 - - Debug.logInfo ("at Bind: with __stack_fin = ", __stack_fin) - Debug.logInfo ("at Bind: this.completed = ", sm.Data.completed) - - if __stack_fin then - Debug.logInfo "at Bind: finished awaiting, calling continuation" - let result = awaiter.GetResult() - (continuation result).Invoke(&sm) - - else - Debug.logInfo "at Bind: await further" - - sm.Data.awaiter <- awaiter - sm.Data.current <- ValueNone - false) - - member inline _.Bind(computation: Async<'T>, continuation: ('T -> ResumableTSC<'U>)) = - ResumableTSC<'U>(fun sm -> - let mutable awaiter = Async.StartImmediateAsTask(computation, cancellationToken = sm.Data.cancellationToken).GetAwaiter() - - let mutable __stack_fin = true - - Debug.logInfo "at Bind" - - if not awaiter.IsCompleted then - // This will yield with __stack_fin2 = false - // This will resume with __stack_fin2 = true - let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) - __stack_fin <- __stack_fin2 - - Debug.logInfo ("at Bind: with __stack_fin = ", __stack_fin) - Debug.logInfo ("at Bind: this.completed = ", sm.Data.completed) - - if __stack_fin then - Debug.logInfo "at Bind: finished awaiting, calling continuation" - let result = awaiter.GetResult() - (continuation result).Invoke(&sm) - - else - Debug.logInfo "at Bind: await further" - - sm.Data.awaiter <- awaiter - sm.Data.current <- ValueNone - false) - -[] -module TaskSeqBuilder = - /// Builds an asynchronous task sequence based on IAsyncEnumerable<'T> using computation expression syntax. - let taskSeq = TaskSeqBuilder() - -/// Builder for computation expressions. Inherits all members from -/// , using the dynamic (ResumptionDynamicInfo-based) path when the -/// F# compiler cannot emit static resumable code (e.g., in F# Interactive). -type TaskSeqDynamicBuilder() = - inherit TaskSeqBuilder() - -[] -module TaskSeqDynamicBuilder = - /// Builds an asynchronous task sequence, with a dynamic resumable code fallback for scenarios - /// where the F# compiler cannot generate static resumable code (e.g., in F# Interactive / FSI). - let taskSeqDynamic = TaskSeqDynamicBuilder() diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi deleted file mode 100644 index 9caa7091..00000000 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi +++ /dev/null @@ -1,266 +0,0 @@ -namespace FSharp.Control - -open System -open System.Threading -open System.Threading.Tasks -open System.Threading.Tasks.Sources -open System.Runtime.CompilerServices -open System.Collections.Generic - -open FSharp.Core.CompilerServices - -[] -module Internal = - - /// - /// Setting from environment variable , which, - /// when set, enables (very) verbose printing of flow and state - /// - val initVerbose: unit -> bool - - /// Call MoveNext on an IAsyncStateMachine by reference - val inline moveNextRef: x: byref<#IAsyncStateMachine> -> unit - - /// F# requires that we implement interfaces even on an abstract class. - val inline raiseNotImpl: unit -> 'a - -/// -/// Represents a task sequence and is the output of using the -/// computation expression from this library. It is an alias for . -/// -/// The type is deprecated since version 0.4.0, -/// please use in its stead. See . -/// -[' is deprecated in favor of 'TaskSeq<_>'. It will be removed in an upcoming release.">] -type taskSeq<'T> = IAsyncEnumerable<'T> - -/// -/// Represents a task sequence and is the output of using the -/// computation expression from this library. It is an alias for . -/// -type TaskSeq<'T> = IAsyncEnumerable<'T> - -/// TaskSeqCode type alias of ResumableCode delegate type, specially recognized by the F# compiler -and ResumableTSC<'T> = ResumableCode, unit> - -/// -/// Contains the state data for the computation expression builder. -/// For use in this library only. Required by the method. -/// -and TaskSeqStateMachine<'T> = ResumableStateMachine> -and TaskSeqResumptionFunc<'T> = ResumptionFunc> -and TaskSeqResumptionDynamicInfo<'T> = ResumptionDynamicInfo> - -/// -/// Contains the state data for the computation expression builder. -/// For use in this library only. Required by the method. -/// -and [] TaskSeqStateMachineData<'T> = - - new: unit -> TaskSeqStateMachineData<'T> - - [] - val mutable cancellationToken: CancellationToken - - /// Keeps track of the objects that need to be disposed off on IAsyncDispose. - [] - val mutable disposalStack: ResizeArray<(unit -> Task)> - - [] - val mutable awaiter: ICriticalNotifyCompletion - - [] - val mutable promiseOfValueOrEnd: ManualResetValueTaskSourceCore - - /// Helper struct providing methods for awaiting 'next' in async iteration scenarios. - [] - val mutable builder: AsyncIteratorMethodBuilder - - /// Whether or not a full iteration through the IAsyncEnumerator has completed - [] - val mutable completed: bool - - /// Used by the AsyncEnumerator interface to return the Current value when - /// IAsyncEnumerator.Current is called - [] - val mutable current: ValueOption<'T> - - /// A reference to 'self', because otherwise we can't use byref in the resumable code. - [] - val mutable boxedSelf: TaskSeqBase<'T> - - member PopDispose: unit -> unit - - member PushDispose: disposer: (unit -> Task) -> unit - -/// -/// Abstract base class for . -/// For use by this library only, should not be used directly in user code. Its operation depends highly on resumable state. -/// -and [] TaskSeqBase<'T> = - interface IValueTaskSource - interface IValueTaskSource - interface IAsyncStateMachine - interface IAsyncEnumerable<'T> - interface IAsyncEnumerator<'T> - - new: unit -> TaskSeqBase<'T> - - abstract MoveNextAsyncResult: unit -> ValueTask - -/// -/// Main implementation of generic and related interfaces, -/// which forms the meat of the logic behind computation expresssions. -/// For use by this library only, should not be used directly in user code. Its operation depends highly on resumable state. -/// -and [] TaskSeq<'Machine, 'T - when 'Machine :> IAsyncStateMachine and 'Machine :> IResumableStateMachine>> = - inherit TaskSeqBase<'T> - interface IAsyncEnumerator<'T> - interface IAsyncEnumerable<'T> - interface IAsyncStateMachine - interface IValueTaskSource - interface IValueTaskSource - - new: unit -> TaskSeq<'Machine, 'T> - - [] - val mutable _initialMachine: 'Machine - - /// Keeps the active state machine. - [] - val mutable _machine: 'Machine - - //new: unit -> TaskSeq<'Machine, 'T> - member InitMachineData: ct: CancellationToken * machine: byref<'Machine> -> unit - override MoveNextAsyncResult: unit -> ValueTask - -/// -/// Concrete implementation of for taskSeq computation -/// expressions, used in the dynamic (FSI) path. Handles state-machine transitions when the F# compiler -/// cannot generate static resumable code. -/// For use by this library only. -/// -and [] TaskSeqDynamicInfo<'T> = - inherit TaskSeqResumptionDynamicInfo<'T> - new: initialResumptionFunc: TaskSeqResumptionFunc<'T> -> TaskSeqDynamicInfo<'T> - -/// -/// Dynamic (FSI-compatible) implementation of for taskSeq -/// computation expressions. Used when the F# compiler cannot generate static resumable code (e.g., in FSI). -/// For use by this library only. -/// -and [] TaskSeqDynamic<'T> = - inherit TaskSeqBase<'T> - interface IAsyncEnumerator<'T> - interface IAsyncEnumerable<'T> - interface IAsyncStateMachine - interface IValueTaskSource - interface IValueTaskSource - - new: unit -> TaskSeqDynamic<'T> - - [] - val mutable _machine: TaskSeqStateMachine<'T> - - [] - val mutable _initialResumptionFunc: TaskSeqResumptionFunc<'T> - - member InitDynamicMachineData: ct: CancellationToken -> unit - override MoveNextAsyncResult: unit -> ValueTask - -/// -/// Main builder class for the computation expression. -/// -[] -type TaskSeqBuilder = - - member inline Combine: task1: ResumableTSC<'T> * task2: ResumableTSC<'T> -> ResumableTSC<'T> - member inline Delay: f: (unit -> ResumableTSC<'T>) -> ResumableTSC<'T> - member inline Run: code: ResumableTSC<'T> -> TaskSeq<'T> - member inline TryFinally: body: ResumableTSC<'T> * compensationAction: (unit -> unit) -> ResumableTSC<'T> - member inline TryFinallyAsync: body: ResumableTSC<'T> * compensationAction: (unit -> Task) -> ResumableTSC<'T> - member inline TryWith: body: ResumableTSC<'T> * catch: (exn -> ResumableTSC<'T>) -> ResumableTSC<'T> - - member inline Using: - disp: 'Disp * body: ('Disp -> ResumableTSC<'T>) -> ResumableTSC<'T> when 'Disp :> IAsyncDisposable - - member inline While: condition: (unit -> bool) * body: ResumableTSC<'T> -> ResumableTSC<'T> - /// Used by `For`. F# currently doesn't support `while!`, so this cannot be called directly from the CE - member inline WhileAsync: condition: (unit -> ValueTask) * body: ResumableTSC<'T> -> ResumableTSC<'T> - member inline Yield: value: 'T -> ResumableTSC<'T> - member inline Zero: unit -> ResumableTSC<'T> - -[] -module TaskSeqBuilder = - - /// - /// Builds an asynchronous task sequence based on using computation expression syntax. - /// - val taskSeq: TaskSeqBuilder - -/// -/// Contains low priority extension methods for the main builder class for the computation expression. -/// The , and modules are not meant to be -/// accessed directly from user code. They solely serve to disambiguate overload resolution inside the computation expression. -/// -[] -module LowPriority = - type TaskSeqBuilder with - - [] - member inline Bind< ^TaskLike, 'T, 'U, ^Awaiter> : - task: ^TaskLike * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> - when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) - and ^Awaiter :> ICriticalNotifyCompletion - and ^Awaiter: (member get_IsCompleted: unit -> bool) - and ^Awaiter: (member GetResult: unit -> 'T) - -/// -/// Contains low priority extension methods for the main builder class for the computation expression. -/// The , and modules are not meant to be -/// accessed directly from user code. They solely serve to disambiguate overload resolution inside the computation expression. -/// -[] -module MediumPriority = - type TaskSeqBuilder with - - // NOTE: syntax with '#Disposable' won't work properly in FSI - member inline Using: - dispensation: 'Disp * body: ('Disp -> ResumableTSC<'T>) -> ResumableTSC<'T> when 'Disp :> IDisposable - - member inline For: sequence: seq<'TElement> * body: ('TElement -> ResumableTSC<'T>) -> ResumableTSC<'T> - member inline YieldFrom: source: seq<'T> -> ResumableTSC<'T> - member inline For: source: #TaskSeq<'TElement> * body: ('TElement -> ResumableTSC<'T>) -> ResumableTSC<'T> - member inline YieldFrom: source: TaskSeq<'T> -> ResumableTSC<'T> - -/// -/// Contains low priority extension methods for the main builder class for the computation expression. -/// The , and modules are not meant to be -/// accessed directly from user code. They solely serve to disambiguate overload resolution inside the computation expression. -/// -[] -module HighPriority = - type TaskSeqBuilder with - - member inline Bind: task: Task<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> - member inline Bind: computation: Async<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> - -/// -/// Builder class for the computation expression. Inherits all members -/// from , using the dynamic resumable code path as fallback when the -/// F# compiler cannot generate static resumable code (e.g., in F# Interactive / FSI). -/// -[] -type TaskSeqDynamicBuilder = - inherit TaskSeqBuilder - new: unit -> TaskSeqDynamicBuilder - -[] -module TaskSeqDynamicBuilder = - - /// - /// Builds an asynchronous task sequence, with a dynamic resumable code fallback for scenarios - /// where the F# compiler cannot generate static resumable code (e.g., in F# Interactive / FSI). - /// - val taskSeqDynamic: TaskSeqDynamicBuilder diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs deleted file mode 100644 index 60821af8..00000000 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ /dev/null @@ -1,1832 +0,0 @@ -namespace FSharp.Control - -open System -open System.Collections.Generic -open System.Threading -open System.Threading.Tasks - -[] -type internal AsyncEnumStatus = - | BeforeAll - | WithCurrent - | AfterAll - -[] -type internal TakeOrSkipKind = - /// use the Seq.take semantics, raises exception if not enough elements - | Take - /// use the Seq.skip semantics, raises exception if not enough elements - | Skip - /// use the Seq.truncate semantics, safe operation, returns all if count exceeds the seq - | Truncate - /// no Seq equiv, but like Stream.drop in Scala: safe operation, return empty if not enough elements - | Drop - -[] -type internal Action<'T, 'U, 'TaskU when 'TaskU :> Task<'U>> = - | CountableAction of countable_action: (int -> 'T -> 'U) - | SimpleAction of simple_action: ('T -> 'U) - | AsyncCountableAction of async_countable_action: (int -> 'T -> 'TaskU) - | AsyncSimpleAction of async_simple_action: ('T -> 'TaskU) - -[] -type internal FolderAction<'T, 'State, 'TaskState when 'TaskState :> Task<'State>> = - | FolderAction of state_action: ('State -> 'T -> 'State) - | AsyncFolderAction of async_state_action: ('State -> 'T -> 'TaskState) - -[] -type internal ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U option>> = - | TryPick of try_pick: ('T -> 'U option) - | TryPickAsync of async_try_pick: ('T -> 'TaskOption) - -[] -type internal ChooserVAction<'T, 'U, 'TaskValueOption when 'TaskValueOption :> Task<'U voption>> = - | TryPickV of try_pickv: ('T -> 'U voption) - | TryPickVAsync of async_try_pickv: ('T -> 'TaskValueOption) - -[] -type internal PredicateAction<'T, 'TaskBool when 'TaskBool :> Task> = - | Predicate of try_filter: ('T -> bool) - | PredicateAsync of async_try_filter: ('T -> 'TaskBool) - -[] -type internal InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> = - | InitAction of init_item: (int -> 'T) - | InitActionAsync of async_init_item: (int -> 'TaskT) - -[] -type internal ProjectorAction<'T, 'Key, 'TaskKey when 'TaskKey :> Task<'Key>> = - | ProjectorAction of projector: ('T -> 'Key) - | AsyncProjectorAction of async_projector: ('T -> 'TaskKey) - -[] -type internal MapFolderAction<'T, 'State, 'Result, 'TaskResultState when 'TaskResultState :> Task<'Result * 'State>> = - | MapFolderAction of map_folder_action: ('State -> 'T -> 'Result * 'State) - | AsyncMapFolderAction of async_map_folder_action: ('State -> 'T -> 'TaskResultState) - -[] -type internal ManyOrOne<'T> = - | Many of source_seq: TaskSeq<'T> - | One of source_item: 'T - -module internal TaskSeqInternal = - /// Raise an NRE for arguments that are null. Only used for 'source' parameters, never for function parameters. - let inline checkNonNull argName arg = - if isNull arg then - nullArg argName - - let inline raiseEmptySeq () = invalidArg "source" "The input task sequence was empty." - - /// Moves the enumerator to its first element, assuming it has just been allocated. - /// Raises "The input sequence was empty" if there was no first element. - let inline moveFirstOrRaiseUnsafe (e: IAsyncEnumerator<_>) = task { - let! hasFirst = e.MoveNextAsync() - - if not hasFirst then - invalidArg "source" "The input task sequence was empty." - } - - /// Tests the given integer value and raises if it is -1 or lower. - let inline raiseCannotBeNegative name value = - if value >= 0 then - () - else - invalidArg name $"The value must be non-negative, but was {value}." - - let inline raiseOutOfBounds name = - invalidArg name "The value or index must be within the bounds of the task sequence." - - let inline raiseInsufficient () = - // this is correct, it is NOT an InvalidOperationException (see Seq.fs in F# Core) - // but instead, it's an ArgumentException... FWIW lol - invalidArg "source" "The input task sequence was has an insufficient number of elements." - - let inline raiseNotFound () = - KeyNotFoundException("The predicate function or index did not satisfy any item in the task sequence.") - |> raise - - let isEmpty (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let! step = e.MoveNextAsync() - return not step - } - - let empty<'T> = - { new IAsyncEnumerable<'T> with - member _.GetAsyncEnumerator _ = - { new IAsyncEnumerator<'T> with - member _.MoveNextAsync() = ValueTask.False - member _.Current = Unchecked.defaultof<'T> - member _.DisposeAsync() = ValueTask.CompletedTask - } - } - - let singleton (value: 'T) = - { new IAsyncEnumerable<'T> with - member _.GetAsyncEnumerator _ = - let mutable status = BeforeAll - - { new IAsyncEnumerator<'T> with - member _.MoveNextAsync() = - match status with - | BeforeAll -> - status <- WithCurrent - ValueTask.True - | WithCurrent -> - status <- AfterAll - ValueTask.False - | AfterAll -> ValueTask.False - - member _.Current: 'T = - match status with - | WithCurrent -> value - | _ -> Unchecked.defaultof<'T> - - member _.DisposeAsync() = ValueTask.CompletedTask - } - } - - let replicate count value = - raiseCannotBeNegative (nameof count) count - - taskSeq { - for _ in 1..count do - yield value - } - - let replicateInfinite value = taskSeq { - while true do - yield value - } - - let replicateInfiniteAsync (computation: unit -> #Task<'T>) = taskSeq { - while true do - let! value = computation () - yield value - } - - let replicateUntilNoneAsync (computation: unit -> #Task<'T option>) = taskSeq { - let mutable go = true - - while go do - let! result = computation () - - match result with - | Some value -> yield value - | None -> go <- false - } - - /// Returns length unconditionally, or based on a predicate - let lengthBy predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable i = 0 - - match predicate with - | None -> - while! e.MoveNextAsync() do - i <- i + 1 - - | Some(Predicate predicate) -> - while! e.MoveNextAsync() do - if predicate e.Current then - i <- i + 1 - - | Some(PredicateAsync predicate) -> - while! e.MoveNextAsync() do - match! predicate e.Current with - | true -> i <- i + 1 - | false -> () - - return i - } - - /// Returns length unconditionally, or based on a predicate - let lengthBeforeMax max (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable i = 0 - let mutable go = true - - while go && i < max do - let! hasMore = e.MoveNextAsync() - - if hasMore then i <- i + 1 else go <- false - - return i - } - - let inline maxMin ([] maxOrMin) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - do! moveFirstOrRaiseUnsafe e - - let mutable acc = e.Current - - while! e.MoveNextAsync() do - acc <- maxOrMin e.Current acc - - return acc - } - - let inline tryMaxMin ([] maxOrMin) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if not hasFirst then - return None - else - let mutable acc = e.Current - - while! e.MoveNextAsync() do - acc <- maxOrMin e.Current acc - - return Some acc - } - - // 'compare' is either `<` or `>` (i.e, less-than, greater-than resp.) - let inline maxMinBy ([] compare) ([] projection) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - do! moveFirstOrRaiseUnsafe e - - let value = e.Current - let mutable accProjection = projection value - let mutable accValue = value - - while! e.MoveNextAsync() do - let value = e.Current - let currentProjection = projection value - - if compare accProjection currentProjection then - accProjection <- currentProjection - accValue <- value - - return accValue - } - - // 'compare' is either `<` or `>` (i.e, less-than, greater-than resp.) - let inline maxMinByAsync ([] compare) ([] projectionAsync) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - do! moveFirstOrRaiseUnsafe e - - let value = e.Current - let! projValue = projectionAsync value - let mutable accProjection = projValue - let mutable accValue = value - - while! e.MoveNextAsync() do - let value = e.Current - let! currentProjection = projectionAsync value - - if compare accProjection currentProjection then - accProjection <- currentProjection - accValue <- value - - return accValue - } - - let tryExactlyOne (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - match! e.MoveNextAsync() with - | true -> - // grab first item and test if there's a second item - let current = e.Current - - match! e.MoveNextAsync() with - | true -> return None // 2 or more items - | false -> return Some current // exactly one - - | false -> - // zero items - return None - } - - - let init count initializer = taskSeq { - let mutable i = 0 - - let count = - match count with - | Some c -> - raiseCannotBeNegative (nameof count) c - c - - | None -> Int32.MaxValue - - match initializer with - | InitAction init -> - while i < count do - yield init i - i <- i + 1 - - | InitActionAsync asyncInit -> - while i < count do - let! result = asyncInit i - yield result - i <- i + 1 - - } - - let unfold generator state = taskSeq { - let mutable go = true - let mutable currentState = state - - while go do - match generator currentState with - | None -> go <- false - | Some(value, nextState) -> - yield value - currentState <- nextState - } - - let unfoldAsync generator state = taskSeq { - let mutable go = true - let mutable currentState = state - - while go do - let! result = (generator currentState: Task<_>) - - match result with - | None -> go <- false - | Some(value, nextState) -> - yield value - currentState <- nextState - } - - let iter action (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - // Each branch keeps its own while! loop so the match dispatch is hoisted out and - // the JIT sees a tight, single-case loop (same pattern as sum/sumBy etc.). - match action with - | CountableAction action -> - let mutable i = 0 - - while! e.MoveNextAsync() do - action i e.Current - i <- i + 1 - - | SimpleAction action -> - while! e.MoveNextAsync() do - action e.Current - - | AsyncCountableAction action -> - let mutable i = 0 - - while! e.MoveNextAsync() do - do! action i e.Current - i <- i + 1 - - | AsyncSimpleAction action -> - while! e.MoveNextAsync() do - do! action e.Current - } - - let fold folder initial (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable result = initial - - match folder with - | FolderAction folder -> - while! e.MoveNextAsync() do - result <- folder result e.Current - - | AsyncFolderAction folder -> - while! e.MoveNextAsync() do - let! tempResult = folder result e.Current - result <- tempResult - - return result - } - - let foldWhile predicate folder initial (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable result = initial - let mutable running = true - - while running do - let! hasNext = e.MoveNextAsync() - - if hasNext then - if predicate result e.Current then - result <- folder result e.Current - else - running <- false - else - running <- false - - return result - } - - let foldWhileAsync predicate folder initial (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable result = initial - let mutable running = true - - while running do - let! hasNext = e.MoveNextAsync() - - if hasNext then - let! keepGoing = predicate result e.Current - - if keepGoing then - let! newState = folder result e.Current - result <- newState - else - running <- false - else - running <- false - - return result - } - - let scan folder initial (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - match folder with - | FolderAction folder -> taskSeq { - let mutable state = initial - yield state - - for item in source do - state <- folder state item - yield state - } - - | AsyncFolderAction folder -> taskSeq { - let mutable state = initial - yield state - - for item in source do - let! newState = folder state item - state <- newState - yield state - } - - let reduce folder (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if not hasFirst then - raiseEmptySeq () - - let mutable result = e.Current - - match folder with - | FolderAction folder -> - while! e.MoveNextAsync() do - result <- folder result e.Current - - | AsyncFolderAction folder -> - while! e.MoveNextAsync() do - let! tempResult = folder result e.Current - result <- tempResult - - return result - } - - let mapFold (folder: MapFolderAction<_, _, _, _>) initial (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable state = initial - let results = ResizeArray() - - match folder with - | MapFolderAction folder -> - while! e.MoveNextAsync() do - let result, newState = folder state e.Current - results.Add result - state <- newState - - | AsyncMapFolderAction folder -> - while! e.MoveNextAsync() do - let! (result, newState) = folder state e.Current - results.Add result - state <- newState - - return results.ToArray(), state - } - - let threadState (folder: 'State -> 'T -> 'U * 'State) initial (source: TaskSeq<'T>) : TaskSeq<'U> = - checkNonNull (nameof source) source - - taskSeq { - let mutable state = initial - - for item in source do - let result, newState = folder state item - state <- newState - yield result - } - - let threadStateAsync (folder: 'State -> 'T -> #Task<'U * 'State>) initial (source: TaskSeq<'T>) : TaskSeq<'U> = - checkNonNull (nameof source) source - - taskSeq { - let mutable state = initial - - for item in source do - let! (result, newState) = folder state item - state <- newState - yield result - } - - let toResizeArrayAsync (source: TaskSeq<'T>) = - checkNonNull (nameof source) source - - task { - let res = ResizeArray<'T>() - use e = source.GetAsyncEnumerator CancellationToken.None - - while! e.MoveNextAsync() do - res.Add e.Current - - return res - } - - let toResizeArrayAndMapAsync mapper source = (toResizeArrayAsync >> Task.map mapper) source - - let map mapper (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - match mapper with - | CountableAction mapper -> taskSeq { - let mutable i = 0 - - for c in source do - yield mapper i c - i <- i + 1 - } - - | SimpleAction mapper -> taskSeq { - for c in source do - yield mapper c - } - - | AsyncCountableAction mapper -> taskSeq { - let mutable i = 0 - - for c in source do - let! result = mapper i c - yield result - i <- i + 1 - } - - | AsyncSimpleAction mapper -> taskSeq { - for c in source do - let! result = mapper c - yield result - } - - let zip (source1: TaskSeq<_>) (source2: TaskSeq<_>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - - taskSeq { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - go <- step1 && step2 - - while go do - yield e1.Current, e2.Current - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - go <- step1 && step2 - } - - let zip3 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - checkNonNull (nameof source3) source3 - - taskSeq { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - use e3 = source3.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let! step3 = e3.MoveNextAsync() - go <- step1 && step2 && step3 - - while go do - yield e1.Current, e2.Current, e3.Current - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let! step3 = e3.MoveNextAsync() - go <- step1 && step2 && step3 - } - - let zipWith (mapping: 'T -> 'U -> 'V) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - - taskSeq { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - go <- step1 && step2 - - while go do - yield mapping e1.Current e2.Current - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - go <- step1 && step2 - } - - let zipWithAsync (mapping: 'T -> 'U -> #Task<'V>) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - - taskSeq { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - go <- step1 && step2 - - while go do - let! result = mapping e1.Current e2.Current - yield result - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - go <- step1 && step2 - } - - let zipWith3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'V) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) (source3: TaskSeq<'T3>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - checkNonNull (nameof source3) source3 - - taskSeq { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - use e3 = source3.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let! step3 = e3.MoveNextAsync() - go <- step1 && step2 && step3 - - while go do - yield mapping e1.Current e2.Current e3.Current - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let! step3 = e3.MoveNextAsync() - go <- step1 && step2 && step3 - } - - let zipWithAsync3 (mapping: 'T1 -> 'T2 -> 'T3 -> #Task<'V>) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) (source3: TaskSeq<'T3>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - checkNonNull (nameof source3) source3 - - taskSeq { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - use e3 = source3.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let! step3 = e3.MoveNextAsync() - go <- step1 && step2 && step3 - - while go do - let! result = mapping e1.Current e2.Current e3.Current - yield result - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let! step3 = e3.MoveNextAsync() - go <- step1 && step2 && step3 - } - - let compareWith (comparer: 'T -> 'T -> int) (source1: TaskSeq<'T>) (source2: TaskSeq<'T>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - - task { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - let mutable result = 0 - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let mutable has1 = step1 - let mutable has2 = step2 - - while result = 0 && (has1 || has2) do - match has1, has2 with - | false, _ -> result <- -1 // source1 is shorter: less than - | _, false -> result <- 1 // source2 is shorter: greater than - | true, true -> - let cmp = comparer e1.Current e2.Current - - if cmp <> 0 then - result <- cmp - else - let! s1 = e1.MoveNextAsync() - let! s2 = e2.MoveNextAsync() - has1 <- s1 - has2 <- s2 - - return result - } - - let compareWithAsync (comparer: 'T -> 'T -> #Task) (source1: TaskSeq<'T>) (source2: TaskSeq<'T>) = - checkNonNull (nameof source1) source1 - checkNonNull (nameof source2) source2 - - task { - use e1 = source1.GetAsyncEnumerator CancellationToken.None - use e2 = source2.GetAsyncEnumerator CancellationToken.None - let mutable result = 0 - let! step1 = e1.MoveNextAsync() - let! step2 = e2.MoveNextAsync() - let mutable has1 = step1 - let mutable has2 = step2 - - while result = 0 && (has1 || has2) do - match has1, has2 with - | false, _ -> result <- -1 // source1 is shorter: less than - | _, false -> result <- 1 // source2 is shorter: greater than - | true, true -> - let! cmp = comparer e1.Current e2.Current - - if cmp <> 0 then - result <- cmp - else - let! s1 = e1.MoveNextAsync() - let! s2 = e2.MoveNextAsync() - has1 <- s1 - has2 <- s2 - - return result - } - - let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - for c in source do - yield! binder c :> IAsyncEnumerable<_> - } - - let collectSeq (binder: _ -> #seq<_>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - for c in source do - yield! binder c :> seq<_> - } - - let collectAsync (binder: _ -> #Task<#IAsyncEnumerable<_>>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - for c in source do - let! result = binder c - yield! result :> IAsyncEnumerable<_> - } - - let collectSeqAsync (binder: _ -> #Task<#seq<_>>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - for c in source do - let! result = binder c - yield! result :> seq<_> - } - - let tryLast (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable last = ValueNone - - while! e.MoveNextAsync() do - last <- ValueSome e.Current - - match last with - | ValueSome value -> return Some value - | ValueNone -> return None - } - - let tryHead (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - match! e.MoveNextAsync() with - | true -> return Some e.Current - | false -> return None - } - - let tryTail (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - match! e.MoveNextAsync() with - | false -> return None - | true -> - return - taskSeq { - while! e.MoveNextAsync() do - yield e.Current - } - |> Some - } - - let firstOrDefault defaultValue source = - tryHead source - |> Task.map (Option.defaultValue defaultValue) - - let lastOrDefault defaultValue source = - tryLast source - |> Task.map (Option.defaultValue defaultValue) - - let splitAt count (source: TaskSeq<'T>) = - checkNonNull (nameof source) source - - if count < 0 then - invalidArg (nameof count) $"The value must be non-negative, but was {count}." - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let first = ResizeArray<'T>(count) - let mutable i = 0 - let mutable go = true - - while go && i < count do - let! step = e.MoveNextAsync() - - if step then - first.Add e.Current - i <- i + 1 - else - go <- false - - // 'rest' captures 'e' from the outer task block; if the source was not exhausted, - // advance once past the last element added to 'first', then yield the remainder. - let rest = taskSeq { - if go then - while! e.MoveNextAsync() do - yield e.Current - } - - return first.ToArray(), rest - } - - let tryItem index (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - if index < 0 then - // while the loop below wouldn't run anyway, we don't want to call MoveNext in this case - // to prevent side effects hitting unnecessarily - return None - else - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let mutable idx = 0 - let mutable foundItem = None - let! step = e.MoveNextAsync() - go <- step - - // advance past the first `index` elements, then capture the current element - while go && idx < index do - let! step = e.MoveNextAsync() - go <- step - idx <- idx + 1 - - if go then - foundItem <- Some e.Current - - return foundItem - } - - let tryPick chooser (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - let mutable go = true - let mutable foundItem = None - let! step = e.MoveNextAsync() - go <- step - - match chooser with - | TryPick picker -> - while go do - match picker e.Current with - | Some value -> - foundItem <- Some value - go <- false - | None -> - let! step = e.MoveNextAsync() - go <- step - - | TryPickAsync picker -> - while go do - match! picker e.Current with - | Some value -> - foundItem <- Some value - go <- false - | None -> - let! step = e.MoveNextAsync() - go <- step - - return foundItem - } - - let tryFind predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - let mutable go = true - let mutable foundItem = None - let! step = e.MoveNextAsync() - go <- step - - match predicate with - | Predicate predicate -> - while go do - let current = e.Current - - match predicate current with - | true -> - foundItem <- Some current - go <- false - | false -> - let! step = e.MoveNextAsync() - go <- step - - | PredicateAsync predicate -> - while go do - let current = e.Current - - match! predicate current with - | true -> - foundItem <- Some current - go <- false - | false -> - let! step = e.MoveNextAsync() - go <- step - - return foundItem - } - - let tryFindIndex predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - - let mutable go = true - let mutable isFound = false - let mutable index = -1 - let! step = e.MoveNextAsync() - go <- step - - match predicate with - | Predicate predicate -> - while go && not isFound do - index <- index + 1 - isFound <- predicate e.Current - - if not isFound then - let! step = e.MoveNextAsync() - go <- step - - | PredicateAsync predicate -> - while go && not isFound do - index <- index + 1 - let! predicateResult = predicate e.Current - isFound <- predicateResult - - if not isFound then - let! step = e.MoveNextAsync() - go <- step - - if isFound then return Some index else return None - } - - let choose chooser (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - - match chooser with - | TryPick picker -> - for item in source do - match picker item with - | Some value -> yield value - | None -> () - - | TryPickAsync picker -> - for item in source do - match! picker item with - | Some value -> yield value - | None -> () - } - - let chooseV chooser (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - - match chooser with - | TryPickV picker -> - for item in source do - match picker item with - | ValueSome value -> yield value - | ValueNone -> () - - | TryPickVAsync picker -> - for item in source do - match! picker item with - | ValueSome value -> yield value - | ValueNone -> () - } - - let filter predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - match predicate with - | Predicate syncPredicate -> - for item in source do - if syncPredicate item then - yield item - - | PredicateAsync asyncPredicate -> - for item in source do - match! asyncPredicate item with - | true -> yield item - | false -> () - } - - let forall predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - match predicate with - | Predicate syncPredicate -> task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable state = true - let! cont = e.MoveNextAsync() - let mutable hasMore = cont - - while state && hasMore do - state <- syncPredicate e.Current - - if state then - let! cont = e.MoveNextAsync() - hasMore <- cont - - return state - } - - | PredicateAsync asyncPredicate -> task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable state = true - let! cont = e.MoveNextAsync() - let mutable hasMore = cont - - while state && hasMore do - let! pred = asyncPredicate e.Current - state <- pred - - if state then - let! cont = e.MoveNextAsync() - hasMore <- cont - - return state - } - - /// Direct bool-returning exists, avoiding the Option<'T> allocation that tryFind+isSome would incur. - let exists predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - match predicate with - | Predicate syncPredicate -> task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable found = false - let! cont = e.MoveNextAsync() - let mutable hasMore = cont - - while not found && hasMore do - found <- syncPredicate e.Current - - if not found then - let! cont = e.MoveNextAsync() - hasMore <- cont - - return found - } - - | PredicateAsync asyncPredicate -> task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable found = false - let! cont = e.MoveNextAsync() - let mutable hasMore = cont - - while not found && hasMore do - let! pred = asyncPredicate e.Current - found <- pred - - if not found then - let! cont = e.MoveNextAsync() - hasMore <- cont - - return found - } - - /// Direct bool-returning contains, avoiding the Option<'T> allocation and closure that tryFind+isSome would incur. - let contains (value: 'T) (source: TaskSeq<'T>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable found = false - let! cont = e.MoveNextAsync() - let mutable hasMore = cont - - while not found && hasMore do - if e.Current = value then - found <- true - else - let! cont = e.MoveNextAsync() - hasMore <- cont - - return found - } - - let distinct (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - // only create hashset when we start iterating; sequential so plain HashSet suffices - let seen = HashSet<_>(HashIdentity.Structural) - - for item in source do - if seen.Add item then - yield item - } - - let distinctBy (projection: _ -> _) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - let seen = HashSet<_>(HashIdentity.Structural) - - for item in source do - if seen.Add(projection item) then - yield item - } - - let distinctByAsync (projection: _ -> #Task<_>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - let seen = HashSet<_>(HashIdentity.Structural) - - for item in source do - let! key = projection item - - if seen.Add key then - yield item - } - - let skipOrTake skipOrTake count (source: TaskSeq<_>) = - checkNonNull (nameof source) source - raiseCannotBeNegative (nameof count) count - - match skipOrTake with - | Skip -> - // don't create a new sequence if count = 0 - if count = 0 then - source - else - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - - for _ in 1..count do - let! hasMore = e.MoveNextAsync() - - if not hasMore then - raiseInsufficient () - - while! e.MoveNextAsync() do - yield e.Current - - } - | Drop -> - // don't create a new sequence if count = 0 - if count = 0 then - source - else - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable i = 0 - let mutable cont = true - - // advance past 'count' elements; stop early if the source is shorter - while cont && i < count do - let! hasMore = e.MoveNextAsync() - if hasMore then i <- i + 1 else cont <- false - - // return remaining elements; enumerator is at element (count-1) so one - // more MoveNext is needed to reach element (count) - if cont then - while! e.MoveNextAsync() do - yield e.Current - - } - | Take -> - // don't initialize an empty task sequence - if count = 0 then - empty - else - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - - for _ in count .. -1 .. 1 do - let! step = e.MoveNextAsync() - - if not step then - raiseInsufficient () - - yield e.Current - } - - | Truncate -> - // don't create a new sequence if count = 0 - if count = 0 then - empty - else - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable yielded = 0 - let mutable cont = true - - // yield up to 'count' elements; stop when exhausted or limit reached - while cont && yielded < count do - let! hasMore = e.MoveNextAsync() - - if hasMore then - yield e.Current - yielded <- yielded + 1 - else - cont <- false - - } - - let takeWhile isInclusive predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! notEmpty = e.MoveNextAsync() - let mutable hasMore = notEmpty - - match predicate with - | Predicate synchronousPredicate -> - while hasMore && synchronousPredicate e.Current do - yield e.Current - let! cont = e.MoveNextAsync() - hasMore <- cont - - | PredicateAsync asyncPredicate -> - let mutable predicateHolds = true - - while hasMore && predicateHolds do // TODO: check perf if `while!` is going to be better or equal - let! predicateIsTrue = asyncPredicate e.Current - - if predicateIsTrue then - yield e.Current - let! cont = e.MoveNextAsync() - hasMore <- cont - - predicateHolds <- predicateIsTrue - - // "inclusive" means: always return the item that we pulled, regardless of the result of applying the predicate - // and only stop thereafter. The non-inclusive versions, in contrast, do not return the item under which the predicate is false. - if hasMore && isInclusive then - yield e.Current - } - - let skipWhile isInclusive predicate (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! notEmpty = e.MoveNextAsync() - let mutable hasMore = notEmpty - - match predicate with - | Predicate synchronousPredicate -> - while hasMore && synchronousPredicate e.Current do - // keep skipping - let! cont = e.MoveNextAsync() - hasMore <- cont - - | PredicateAsync asyncPredicate -> - let mutable predicateHolds = true - - while hasMore && predicateHolds do // TODO: check perf if `while!` is going to be better or equal - let! predicateIsTrue = asyncPredicate e.Current - - if predicateIsTrue then - // keep skipping - let! cont = e.MoveNextAsync() - hasMore <- cont - - predicateHolds <- predicateIsTrue - - // "inclusive" means: always skip the item that we pulled, regardless of the result of applying the predicate - // and only stop thereafter. The non-inclusive versions, in contrast, do not skip the item under which the predicate is false. - if hasMore && not isInclusive then - yield e.Current // don't skip, unless inclusive - - // propagate the rest - while! e.MoveNextAsync() do - yield e.Current - } - - /// InsertAt or InsertManyAt - let insertAt index valueOrValues (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - match valueOrValues with - | Many values -> checkNonNull "values" values - | One _ -> () - - raiseCannotBeNegative (nameof index) index - - taskSeq { - let mutable i = 0 - - for item in source do - if i = index then - match valueOrValues with - | Many values -> yield! values - | One value -> yield value - - yield item - i <- i + 1 - - // allow inserting at the end - if i = index then - match valueOrValues with - | Many values -> yield! values - | One value -> yield value - - if i < index then - raiseOutOfBounds (nameof index) - } - - let removeAt index (source: TaskSeq<'T>) = - checkNonNull (nameof source) source - raiseCannotBeNegative (nameof index) index - - taskSeq { - let mutable i = 0 - - for item in source do - if i <> index then - yield item - - i <- i + 1 - - // cannot remove past end of sequence - if i <= index then - raiseOutOfBounds (nameof index) - } - - let removeManyAt index count (source: TaskSeq<'T>) = - checkNonNull (nameof source) source - raiseCannotBeNegative (nameof index) index - - taskSeq { - let mutable i = 0 - let indexEnd = index + count - - for item in source do - if i < index || i >= indexEnd then - yield item - - i <- i + 1 - - // cannot remove past end of sequence - if i <= index then - raiseOutOfBounds (nameof index) - } - - let updateAt index value (source: TaskSeq<'T>) = - checkNonNull (nameof source) source - raiseCannotBeNegative (nameof index) index - - taskSeq { - let mutable i = 0 - - for item in source do - if i <> index then // most common scenario on top (cpu prediction) - yield item - else - yield value - - i <- i + 1 - - // cannot update past end of sequence - if i <= index then - raiseOutOfBounds (nameof index) - } - - let except (itemsToExclude: TaskSeq<_>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - checkNonNull (nameof itemsToExclude) itemsToExclude - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if hasFirst then - // only create hashset by the time we actually start iterating; - // taskSeq enumerates sequentially, so a plain HashSet suffices — no locking needed. - let hashSet = HashSet<_>(HashIdentity.Structural) - - use excl = itemsToExclude.GetAsyncEnumerator CancellationToken.None - - while! excl.MoveNextAsync() do - hashSet.Add excl.Current |> ignore - - // if true, it was added, and therefore unique, so we return it - // if false, it existed, and therefore a duplicate, and we skip - if hashSet.Add e.Current then - yield e.Current - - while! e.MoveNextAsync() do - let current = e.Current - - if hashSet.Add current then - yield current - - } - - let exceptOfSeq itemsToExclude (source: TaskSeq<_>) = - checkNonNull (nameof source) source - checkNonNull (nameof itemsToExclude) itemsToExclude - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if hasFirst then - // only create hashset by the time we actually start iterating; - // initialize directly from the seq — taskSeq is sequential so no locking needed. - let hashSet = HashSet<_>(itemsToExclude, HashIdentity.Structural) - - // if true, it was added, and therefore unique, so we return it - // if false, it existed, and therefore a duplicate, and we skip - if hashSet.Add e.Current then - yield e.Current - - while! e.MoveNextAsync() do - let current = e.Current - - if hashSet.Add current then - yield current - - } - - let distinctUntilChanged (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if hasFirst then - let mutable previous = e.Current - yield previous - - while! e.MoveNextAsync() do - let current = e.Current - - if current <> previous then - yield current - previous <- current - } - - let distinctUntilChangedWith (comparer: 'T -> 'T -> bool) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if hasFirst then - let mutable previous = e.Current - yield previous - - while! e.MoveNextAsync() do - let current = e.Current - - if not (comparer previous current) then - yield current - previous <- current - } - - let distinctUntilChangedWithAsync (comparer: 'T -> 'T -> #Task) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if hasFirst then - let mutable previous = e.Current - yield previous - - while! e.MoveNextAsync() do - let current = e.Current - let! areEqual = comparer previous current - - if not areEqual then - yield current - previous <- current - } - - let pairwise (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - taskSeq { - use e = source.GetAsyncEnumerator CancellationToken.None - let! hasFirst = e.MoveNextAsync() - - if hasFirst then - let mutable previous = e.Current - - while! e.MoveNextAsync() do - let current = e.Current - yield previous, current - previous <- current - } - - let groupBy (projector: ProjectorAction<'T, 'Key, _>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let groups = Dictionary<'Key, ResizeArray<'T>>(HashIdentity.Structural) - let order = ResizeArray<'Key>() - - match projector with - | ProjectorAction proj -> - while! e.MoveNextAsync() do - let key = proj e.Current - let mutable ra = Unchecked.defaultof<_> - - if not (groups.TryGetValue(key, &ra)) then - ra <- ResizeArray() - groups[key] <- ra - order.Add key - - ra.Add e.Current - - | AsyncProjectorAction proj -> - while! e.MoveNextAsync() do - let! key = proj e.Current - let mutable ra = Unchecked.defaultof<_> - - if not (groups.TryGetValue(key, &ra)) then - ra <- ResizeArray() - groups[key] <- ra - order.Add key - - ra.Add e.Current - - return - Array.init order.Count (fun i -> - let k = order[i] - k, groups[k].ToArray()) - } - - let countBy (projector: ProjectorAction<'T, 'Key, _>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let counts = Dictionary<'Key, int>(HashIdentity.Structural) - let order = ResizeArray<'Key>() - - match projector with - | ProjectorAction proj -> - while! e.MoveNextAsync() do - let key = proj e.Current - let mutable count = 0 - - if not (counts.TryGetValue(key, &count)) then - order.Add key - - counts[key] <- count + 1 - - | AsyncProjectorAction proj -> - while! e.MoveNextAsync() do - let! key = proj e.Current - let mutable count = 0 - - if not (counts.TryGetValue(key, &count)) then - order.Add key - - counts[key] <- count + 1 - - return Array.init order.Count (fun i -> let k = order[i] in k, counts[k]) - } - - let partition (predicate: PredicateAction<'T, _>) (source: TaskSeq<_>) = - checkNonNull (nameof source) source - - task { - use e = source.GetAsyncEnumerator CancellationToken.None - let trueItems = ResizeArray<'T>() - let falseItems = ResizeArray<'T>() - - match predicate with - | Predicate pred -> - while! e.MoveNextAsync() do - let item = e.Current - - if pred item then - trueItems.Add item - else - falseItems.Add item - - | PredicateAsync pred -> - while! e.MoveNextAsync() do - let item = e.Current - let! result = pred item - if result then trueItems.Add item else falseItems.Add item - - return trueItems.ToArray(), falseItems.ToArray() - } - - let chunkBySize chunkSize (source: TaskSeq<'T>) : TaskSeq<'T[]> = - if chunkSize < 1 then - invalidArg (nameof chunkSize) $"The value must be positive, but was %i{chunkSize}." - - checkNonNull (nameof source) source - - taskSeq { - // Use a fixed-size array with a count index to avoid ResizeArray overhead. - let buffer = Array.zeroCreate<'T> chunkSize - let mutable count = 0 - - for item in source do - buffer.[count] <- item - count <- count + 1 - - if count = chunkSize then - yield Array.copy buffer - count <- 0 - - if count > 0 then - // Last partial chunk: copy only the filled portion. - yield buffer.[0 .. count - 1] - } - - let chunkBy (projection: 'T -> 'Key) (source: TaskSeq<'T>) : TaskSeq<'Key * 'T[]> = - checkNonNull (nameof source) source - - taskSeq { - let mutable maybeCurrentKey = ValueNone - let mutable currentChunk = ResizeArray<'T>() - - for item in source do - let key = projection item - - match maybeCurrentKey with - | ValueNone -> - maybeCurrentKey <- ValueSome key - currentChunk.Add item - | ValueSome prevKey -> - if prevKey = key then - currentChunk.Add item - else - yield prevKey, currentChunk.ToArray() - currentChunk.Clear() // reuse backing array; ToArray() already captured a snapshot - currentChunk.Add item - maybeCurrentKey <- ValueSome key - - match maybeCurrentKey with - | ValueNone -> () - | ValueSome lastKey -> yield lastKey, currentChunk.ToArray() - } - - let chunkByAsync (projection: 'T -> #Task<'Key>) (source: TaskSeq<'T>) : TaskSeq<'Key * 'T[]> = - checkNonNull (nameof source) source - - taskSeq { - let mutable maybeCurrentKey = ValueNone - let mutable currentChunk = ResizeArray<'T>() - - for item in source do - let! key = projection item - - match maybeCurrentKey with - | ValueNone -> - maybeCurrentKey <- ValueSome key - currentChunk.Add item - | ValueSome prevKey -> - if prevKey = key then - currentChunk.Add item - else - yield prevKey, currentChunk.ToArray() - currentChunk.Clear() // reuse backing array; ToArray() already captured a snapshot - currentChunk.Add item - maybeCurrentKey <- ValueSome key - - match maybeCurrentKey with - | ValueNone -> () - | ValueSome lastKey -> yield lastKey, currentChunk.ToArray() - } - - let windowed windowSize (source: TaskSeq<_>) = - if windowSize <= 0 then - invalidArg (nameof windowSize) $"The value must be positive, but was %i{windowSize}." - - checkNonNull (nameof source) source - - taskSeq { - // Ring buffer: arr holds elements in circular order. - // 'count' tracks total elements seen; count % windowSize is the next write position. - let arr = Array.zeroCreate windowSize - let mutable count = 0 - - for item in source do - arr.[count % windowSize] <- item - count <- count + 1 - - if count >= windowSize then - // Copy ring buffer in source order into a fresh array. - let result = Array.zeroCreate windowSize - let start = count % windowSize // index of oldest element in the ring - - if start = 0 then - Array.blit arr 0 result 0 windowSize - else - Array.blit arr start result 0 (windowSize - start) - Array.blit arr 0 result (windowSize - start) start - - yield result - } diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs deleted file mode 100644 index 147734bf..00000000 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ /dev/null @@ -1,78 +0,0 @@ -namespace FSharp.Control - -open System -open System.Threading.Tasks - -[] -module ValueTaskExtensions = - type ValueTask with - static member inline CompletedTask = - // This mimics how it is done in net5.0 and later internally - Unchecked.defaultof - -module ValueTask = - let False = ValueTask() - let True = ValueTask true - let inline fromResult (value: 'T) = ValueTask<'T> value - let inline ofSource taskSource version = ValueTask(taskSource, version) - let inline ofTask (task: Task<'T>) = ValueTask<'T> task - - let inline ignore (valueTask: ValueTask<'T>) = - // this implementation follows Stephen Toub's advice, see: - // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 - if valueTask.IsCompletedSuccessfully then - // ensure any side effect executes - valueTask.Result |> ignore - ValueTask() - else - ValueTask(valueTask.AsTask()) - - [] - let inline FromResult (value: 'T) = ValueTask<'T> value - - [] - let inline ofIValueTaskSource taskSource version = ofSource taskSource version - -module Task = - let inline fromResult (value: 'U) : Task<'U> = Task.FromResult value - let inline ofAsync (async: Async<'T>) = task { return! async } - let inline ofTask (task': Task) = task { do! task' } - let inline apply (func: _ -> _) = func >> Task.FromResult - let inline toAsync (task: Task<'T>) = Async.AwaitTask task - let inline toValueTask (task: Task<'T>) = ValueTask<'T> task - let inline ofValueTask (valueTask: ValueTask<'T>) = task { return! valueTask } - - let inline ignore (task: Task<'T>) = - TaskBuilder.task { - // ensure the task is awaited - let! _ = task - return () - } - :> Task - - let inline map mapper (task: Task<'T>) : Task<'U> = TaskBuilder.task { - let! result = task - return mapper result - } - - let inline bind binder (task: Task<'T>) : Task<'U> = TaskBuilder.task { - let! t = task - return! binder t - } - -module Async = - let inline ofTask (task: Task<'T>) = Async.AwaitTask task - let inline ofUnitTask (task: Task) = Async.AwaitTask task - let inline toTask (async: Async<'T>) = task { return! async } - - let inline ignore (async: Async<'T>) = Async.Ignore async - - let inline map mapper (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { - let! result = async - return mapper result - } - - let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { - let! result = async - return! binder result - } diff --git a/src/FSharp.Control.TaskSeq/Utils.fsi b/src/FSharp.Control.TaskSeq/Utils.fsi deleted file mode 100644 index d252d45a..00000000 --- a/src/FSharp.Control.TaskSeq/Utils.fsi +++ /dev/null @@ -1,106 +0,0 @@ -namespace FSharp.Control - -open System -open System.Threading.Tasks -open System.Threading.Tasks.Sources - -[] -module ValueTaskExtensions = - - /// Shims back-filling .NET 5+ functionality for use on netstandard2.1 - type ValueTask with - - /// (Extension member) Gets a ValueTask that has already completed successfully. - static member inline CompletedTask: ValueTask - -module ValueTask = - - /// A successfully completed ValueTask of boolean that has the value false. - val False: ValueTask - - /// A successfully completed ValueTask of boolean that has the value true. - val True: ValueTask - - /// Creates a ValueTask with the supplied result of the successful operation. - val inline fromResult: value: 'T -> ValueTask<'T> - - /// - /// The function is deprecated since version 0.4.0, - /// please use in its stead. See . - /// - [] - val inline FromResult: value: 'T -> ValueTask<'T> - - /// - /// Initializes a new instance of with an - /// representing its operation. - /// - val inline ofSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask - - /// - /// The function is deprecated since version 0.4.0, - /// please use in its stead. See . - /// - [] - val inline ofIValueTaskSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask - - /// Creates a ValueTask from a Task<'T> - val inline ofTask: task: Task<'T> -> ValueTask<'T> - - /// Convert a ValueTask<'T> into a non-generic ValueTask, ignoring the result - val inline ignore: valueTask: ValueTask<'T> -> ValueTask - -module Task = - - /// Creates a Task<'U> that's completed successfully with the specified result. - val inline fromResult: value: 'U -> Task<'U> - - /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` - val inline ofAsync: async: Async<'T> -> Task<'T> - - /// Convert a non-generic Task into a Task - val inline ofTask: task': Task -> Task - - /// Convert a plain function into a task-returning function - val inline apply: func: ('a -> 'b) -> ('a -> Task<'b>) - - /// Convert a Task<'T> into an Async<'T> - val inline toAsync: task: Task<'T> -> Async<'T> - - /// Convert a Task<'T> into a ValueTask<'T> - val inline toValueTask: task: Task<'T> -> ValueTask<'T> - - /// - /// Convert a ValueTask<'T> to a Task<'T>. For a non-generic ValueTask, - /// consider: . - /// - val inline ofValueTask: valueTask: ValueTask<'T> -> Task<'T> - - /// Convert a Task<'T> into a non-generic Task, ignoring the result - val inline ignore: task: Task<'T> -> Task - - /// Map a Task<'T> - val inline map: mapper: ('T -> 'U) -> task: Task<'T> -> Task<'U> - - /// Bind a Task<'T> - val inline bind: binder: ('T -> #Task<'U>) -> task: Task<'T> -> Task<'U> - -module Async = - - /// Convert an Task<'T> into an Async<'T> - val inline ofTask: task: Task<'T> -> Async<'T> - - /// Convert a non-generic Task into an Async - val inline ofUnitTask: task: Task -> Async - - /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` - val inline toTask: async: Async<'T> -> Task<'T> - - /// Convert an Async<'T> into an Async, ignoring the result - val inline ignore: async: Async<'T> -> Async - - /// Map an Async<'T> - val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U> - - /// Bind an Async<'T> - val inline bind: binder: ('T -> Async<'U>) -> async: Async<'T> -> Async<'U>