diff --git a/.github/workflows/autoloop.md b/.github/workflows/autoloop.md
index ba33d21..4cd407b 100644
--- a/.github/workflows/autoloop.md
+++ b/.github/workflows/autoloop.md
@@ -283,7 +283,6 @@ Examples:
4. **Rejected or errored iterations** do not commit — changes are discarded.
5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR.
6. The branch may be **merged into the default branch** at any time (by a maintainer or CI). After merging, the branch continues to be used for future iterations — it is never deleted while the program is active. On the next iteration, the branch is automatically reset to the default branch (see step 2) so that already-merged commits do not cause patch conflicts.
-7. A **sync workflow** automatically merges the default branch into all active `autoloop/*` branches whenever the default branch changes, keeping them up to date.
### Cross-Linking
diff --git a/.github/workflows/sync-branches.lock.yml b/.github/workflows/sync-branches.lock.yml
deleted file mode 100644
index 6972f09..0000000
--- a/.github/workflows/sync-branches.lock.yml
+++ /dev/null
@@ -1,550 +0,0 @@
-# ___ _ _
-# / _ \ | | (_)
-# | |_| | __ _ ___ _ __ | |_ _ ___
-# | _ |/ _` |/ _ \ '_ \| __| |/ __|
-# | | | | (_| | __/ | | | |_| | (__
-# \_| |_/\__, |\___|_| |_|\__|_|\___|
-# __/ |
-# _ _ |___/
-# | | | | / _| |
-# | | | | ___ _ __ _ __| |_| | _____ ____
-# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
-# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
-# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
-#
-# This file was automatically generated by gh-aw (v0.65.6). DO NOT EDIT.
-#
-# To update this file, edit the corresponding .md file 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/
-#
-# Keeps Autoloop program branches up to date with the default branch.
-# Runs whenever the default branch changes and merges it into all active
-# autoloop/* branches so that program iterations always build on the latest code.
-#
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7347655448caf952972200853ace37356f693514cb8a4ae018797501b79c86a5","compiler_version":"v0.65.6","strict":true,"agent_id":"copilot"}
-
-name: "Sync Branches"
-"on":
- push:
- branches:
- - main
- workflow_dispatch:
- inputs:
- aw_context:
- default: ""
- description: Agent caller context (used internally by Agentic Workflows).
- required: false
- type: string
-
-permissions: {}
-
-concurrency:
- group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}"
-
-run-name: "Sync Branches"
-
-jobs:
- activation:
- needs: pre_activation
- if: needs.pre_activation.outputs.activated == 'true'
- runs-on: ubuntu-slim
- permissions:
- contents: read
- outputs:
- comment_id: ""
- comment_repo: ""
- 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 }}
- steps:
- - name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
- with:
- destination: ${{ runner.temp }}/gh-aw/actions
- - 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 || 'auto' }}
- GH_AW_INFO_VERSION: "latest"
- GH_AW_INFO_AGENT_VERSION: "latest"
- GH_AW_INFO_CLI_VERSION: "v0.65.6"
- GH_AW_INFO_WORKFLOW_NAME: "Sync Branches"
- GH_AW_INFO_EXPERIMENTAL: "false"
- GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
- GH_AW_INFO_STAGED: "false"
- GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
- GH_AW_INFO_FIREWALL_ENABLED: "true"
- GH_AW_INFO_AWF_VERSION: "v0.25.11"
- GH_AW_INFO_AWMG_VERSION: ""
- GH_AW_INFO_FIREWALL_TYPE: "squid"
- GH_AW_COMPILED_STRICT: "true"
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- 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/generate_aw_info.cjs');
- await main(core, context);
- - name: Validate COPILOT_GITHUB_TOKEN secret
- id: validate-secret
- run: ${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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
- sparse-checkout: |
- .github
- .agents
- sparse-checkout-cone-mode: true
- fetch-depth: 1
- - name: Check workflow file timestamps
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_WORKFLOW_FILE: "sync-branches.lock.yml"
- 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/check_workflow_timestamp_api.cjs');
- await main();
- - name: Check compile-agentic version
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_COMPILED_VERSION: "v0.65.6"
- 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/check_version_updates.cjs');
- await main();
- - name: Create prompt with built-in context
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- # poutine:ignore untrusted_checkout_exec
- run: |
- bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh
- {
- cat << 'GH_AW_PROMPT_4db17a6c15417a22_EOF'
-
- GH_AW_PROMPT_4db17a6c15417a22_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 << 'GH_AW_PROMPT_4db17a6c15417a22_EOF'
-
- The following GitHub context information is available for this workflow:
- {{#if __GH_AW_GITHUB_ACTOR__ }}
- - **actor**: __GH_AW_GITHUB_ACTOR__
- {{/if}}
- {{#if __GH_AW_GITHUB_REPOSITORY__ }}
- - **repository**: __GH_AW_GITHUB_REPOSITORY__
- {{/if}}
- {{#if __GH_AW_GITHUB_WORKSPACE__ }}
- - **workspace**: __GH_AW_GITHUB_WORKSPACE__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
- - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
- - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
- - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
- {{/if}}
- {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
- - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
- {{/if}}
- {{#if __GH_AW_GITHUB_RUN_ID__ }}
- - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
- {{/if}}
-
-
- GH_AW_PROMPT_4db17a6c15417a22_EOF
- cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_prompt.md"
- cat << 'GH_AW_PROMPT_4db17a6c15417a22_EOF'
-
- {{#runtime-import .github/workflows/sync-branches.md}}
- GH_AW_PROMPT_4db17a6c15417a22_EOF
- } > "$GH_AW_PROMPT"
- - name: Interpolate variables and render templates
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- 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/interpolate_prompt.cjs');
- await main();
- - name: Substitute placeholders
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_GITHUB_ACTOR: ${{ github.actor }}
- GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
- GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
- GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
- GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
- GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }}
- with:
- script: |
- const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
-
- 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_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
- GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
- GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
- GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
- GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
- 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_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
- GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED
- }
- });
- - 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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- with:
- name: activation
- path: |
- /tmp/gh-aw/aw_info.json
- /tmp/gh-aw/aw-prompts/prompt.txt
- retention-days: 1
-
- agent:
- needs: activation
- runs-on: ubuntu-latest
- permissions:
- contents: write
- env:
- GH_AW_WORKFLOW_ID_SANITIZED: syncbranches
- outputs:
- checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
- effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }}
- inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }}
- model: ${{ needs.activation.outputs.model }}
- steps:
- - name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
- with:
- destination: ${{ runner.temp }}/gh-aw/actions
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- persist-credentials: false
- - 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: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- 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"
- - env:
- DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- name: Merge default branch into all autoloop program branches
- run: "python3 - << 'PYEOF'\nimport os, subprocess, sys\n\ntoken = os.environ.get(\"GITHUB_TOKEN\", \"\")\nrepo = os.environ.get(\"GITHUB_REPOSITORY\", \"\")\ndefault_branch = os.environ.get(\"DEFAULT_BRANCH\", \"main\")\n\n# List all remote branches matching the autoloop/* pattern\nresult = subprocess.run(\n [\"git\", \"branch\", \"-r\", \"--list\", \"origin/autoloop/*\"],\n capture_output=True, text=True\n)\nif result.returncode != 0:\n print(f\"Failed to list remote branches: {result.stderr}\")\n sys.exit(0)\n\nbranches = [b.strip().replace(\"origin/\", \"\") for b in result.stdout.strip().split(\"\\n\") if b.strip()]\n\nif not branches:\n print(\"No autoloop/* branches found. Nothing to sync.\")\n sys.exit(0)\n\nprint(f\"Found {len(branches)} autoloop branch(es) to sync: {branches}\")\n\ndef rev_count(range_spec):\n r = subprocess.run(\n [\"git\", \"rev-list\", \"--count\", range_spec],\n capture_output=True, text=True\n )\n if r.returncode != 0:\n return None\n try:\n return int(r.stdout.strip())\n except ValueError:\n return None\n\nfailed = []\nfor branch in branches:\n print(f\"\\n--- Syncing {branch} with {default_branch} ---\")\n\n # Fetch both branches so the ahead/behind counts below are computed\n # against up-to-date local copies of the remote tips.\n subprocess.run([\"git\", \"fetch\", \"origin\", branch], capture_output=True)\n subprocess.run([\"git\", \"fetch\", \"origin\", default_branch], capture_output=True)\n\n # Compute ahead/behind counts using the remote-tracking refs so we\n # make a decision based on commit delta (not content delta).\n ahead = rev_count(f\"origin/{default_branch}..origin/{branch}\")\n behind = rev_count(f\"origin/{branch}..origin/{default_branch}\")\n if ahead is None or behind is None:\n print(f\" Failed to compute ahead/behind for {branch}\")\n failed.append(branch)\n continue\n print(f\" ahead={ahead} behind={behind}\")\n\n if ahead == 0 and behind > 0:\n # All of the branch's commits are already in the default branch.\n # Merging would produce a noisy \"Merge main into branch\" commit\n # that re-exposes every historical file as a patch touch — the\n # failure mode that triggers gh-aw's E003 (>100 files) when a\n # new PR is opened. Fast-forward the canonical branch instead.\n # This is lossless because ahead=0 proves every commit on the\n # branch is already reachable from the default branch.\n ff = subprocess.run(\n [\"git\", \"checkout\", \"-B\", branch, f\"origin/{default_branch}\"],\n capture_output=True, text=True\n )\n if ff.returncode != 0:\n print(f\" Failed to fast-forward {branch}: {ff.stderr}\")\n failed.append(branch)\n continue\n # Use --force-with-lease so that if anyone else is simultaneously\n # pushing to the branch, the update is rejected rather than\n # overwriting their commits.\n push = subprocess.run(\n [\"git\", \"push\", \"--force-with-lease\", \"origin\", branch],\n capture_output=True, text=True\n )\n if push.returncode != 0:\n print(f\" Failed to force-push {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n print(f\" Fast-forwarded {branch} to origin/{default_branch}\")\n continue\n\n if ahead == 0 and behind == 0:\n # Already at default branch — nothing to do.\n print(f\" {branch} is already up to date with origin/{default_branch}\")\n continue\n\n if ahead > 0 and behind == 0:\n # Unique work preserved; no upstream drift to merge.\n print(f\" {branch} is ahead of origin/{default_branch} with no upstream drift; nothing to merge.\")\n continue\n\n # True divergence (ahead > 0 and behind > 0): check out and merge.\n checkout = subprocess.run(\n [\"git\", \"checkout\", \"-B\", branch, f\"origin/{branch}\"],\n capture_output=True, text=True\n )\n if checkout.returncode != 0:\n print(f\" Failed to checkout {branch}: {checkout.stderr}\")\n failed.append(branch)\n continue\n\n # Merge the default branch into the program branch\n merge = subprocess.run(\n [\"git\", \"merge\", f\"origin/{default_branch}\", \"--no-edit\",\n \"-m\", f\"Merge {default_branch} into {branch}\"],\n capture_output=True, text=True\n )\n if merge.returncode != 0:\n print(f\" Merge conflict or failure for {branch}: {merge.stderr}\")\n # Abort the merge to leave a clean state\n subprocess.run([\"git\", \"merge\", \"--abort\"], capture_output=True)\n failed.append(branch)\n continue\n\n # Push the updated branch\n push = subprocess.run(\n [\"git\", \"push\", \"origin\", branch],\n capture_output=True, text=True\n )\n if push.returncode != 0:\n print(f\" Failed to push {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n\n print(f\" Successfully synced {branch}\")\n\n# Return to default branch\nsubprocess.run([\"git\", \"checkout\", default_branch], capture_output=True)\n\nif failed:\n print(f\"\\n⚠️ Failed to sync {len(failed)} branch(es): {failed}\")\n print(\"These branches may need manual conflict resolution.\")\n # Don't fail the workflow — log the issue but continue\nelse:\n print(f\"\\n✅ All {len(branches)} branch(es) synced successfully.\")\nPYEOF"
- - name: Checkout PR branch
- id: checkout-pr
- if: |
- github.event.pull_request || github.event.issue.pull_request
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- 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);
- const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs');
- await main();
- - name: Install GitHub Copilot CLI
- run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest
- - name: Install AWF binary
- run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11
- - name: Determine automatic lockdown mode for GitHub MCP Server
- id: determine-automatic-lockdown
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- env:
- GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
- GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
- with:
- script: |
- const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs');
- await determineAutomaticLockdown(github, context, core);
- - name: Download container images
- run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11 ghcr.io/github/gh-aw-mcpg:v0.2.11 ghcr.io/github/github-mcp-server:v0.32.0
- - name: Start MCP Gateway
- id: start-mcp-gateway
- env:
- GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }}
- GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }}
- 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 /tmp/gh-aw/mcp-config
-
- # Export gateway environment variables for MCP config and gateway script
- export MCP_GATEWAY_PORT="80"
- export MCP_GATEWAY_DOMAIN="host.docker.internal"
- 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"
- export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/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 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 -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.2.11'
-
- mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_c44c901ef7ee68bd_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh
- {
- "mcpServers": {
- "github": {
- "type": "stdio",
- "container": "ghcr.io/github/github-mcp-server:v0.32.0",
- "env": {
- "GITHUB_HOST": "\${GITHUB_SERVER_URL}",
- "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
- "GITHUB_READ_ONLY": "1",
- "GITHUB_TOOLSETS": "repos"
- },
- "guard-policies": {
- "allow-only": {
- "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY",
- "repos": "$GITHUB_MCP_GUARD_REPOS"
- }
- }
- }
- },
- "gateway": {
- "port": $MCP_GATEWAY_PORT,
- "domain": "${MCP_GATEWAY_DOMAIN}",
- "apiKey": "${MCP_GATEWAY_API_KEY}",
- "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
- }
- }
- GH_AW_MCP_CONFIG_c44c901ef7ee68bd_EOF
- - name: Download activation artifact
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- with:
- name: activation
- path: /tmp/gh-aw
- - name: Clean git credentials
- continue-on-error: true
- run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh
- - name: Execute GitHub Copilot CLI
- id: agentic_execution
- # Copilot CLI tool arguments (sorted):
- timeout-minutes: 10
- run: |
- set -o pipefail
- touch /tmp/gh-aw/agent-step-summary.md
- # shellcheck disable=SC1003
- sudo -E awf --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" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,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,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,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,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.11 --skip-pull --enable-api-proxy \
- -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
- env:
- COPILOT_AGENT_RUNNER_TYPE: STANDALONE
- COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
- 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_VERSION: v0.65.6
- GITHUB_API_URL: ${{ github.api_url }}
- GITHUB_AW: true
- 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]
- XDG_CONFIG_HOME: /home/runner
- - name: Detect inference access error
- id: detect-inference-error
- if: always()
- continue-on-error: true
- run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh
- - name: Configure Git credentials
- env:
- REPO_NAME: ${{ github.repository }}
- SERVER_URL: ${{ github.server_url }}
- 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- 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/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: Parse agent logs for step summary
- if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- 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);
- 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- 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/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 so they can be uploaded as artifacts
- # AWF runs with sudo, creating files owned by root
- sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 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
- run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_token_usage.sh
- - name: Upload agent artifacts
- if: always()
- continue-on-error: true
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- 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/agent-stdio.log
- /tmp/gh-aw/agent/
- if-no-files-found: ignore
- - name: Upload firewall audit logs
- if: always()
- continue-on-error: true
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- with:
- name: firewall-audit-logs
- path: |
- /tmp/gh-aw/sandbox/firewall/logs/
- /tmp/gh-aw/sandbox/firewall/audit/
- if-no-files-found: ignore
-
- pre_activation:
- runs-on: ubuntu-slim
- outputs:
- activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
- matched_command: ''
- steps:
- - name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
- with:
- destination: ${{ runner.temp }}/gh-aw/actions
- - name: Check team membership for workflow
- id: check_membership
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- 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);
- const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs');
- await main();
-
diff --git a/.github/workflows/sync-branches.md b/.github/workflows/sync-branches.md
deleted file mode 100644
index 3d87064..0000000
--- a/.github/workflows/sync-branches.md
+++ /dev/null
@@ -1,169 +0,0 @@
----
-description: |
- Keeps Autoloop program branches up to date with the default branch.
- Runs whenever the default branch changes and merges it into all active
- autoloop/* branches so that program iterations always build on the latest code.
-
-on:
- push:
- branches: [main] # ← update this if your default branch is not 'main'
- workflow_dispatch:
-
-permissions: read-all
-
-timeout-minutes: 10
-
-tools:
- github:
- toolsets: [repos]
- bash: true
-
-steps:
- - name: Merge default branch into all autoloop program branches
- env:
- GITHUB_REPOSITORY: ${{ github.repository }}
- DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- run: |
- python3 - << 'PYEOF'
- import os, subprocess, sys
-
- token = os.environ.get("GITHUB_TOKEN", "")
- repo = os.environ.get("GITHUB_REPOSITORY", "")
- default_branch = os.environ.get("DEFAULT_BRANCH", "main")
-
- # List all remote branches matching the autoloop/* pattern
- result = subprocess.run(
- ["git", "branch", "-r", "--list", "origin/autoloop/*"],
- capture_output=True, text=True
- )
- if result.returncode != 0:
- print(f"Failed to list remote branches: {result.stderr}")
- sys.exit(0)
-
- branches = [b.strip().replace("origin/", "") for b in result.stdout.strip().split("\n") if b.strip()]
-
- if not branches:
- print("No autoloop/* branches found. Nothing to sync.")
- sys.exit(0)
-
- print(f"Found {len(branches)} autoloop branch(es) to sync: {branches}")
-
- def rev_count(range_spec):
- r = subprocess.run(
- ["git", "rev-list", "--count", range_spec],
- capture_output=True, text=True
- )
- if r.returncode != 0:
- return None
- try:
- return int(r.stdout.strip())
- except ValueError:
- return None
-
- failed = []
- for branch in branches:
- print(f"\n--- Syncing {branch} with {default_branch} ---")
-
- # Fetch both branches so the ahead/behind counts below are computed
- # against up-to-date local copies of the remote tips.
- subprocess.run(["git", "fetch", "origin", branch], capture_output=True)
- subprocess.run(["git", "fetch", "origin", default_branch], capture_output=True)
-
- # Compute ahead/behind counts using the remote-tracking refs so we
- # make a decision based on commit delta (not content delta).
- ahead = rev_count(f"origin/{default_branch}..origin/{branch}")
- behind = rev_count(f"origin/{branch}..origin/{default_branch}")
- if ahead is None or behind is None:
- print(f" Failed to compute ahead/behind for {branch}")
- failed.append(branch)
- continue
- print(f" ahead={ahead} behind={behind}")
-
- if ahead == 0 and behind > 0:
- # All of the branch's commits are already in the default branch.
- # Merging would produce a noisy "Merge main into branch" commit
- # that re-exposes every historical file as a patch touch — the
- # failure mode that triggers gh-aw's E003 (>100 files) when a
- # new PR is opened. Fast-forward the canonical branch instead.
- # This is lossless because ahead=0 proves every commit on the
- # branch is already reachable from the default branch.
- ff = subprocess.run(
- ["git", "checkout", "-B", branch, f"origin/{default_branch}"],
- capture_output=True, text=True
- )
- if ff.returncode != 0:
- print(f" Failed to fast-forward {branch}: {ff.stderr}")
- failed.append(branch)
- continue
- # Use --force-with-lease so that if anyone else is simultaneously
- # pushing to the branch, the update is rejected rather than
- # overwriting their commits.
- push = subprocess.run(
- ["git", "push", "--force-with-lease", "origin", branch],
- capture_output=True, text=True
- )
- if push.returncode != 0:
- print(f" Failed to force-push {branch}: {push.stderr}")
- failed.append(branch)
- continue
- print(f" Fast-forwarded {branch} to origin/{default_branch}")
- continue
-
- if ahead == 0 and behind == 0:
- # Already at default branch — nothing to do.
- print(f" {branch} is already up to date with origin/{default_branch}")
- continue
-
- if ahead > 0 and behind == 0:
- # Unique work preserved; no upstream drift to merge.
- print(f" {branch} is ahead of origin/{default_branch} with no upstream drift; nothing to merge.")
- continue
-
- # True divergence (ahead > 0 and behind > 0): check out and merge.
- checkout = subprocess.run(
- ["git", "checkout", "-B", branch, f"origin/{branch}"],
- capture_output=True, text=True
- )
- if checkout.returncode != 0:
- print(f" Failed to checkout {branch}: {checkout.stderr}")
- failed.append(branch)
- continue
-
- # Merge the default branch into the program branch
- merge = subprocess.run(
- ["git", "merge", f"origin/{default_branch}", "--no-edit",
- "-m", f"Merge {default_branch} into {branch}"],
- capture_output=True, text=True
- )
- if merge.returncode != 0:
- print(f" Merge conflict or failure for {branch}: {merge.stderr}")
- # Abort the merge to leave a clean state
- subprocess.run(["git", "merge", "--abort"], capture_output=True)
- failed.append(branch)
- continue
-
- # Push the updated branch
- push = subprocess.run(
- ["git", "push", "origin", branch],
- capture_output=True, text=True
- )
- if push.returncode != 0:
- print(f" Failed to push {branch}: {push.stderr}")
- failed.append(branch)
- continue
-
- print(f" Successfully synced {branch}")
-
- # Return to default branch
- subprocess.run(["git", "checkout", default_branch], capture_output=True)
-
- if failed:
- print(f"\n⚠️ Failed to sync {len(failed)} branch(es): {failed}")
- print("These branches may need manual conflict resolution.")
- # Don't fail the workflow — log the issue but continue
- else:
- print(f"\n✅ All {len(branches)} branch(es) synced successfully.")
- PYEOF
----
-
-Sync all autoloop/* branches with the default branch.
diff --git a/AGENTS.md b/AGENTS.md
index 63d0475..1ee2120 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,7 +11,6 @@ autoloop/
├── AGENTS.md ← you are here
├── workflows/ ← Agentic Workflow definitions
│ ├── autoloop.md ← main autoloop workflow (compiled by gh-aw)
-│ ├── sync-branches.md ← syncs default branch into autoloop/* branches
│ ├── shared/ ← shared workflow fragments
│ │ └── reporting.md
│ └── scripts/ ← standalone scripts invoked from steps
@@ -72,7 +71,7 @@ The workflow (`workflows/autoloop.md`) is compiled by `gh aw compile` into `.git
6. Updates the program's state file in repo-memory with all state (Machine State table + research sections)
7. If the program has a `target-metric` and the metric is reached, marks it as completed (removes `autoloop-program` label, adds `autoloop-completed` label for issue-based programs)
-A companion workflow (`workflows/sync-branches.md`) runs on every push to the default branch and merges it into all active `autoloop/*` program branches, keeping them up to date.
+Branch freshness is handled by the iteration loop itself: each iteration's Step 3 (in `workflows/autoloop.md`, "Iteration Loop" → step "Branch Setup") fast-forwards or merges `origin/main` into the program's `autoloop/*` branch as needed. No separate sync workflow is required.
### Evolution Strategy
@@ -134,9 +133,8 @@ Programs run on a schedule, but can also be triggered manually:
To deploy the workflow to a repository:
1. Copy `workflows/autoloop.md` to `.github/workflows/autoloop.md` in the target repo
-2. Copy `workflows/sync-branches.md` to `.github/workflows/sync-branches.md` in the target repo
-3. Copy `workflows/shared/` to `.github/workflows/shared/` in the target repo
-4. Copy `workflows/scripts/` to `.github/workflows/scripts/` in the target repo
-5. Run `gh aw compile autoloop` and `gh aw compile sync-branches` to generate the lock files
-6. Copy program directories to `.autoloop/programs/` in the target repo
-7. Commit and push
+2. Copy `workflows/shared/` to `.github/workflows/shared/` in the target repo
+3. Copy `workflows/scripts/` to `.github/workflows/scripts/` in the target repo
+4. Run `gh aw compile autoloop` to generate the lock file
+5. Copy program directories to `.autoloop/programs/` in the target repo
+6. Commit and push
diff --git a/README.md b/README.md
index 79773e1..bc1693d 100644
--- a/README.md
+++ b/README.md
@@ -171,7 +171,6 @@ The command is handled by the `autoloop` Agentic Workflow — it is **not** a Gi
autoloop/
├── workflows/ ← Agentic Workflow definitions
│ ├── autoloop.md ← the workflow (compiled by gh aw)
-│ ├── sync-branches.md ← syncs default branch into autoloop/* branches
│ └── shared/
│ └── reporting.md
├── .autoloop/
diff --git a/install.md b/install.md
index 52923c9..04b7851 100644
--- a/install.md
+++ b/install.md
@@ -85,10 +85,9 @@ rm -rf /tmp/autoloop
```bash
gh aw compile autoloop
-gh aw compile sync-branches
```
-**What this does**: Generates `.github/workflows/autoloop.lock.yml` and `.github/workflows/sync-branches.lock.yml` from the workflow definitions.
+**What this does**: Generates `.github/workflows/autoloop.lock.yml` from the workflow definition.
## Step 5: Create a Branch, Commit, and Open a Pull Request
@@ -141,7 +140,7 @@ Optionally, you may copy existing examples from the [`.autoloop/programs/`](.aut
### Compile fails
-- Ensure `.github/workflows/autoloop.md` and `.github/workflows/sync-branches.md` exist
+- Ensure `.github/workflows/autoloop.md` exists
- Ensure `.github/workflows/shared/` directory was copied
- Re-run `gh aw compile autoloop` with `--verbose` for details
diff --git a/tests/test_scheduling.py b/tests/test_scheduling.py
index 7064391..6f04b88 100644
--- a/tests/test_scheduling.py
+++ b/tests/test_scheduling.py
@@ -685,92 +685,6 @@ def test_clone_before_scheduling(self):
)
-class TestSyncBranchesCredentialOrdering:
- """Verify that Git credentials are configured before the merge/push step.
-
- The sync-branches workflow merges the default branch into autoloop/*
- branches. Merge commits require a Git identity (user.name/user.email)
- and pushes/fetches need an authenticated remote URL. Both must be
- configured before the merge step runs.
- """
-
- CRED_STEP = "Set up Git identity and authentication"
- MERGE_STEP = "Merge default branch into all autoloop program branches"
-
- def _load_steps(self):
- """Return the list of pre-step names from workflows/sync-branches.md."""
- import os
-
- wf_path = os.path.join(os.path.dirname(__file__), "..", "workflows", "sync-branches.md")
- with open(wf_path) as f:
- content = f.read()
- step_names = []
- for m in re.finditer(r'^\s*-\s*name:\s*(.+)$', content, re.MULTILINE):
- step_names.append(m.group(1).strip())
- return step_names
-
- def _load_lock_steps(self):
- """Return the list of step names from the agent job in
- .github/workflows/sync-branches.lock.yml.
-
- Parsed with a regex (rather than PyYAML) so the test has no
- external dependencies beyond pytest.
- """
- import os
-
- lock_path = os.path.join(
- os.path.dirname(__file__), "..", ".github", "workflows", "sync-branches.lock.yml"
- )
- with open(lock_path) as f:
- content = f.read()
- # Restrict to the 'agent:' job body so we don't pick up step names
- # from other jobs (e.g. 'activation').
- agent_match = re.search(r"^ agent:\n((?: .*\n|\n)+)", content, re.MULTILINE)
- if not agent_match:
- return []
- agent_body = agent_match.group(1)
- # Step names appear as either ' - name: ' or
- # ' name: ' (when the step starts with '- env:').
- step_names = []
- for m in re.finditer(r'^\s{6,8}(?:- )?name:\s*(.+)$', agent_body, re.MULTILINE):
- step_names.append(m.group(1).strip())
- return step_names
-
- def test_cred_step_exists(self):
- """A step that configures Git identity/auth must exist in the source."""
- steps = self._load_steps()
- assert self.CRED_STEP in steps, (
- f"Expected step '{self.CRED_STEP}' not found. Steps: {steps}"
- )
-
- def test_creds_before_merge(self):
- """The credential step must come before the merge step in the source."""
- steps = self._load_steps()
- cred_idx = steps.index(self.CRED_STEP)
- merge_idx = steps.index(self.MERGE_STEP)
- assert cred_idx < merge_idx, (
- f"'{self.CRED_STEP}' (index {cred_idx}) must come before "
- f"'{self.MERGE_STEP}' (index {merge_idx}). Steps: {steps}"
- )
-
- def test_lock_creds_before_merge(self):
- """In the compiled lock file, Configure Git credentials must come before the merge step."""
- steps = self._load_lock_steps()
- cred_names = [s for s in steps if "Configure Git credentials" in s]
- assert cred_names, (
- f"No 'Configure Git credentials' step found in lock file. Steps: {steps}"
- )
- merge_names = [s for s in steps if "Merge default branch" in s]
- assert merge_names, (
- f"No merge step found in lock file. Steps: {steps}"
- )
- cred_idx = steps.index(cred_names[0])
- merge_idx = steps.index(merge_names[0])
- assert cred_idx < merge_idx, (
- f"'Configure Git credentials' (index {cred_idx}) must come before "
- f"merge step (index {merge_idx}). Steps: {steps}"
- )
-
# ---------------------------------------------------------------------------
# Single-PR-per-program invariant: safe-outputs config + existing_pr lookup
diff --git a/workflows/autoloop.md b/workflows/autoloop.md
index 5f14579..1b5670d 100644
--- a/workflows/autoloop.md
+++ b/workflows/autoloop.md
@@ -325,7 +325,6 @@ Examples:
4. **Rejected or errored iterations** do not commit — changes are discarded.
5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR.
6. The branch may be **merged into the default branch** at any time (by a maintainer or CI). After merging, the branch continues to be used for future iterations — it is never deleted while the program is active. On the next iteration, the branch is automatically reset to the default branch (see step 2) so that already-merged commits do not cause patch conflicts.
-7. A **sync workflow** automatically merges the default branch into all active `autoloop/*` branches whenever the default branch changes, keeping them up to date.
### Cross-Linking
diff --git a/workflows/sync-branches.md b/workflows/sync-branches.md
deleted file mode 100644
index cad1adb..0000000
--- a/workflows/sync-branches.md
+++ /dev/null
@@ -1,196 +0,0 @@
----
-description: |
- Keeps Autoloop program branches up to date with the default branch.
- Runs whenever the default branch changes and merges it into all active
- autoloop/* branches so that program iterations always build on the latest code.
-
-on:
- push:
- branches: [main] # ← update this if your default branch is not 'main'
- workflow_dispatch:
-
-permissions:
- contents: write
-
-timeout-minutes: 10
-
-tools:
- github:
- toolsets: [repos]
- bash: true
-
-steps:
- - name: Set up Git identity and authentication
- env:
- GH_TOKEN: ${{ github.token }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_SERVER_URL: ${{ github.server_url }}
- run: |
- node - << 'JSEOF'
- const { spawnSync } = require('child_process');
- function git(...args) {
- const result = spawnSync('git', args, { encoding: 'utf-8' });
- if (result.status !== 0) {
- console.error('git ' + args.join(' ') + ' failed: ' + result.stderr);
- process.exit(1);
- }
- return result;
- }
- git('config', '--global', 'user.email', 'github-actions[bot]@users.noreply.github.com');
- git('config', '--global', 'user.name', 'github-actions[bot]');
- const ghToken = process.env.GH_TOKEN || '';
- const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
- const repo = process.env.GITHUB_REPOSITORY || '';
- if (ghToken && repo) {
- const authUrl = serverUrl.replace('https://', 'https://x-access-token:' + ghToken + '@') + '/' + repo + '.git';
- git('remote', 'set-url', 'origin', authUrl);
- }
- console.log('Git identity and authentication configured.');
- JSEOF
-
- - name: Merge default branch into all autoloop program branches
- env:
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_TOKEN: ${{ github.token }}
- DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
- node - << 'JSEOF'
- const { execSync, spawnSync } = require('child_process');
-
- const defaultBranch = process.env.DEFAULT_BRANCH || 'main';
-
- function git(...args) {
- const result = spawnSync('git', args, { encoding: 'utf-8' });
- return { returncode: result.status, stdout: result.stdout || '', stderr: result.stderr || '' };
- }
-
- // Discover all remote branches matching the autoloop/* pattern.
- // Use ls-remote instead of 'git branch -r' so we don't depend on
- // pre-fetched remote-tracking refs (shallow checkouts won't have them).
- const listResult = git('ls-remote', '--heads', 'origin', 'autoloop/*');
- if (listResult.returncode !== 0) {
- console.log('Failed to list remote branches: ' + listResult.stderr);
- process.exit(0);
- }
-
- const branches = listResult.stdout.trim().split('\n')
- .map(b => b.trim())
- .filter(b => b)
- .map(b => b.replace(/^.*refs\/heads\//, ''));
-
- if (branches.length === 0) {
- console.log('No autoloop/* branches found. Nothing to sync.');
- process.exit(0);
- }
-
- console.log('Found ' + branches.length + ' autoloop branch(es) to sync: ' + JSON.stringify(branches));
-
- const failed = [];
- for (const branch of branches) {
- console.log('\n--- Syncing ' + branch + ' with ' + defaultBranch + ' ---');
-
- // Fetch both branches
- git('fetch', 'origin', branch);
- git('fetch', 'origin', defaultBranch);
-
- // Compute ahead/behind counts using the remote-tracking refs so we
- // make a decision based on commit delta (not content delta).
- const aheadResult = git('rev-list', '--count',
- 'origin/' + defaultBranch + '..origin/' + branch);
- const behindResult = git('rev-list', '--count',
- 'origin/' + branch + '..origin/' + defaultBranch);
- if (aheadResult.returncode !== 0 || behindResult.returncode !== 0) {
- console.log(' Failed to compute ahead/behind for ' + branch + ': ' +
- (aheadResult.stderr || behindResult.stderr));
- failed.push(branch);
- continue;
- }
- const ahead = parseInt((aheadResult.stdout || '0').trim(), 10) || 0;
- const behind = parseInt((behindResult.stdout || '0').trim(), 10) || 0;
- console.log(' ahead=' + ahead + ' behind=' + behind);
-
- if (ahead === 0 && behind > 0) {
- // All of the branch's commits are already in the default branch.
- // Merging would produce a noisy "Merge main into branch" commit
- // that re-exposes every historical file as a patch touch — the
- // failure mode that triggers gh-aw's E003 (>100 files) when a
- // new PR is opened. Fast-forward the canonical branch instead.
- // This is lossless because ahead=0 proves every commit on the
- // branch is already reachable from the default branch.
- const ff = git('checkout', '-B', branch, 'origin/' + defaultBranch);
- if (ff.returncode !== 0) {
- console.log(' Failed to fast-forward ' + branch + ': ' + ff.stderr);
- failed.push(branch);
- continue;
- }
- // Use --force-with-lease so that if anyone else is simultaneously
- // pushing to the branch, the update is rejected rather than
- // overwriting their commits.
- const push = git('push', '--force-with-lease', 'origin', branch);
- if (push.returncode !== 0) {
- console.log(' Failed to force-push ' + branch + ': ' + push.stderr);
- failed.push(branch);
- continue;
- }
- console.log(' Fast-forwarded ' + branch + ' to origin/' + defaultBranch);
- continue;
- }
-
- if (ahead === 0 && behind === 0) {
- // Already at default branch — nothing to do.
- console.log(' ' + branch + ' is already up to date with origin/' + defaultBranch);
- continue;
- }
-
- if (ahead > 0 && behind === 0) {
- // Unique work preserved; no upstream drift to merge.
- console.log(' ' + branch + ' is ahead of origin/' + defaultBranch + ' with no upstream drift; nothing to merge.');
- continue;
- }
-
- // True divergence (ahead > 0 && behind > 0): check out and merge.
- let checkout = git('checkout', '-B', branch, 'origin/' + branch);
- if (checkout.returncode !== 0) {
- console.log(' Failed to checkout ' + branch + ': ' + checkout.stderr);
- failed.push(branch);
- continue;
- }
-
- const merge = git('merge', 'origin/' + defaultBranch, '--no-edit',
- '-m', 'Merge ' + defaultBranch + ' into ' + branch);
- if (merge.returncode !== 0) {
- console.log(' Merge conflict or failure for ' + branch + ': ' + merge.stderr);
- // Abort the merge to leave a clean state
- git('merge', '--abort');
- failed.push(branch);
- continue;
- }
-
- // Push the updated branch
- const push = git('push', 'origin', branch);
- if (push.returncode !== 0) {
- console.log(' Failed to push ' + branch + ': ' + push.stderr);
- failed.push(branch);
- continue;
- }
-
- console.log(' Successfully synced ' + branch);
- }
-
- // Return to default branch
- git('checkout', defaultBranch);
-
- if (failed.length > 0) {
- console.log('\n\u26a0\ufe0f Failed to sync ' + failed.length + " branch(es): " + JSON.stringify(failed)); // \u26a0\ufe0f = warning sign
- console.log('These branches may need manual conflict resolution.');
- // Don't fail the workflow -- log the issue but continue
- } else {
- console.log('\n\u2705 All ' + branches.length + " branch(es) synced successfully."); // \u2705 = checkmark
- }
- JSEOF
----
-
-Sync all autoloop/* branches with the default branch.