diff --git a/.github/workflows/autoloop.lock.yml b/.github/workflows/autoloop.lock.yml
index 60a852fd..30d0e263 100644
--- a/.github/workflows/autoloop.lock.yml
+++ b/.github/workflows/autoloop.lock.yml
@@ -1,3 +1,5 @@
+# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"aace167ee0f389330d7d95d0758d2f406b7b7a63956d5869c250e1013278675a","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"}
+# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.68.3","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0"},{"image":"node:lts-alpine"}]}
# ___ _ _
# / _ \ | | (_)
# | |_| | __ _ ___ _ __ | |_ _ ___
@@ -12,7 +14,7 @@
# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
#
-# This file was automatically generated by gh-aw (v0.65.6). DO NOT EDIT.
+# This file was automatically generated by gh-aw (v0.68.3). DO NOT EDIT.
#
# To update this file, edit githubnext/autoloop and run:
# gh aw compile
@@ -29,7 +31,7 @@
# - Persists all state via repo-memory (human-readable, human-editable)
# - Commits accepted improvements to a long-running branch per program
# - Maintains a single draft PR per program that accumulates all accepted iterations
-# - Maintains a living experiment log as a GitHub issue
+# - Maintains a single GitHub issue per program where all status, iteration logs, and human steering live
#
# Source: githubnext/autoloop
#
@@ -37,7 +39,27 @@
# Imports:
# - shared/reporting.md
#
-# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"c8a09645e1afbcbd0273875049bedcf145441597aa39db659ebe1971d76c2489","compiler_version":"v0.65.6","strict":true,"agent_id":"copilot"}
+# Secrets used:
+# - GH_AW_CI_TRIGGER_TOKEN
+# - GH_AW_GITHUB_MCP_SERVER_TOKEN
+# - GH_AW_GITHUB_TOKEN
+# - GITHUB_TOKEN
+#
+# Custom actions used:
+# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+# - actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+# - github/gh-aw-actions/setup@v0.68.3
+#
+# Container images used:
+# - ghcr.io/github/gh-aw-firewall/agent:0.25.20
+# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20
+# - ghcr.io/github/gh-aw-firewall/squid:0.25.20
+# - ghcr.io/github/gh-aw-mcpg:v0.2.19
+# - ghcr.io/github/github-mcp-server:v0.32.0
+# - node:lts-alpine
name: "Autoloop"
"on":
@@ -94,6 +116,7 @@ jobs:
if: "needs.pre_activation.outputs.activated == 'true' && ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/autoloop ') || startsWith(github.event.issue.body, '/autoloop\n') || github.event.issue.body == '/autoloop') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/autoloop ') || startsWith(github.event.comment.body, '/autoloop\n') || github.event.comment.body == '/autoloop') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/autoloop ') || startsWith(github.event.comment.body, '/autoloop\n') || github.event.comment.body == '/autoloop') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/autoloop ') || startsWith(github.event.comment.body, '/autoloop\n') || github.event.comment.body == '/autoloop') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/autoloop ') || startsWith(github.event.pull_request.body, '/autoloop\n') || github.event.pull_request.body == '/autoloop') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/autoloop ') || startsWith(github.event.discussion.body, '/autoloop\n') || github.event.discussion.body == '/autoloop') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/autoloop ') || startsWith(github.event.comment.body, '/autoloop\n') || github.event.comment.body == '/autoloop')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request')) && (!(github.event_name == 'pull_request_review_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment')))"
runs-on: ubuntu-slim
permissions:
+ actions: read
contents: read
discussions: write
issues: write
@@ -105,51 +128,56 @@ jobs:
comment_url: ${{ steps.add-comment.outputs.comment-url }}
lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }}
model: ${{ steps.generate_aw_info.outputs.model }}
+ 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
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }}
- 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_VERSION: "1.0.21"
+ GH_AW_INFO_AGENT_VERSION: "1.0.21"
+ GH_AW_INFO_CLI_VERSION: "v0.68.3"
GH_AW_INFO_WORKFLOW_NAME: "Autoloop"
GH_AW_INFO_EXPERIMENTAL: "false"
GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
GH_AW_INFO_STAGED: "false"
GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","node","python","rust","java","dotnet"]'
GH_AW_INFO_FIREWALL_ENABLED: "true"
- GH_AW_INFO_AWF_VERSION: "v0.25.11"
+ GH_AW_INFO_AWF_VERSION: "v0.25.20"
GH_AW_INFO_AWMG_VERSION: ""
GH_AW_INFO_FIREWALL_TYPE: "squid"
GH_AW_COMPILED_STRICT: "true"
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs');
await main(core, context);
- 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
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);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs');
await main();
- name: Checkout .github and .agents folders
@@ -161,45 +189,47 @@ jobs:
.agents
sparse-checkout-cone-mode: true
fetch-depth: 1
- - name: Check workflow file timestamps
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ - name: Check workflow lock file
+ id: check-lock-file
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_WORKFLOW_FILE: "autoloop.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);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
- GH_AW_COMPILED_VERSION: "v0.65.6"
+ GH_AW_COMPILED_VERSION: "v0.68.3"
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_WORKFLOW_NAME: "Autoloop"
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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
@@ -220,23 +250,23 @@ jobs:
GH_AW_WIKI_NOTE: ${{ '' }}
# poutine:ignore untrusted_checkout_exec
run: |
- bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh
+ bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
- cat << 'GH_AW_PROMPT_37fc23637021c26a_EOF'
+ cat << 'GH_AW_PROMPT_f6f83fa4ec7fd0bf_EOF'
- GH_AW_PROMPT_37fc23637021c26a_EOF
+ GH_AW_PROMPT_f6f83fa4ec7fd0bf_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_37fc23637021c26a_EOF'
+ cat << 'GH_AW_PROMPT_f6f83fa4ec7fd0bf_EOF'
Tools: add_comment(max:7), create_issue(max:2), update_issue(max:3), create_pull_request, add_labels(max:2), remove_labels(max:2), push_to_pull_request_branch, missing_tool, missing_data, noop
- GH_AW_PROMPT_37fc23637021c26a_EOF
+ GH_AW_PROMPT_f6f83fa4ec7fd0bf_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_37fc23637021c26a_EOF'
+ cat << 'GH_AW_PROMPT_f6f83fa4ec7fd0bf_EOF'
The following GitHub context information is available for this workflow:
@@ -269,7 +299,7 @@ jobs:
- **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 without proper authentication. 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).
- GH_AW_PROMPT_37fc23637021c26a_EOF
+ GH_AW_PROMPT_f6f83fa4ec7fd0bf_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"
@@ -277,14 +307,14 @@ jobs:
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_37fc23637021c26a_EOF'
+ cat << 'GH_AW_PROMPT_f6f83fa4ec7fd0bf_EOF'
{{#runtime-import .github/workflows/shared/reporting.md}}
{{#runtime-import .github/workflows/autoloop.md}}
- GH_AW_PROMPT_37fc23637021c26a_EOF
+ GH_AW_PROMPT_f6f83fa4ec7fd0bf_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
@@ -294,11 +324,11 @@ jobs:
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_GITHUB_ACTOR: ${{ github.actor }}
@@ -323,7 +353,7 @@ jobs:
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs');
@@ -356,20 +386,22 @@ jobs:
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
+ 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
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh"
- name: Upload activation artifact
if: success()
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: activation
path: |
/tmp/gh-aw/aw_info.json
/tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/github_rate_limits.jsonl
+ if-no-files-found: ignore
retention-days: 1
agent:
@@ -399,24 +431,33 @@ jobs:
GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
GH_AW_WORKFLOW_ID_SANITIZED: autoloop
outputs:
+ agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }}
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-inference-error.outputs.inference_access_error || 'false' }}
+ inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }}
+ mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }}
model: ${{ needs.activation.outputs.model }}
+ model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }}
output: ${{ steps.collect_output.outputs.output }}
output_types: ${{ steps.collect_output.outputs.output_types }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
steps:
- name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
- name: Set runtime paths
id: set-runtime-paths
run: |
- echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" >> "$GITHUB_OUTPUT"
- echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" >> "$GITHUB_OUTPUT"
- echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" >> "$GITHUB_OUTPUT"
+ {
+ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -428,18 +469,27 @@ jobs:
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: Setup Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: '3.12'
- name: Create gh-aw temp directory
- run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh
+ 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
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh"
env:
GH_TOKEN: ${{ github.token }}
+ - env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_TOKEN: ${{ github.token }}
+ name: Clone repo-memory for scheduler
+ run: "mkdir -p /tmp/gh-aw/repo-memory\nif [ -d /tmp/gh-aw/repo-memory/autoloop/.git ]; then\n echo \"repo-memory/autoloop already cloned; skipping\"\nelse\n # Pass the token via an http.extraHeader rather than embedding it in the\n # URL — keeps it out of process listings and any logged remote URLs.\n AUTH_HEADER=\"Authorization: Basic $(printf 'x-access-token:%s' \"${GITHUB_TOKEN}\" | base64 -w0)\"\n git -c \"http.extraHeader=${AUTH_HEADER}\" clone --depth=1 --branch memory/autoloop \\\n \"https://github.com/${GITHUB_REPOSITORY}.git\" \\\n /tmp/gh-aw/repo-memory/autoloop \\\n || {\n echo \"memory/autoloop branch not found (first run); creating empty dir\"\n mkdir -p /tmp/gh-aw/repo-memory/autoloop\n }\nfi\n"
- env:
AUTOLOOP_PROGRAM: ${{ github.event.inputs.program }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ github.token }}
name: Check which programs are due
- run: "python3 - << 'PYEOF'\nimport os, json, re, glob, sys\nimport urllib.request, urllib.error\nfrom datetime import datetime, timezone, timedelta\n\nprograms_dir = \".autoloop/programs\"\nautoloop_dir = \".autoloop/programs\"\ntemplate_file = os.path.join(autoloop_dir, \"example.md\")\n\n# Read program state from repo-memory (persistent git-backed storage)\ngithub_token = os.environ.get(\"GITHUB_TOKEN\", \"\")\nrepo = os.environ.get(\"GITHUB_REPOSITORY\", \"\")\nforced_program = os.environ.get(\"AUTOLOOP_PROGRAM\", \"\").strip()\n\n# Repo-memory files are cloned to /tmp/gh-aw/repo-memory/{id}/ where {id}\n# is derived from the branch-name configured in the tools section (memory/autoloop → autoloop)\nrepo_memory_dir = \"/tmp/gh-aw/repo-memory/autoloop\"\n\ndef parse_machine_state(content):\n \"\"\"Parse the ⚙️ Machine State table from a state file. Returns a dict.\"\"\"\n state = {}\n m = re.search(r'## ⚙️ Machine State.*?\\n(.*?)(?=\\n## |\\Z)', content, re.DOTALL)\n if not m:\n return state\n section = m.group(0)\n for row in re.finditer(r'\\|\\s*(.+?)\\s*\\|\\s*(.+?)\\s*\\|', section):\n raw_key = row.group(1).strip()\n raw_val = row.group(2).strip()\n if raw_key.lower() in (\"field\", \"---\", \":---\", \":---:\", \"---:\"):\n continue\n key = raw_key.lower().replace(\" \", \"_\")\n val = None if raw_val in (\"—\", \"-\", \"\") else raw_val\n state[key] = val\n # Coerce types\n for int_field in (\"iteration_count\", \"consecutive_errors\"):\n if int_field in state:\n try:\n state[int_field] = int(state[int_field])\n except (ValueError, TypeError):\n state[int_field] = 0\n if \"paused\" in state:\n state[\"paused\"] = str(state.get(\"paused\", \"\")).lower() == \"true\"\n if \"completed\" in state:\n state[\"completed\"] = str(state.get(\"completed\", \"\")).lower() == \"true\"\n # recent_statuses: stored as comma-separated words (e.g. \"accepted, rejected, error\")\n rs_raw = state.get(\"recent_statuses\") or \"\"\n if rs_raw:\n state[\"recent_statuses\"] = [s.strip().lower() for s in rs_raw.split(\",\") if s.strip()]\n else:\n state[\"recent_statuses\"] = []\n return state\n\ndef read_program_state(program_name):\n \"\"\"Read scheduling state from the repo-memory state file.\"\"\"\n state_file = os.path.join(repo_memory_dir, f\"{program_name}.md\")\n if not os.path.isfile(state_file):\n print(f\" {program_name}: no state file found (first run)\")\n return {}\n with open(state_file, encoding=\"utf-8\") as f:\n content = f.read()\n return parse_machine_state(content)\n\n# Bootstrap: create autoloop programs directory and template if missing\nif not os.path.isdir(autoloop_dir):\n os.makedirs(autoloop_dir, exist_ok=True)\n bt = chr(96) # backtick — avoid literal backticks that break gh-aw compiler\n template = \"\\n\".join([\n \"\",\n \"\",\n \"\",\n \"\",\n \"# Autoloop Program\",\n \"\",\n \"\",\n \"\",\n \"## Goal\",\n \"\",\n \"\",\n \"\",\n \"REPLACE THIS with your optimization goal.\",\n \"\",\n \"## Target\",\n \"\",\n \"\",\n \"\",\n \"Only modify these files:\",\n f\"- {bt}REPLACE_WITH_FILE{bt} -- (describe what this file does)\",\n \"\",\n \"Do NOT modify:\",\n \"- (list files that must not be touched)\",\n \"\",\n \"## Evaluation\",\n \"\",\n \"\",\n \"\",\n f\"{bt}{bt}{bt}bash\",\n \"REPLACE_WITH_YOUR_EVALUATION_COMMAND\",\n f\"{bt}{bt}{bt}\",\n \"\",\n f\"The metric is {bt}REPLACE_WITH_METRIC_NAME{bt}. **Lower/Higher is better.** (pick one)\",\n \"\",\n ])\n with open(template_file, \"w\") as f:\n f.write(template)\n # Leave the template unstaged — the agent will create a draft PR with it\n print(f\"BOOTSTRAPPED: created {template_file} locally (agent will create a draft PR)\")\n\n# Find all program files from all locations:\n# 1. Directory-based programs: .autoloop/programs//program.md (preferred)\n# 2. Bare markdown programs: .autoloop/programs/.md (simple)\n# 3. Issue-based programs: GitHub issues with the 'autoloop-program' label\nprogram_files = []\nissue_programs = {} # name -> {issue_number, file}\n\n# Scan .autoloop/programs/ for directory-based programs\nif os.path.isdir(programs_dir):\n for entry in sorted(os.listdir(programs_dir)):\n prog_dir = os.path.join(programs_dir, entry)\n if os.path.isdir(prog_dir):\n # Look for program.md inside the directory\n prog_file = os.path.join(prog_dir, \"program.md\")\n if os.path.isfile(prog_file):\n program_files.append(prog_file)\n\n# Scan .autoloop/programs/ for bare markdown programs\nbare_programs = sorted(glob.glob(os.path.join(autoloop_dir, \"*.md\")))\nfor pf in bare_programs:\n program_files.append(pf)\n\n# Scan GitHub issues with the 'autoloop-program' label\nissue_programs_dir = \"/tmp/gh-aw/issue-programs\"\nos.makedirs(issue_programs_dir, exist_ok=True)\ntry:\n api_url = f\"https://api.github.com/repos/{repo}/issues?labels=autoloop-program&state=open&per_page=100\"\n req = urllib.request.Request(api_url, headers={\n \"Authorization\": f\"token {github_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n })\n with urllib.request.urlopen(req, timeout=30) as resp:\n issues = json.loads(resp.read().decode())\n for issue in issues:\n if issue.get(\"pull_request\"):\n continue # skip PRs\n body = issue.get(\"body\") or \"\"\n title = issue.get(\"title\") or \"\"\n number = issue[\"number\"]\n # Derive program name from issue title: slugify to lowercase with hyphens\n slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')\n slug = re.sub(r'-+', '-', slug) # collapse consecutive hyphens\n if not slug:\n slug = f\"issue-{number}\"\n # Avoid slug collisions: if another issue already claimed this slug, append issue number\n if slug in issue_programs:\n print(f\" Warning: slug '{slug}' (issue #{number}) collides with issue #{issue_programs[slug]['issue_number']}, appending issue number\")\n slug = f\"{slug}-{number}\"\n # Write issue body to a temp file so the scheduling loop can process it\n issue_file = os.path.join(issue_programs_dir, f\"{slug}.md\")\n with open(issue_file, \"w\") as f:\n f.write(body)\n program_files.append(issue_file)\n issue_programs[slug] = {\"issue_number\": number, \"file\": issue_file, \"title\": title}\n print(f\" Found issue-based program: '{slug}' (issue #{number})\")\nexcept Exception as e:\n print(f\" Warning: could not fetch issue-based programs: {e}\")\n\nif not program_files:\n # Fallback to single-file locations\n for path in [\".autoloop/program.md\", \"program.md\"]:\n if os.path.isfile(path):\n program_files = [path]\n break\n\nif not program_files:\n print(\"NO_PROGRAMS_FOUND\")\n os.makedirs(\"/tmp/gh-aw\", exist_ok=True)\n with open(\"/tmp/gh-aw/autoloop.json\", \"w\") as f:\n json.dump({\"due\": [], \"skipped\": [], \"unconfigured\": [], \"no_programs\": True}, f)\n sys.exit(0)\n\nos.makedirs(\"/tmp/gh-aw\", exist_ok=True)\nnow = datetime.now(timezone.utc)\ndue = []\nskipped = []\nunconfigured = []\nall_programs = {} # name -> file path (populated during scanning)\n\n# Schedule string to timedelta\ndef parse_schedule(s):\n s = s.strip().lower()\n m = re.match(r\"every\\s+(\\d+)\\s*h\", s)\n if m:\n return timedelta(hours=int(m.group(1)))\n m = re.match(r\"every\\s+(\\d+)\\s*m\", s)\n if m:\n return timedelta(minutes=int(m.group(1)))\n if s == \"daily\":\n return timedelta(hours=24)\n if s == \"weekly\":\n return timedelta(days=7)\n return None # No per-program schedule — always due\n\ndef get_program_name(pf):\n \"\"\"Extract program name from file path.\n Directory-based: .autoloop/programs//program.md -> \n Bare markdown: .autoloop/programs/.md -> \n Issue-based: /tmp/gh-aw/issue-programs/.md -> \n \"\"\"\n if pf.endswith(\"/program.md\"):\n # Directory-based program: name is the parent directory\n return os.path.basename(os.path.dirname(pf))\n else:\n # Bare markdown or issue-based program: name is the filename without .md\n return os.path.splitext(os.path.basename(pf))[0]\n\nfor pf in program_files:\n name = get_program_name(pf)\n all_programs[name] = pf\n with open(pf) as f:\n content = f.read()\n\n # Check sentinel (skip for issue-based programs which use AUTOLOOP:ISSUE-PROGRAM)\n if \"\" in content:\n unconfigured.append(name)\n continue\n\n # Check for TODO/REPLACE placeholders\n if re.search(r'\\bTODO\\b|\\bREPLACE', content):\n unconfigured.append(name)\n continue\n\n # Parse optional YAML frontmatter for schedule and target-metric\n # Strip leading HTML comments before checking (issue-based programs may have them)\n content_stripped = re.sub(r'^(\\s*\\s*\\n)*', '', content, flags=re.DOTALL)\n schedule_delta = None\n target_metric = None\n fm_match = re.match(r\"^---\\s*\\n(.*?)\\n---\\s*\\n\", content_stripped, re.DOTALL)\n if fm_match:\n for line in fm_match.group(1).split(\"\\n\"):\n if line.strip().startswith(\"schedule:\"):\n schedule_str = line.split(\":\", 1)[1].strip()\n schedule_delta = parse_schedule(schedule_str)\n if line.strip().startswith(\"target-metric:\"):\n try:\n target_metric = float(line.split(\":\", 1)[1].strip())\n except (ValueError, TypeError):\n print(f\" Warning: {name} has invalid target-metric value: {line.split(':', 1)[1].strip()}\")\n\n # Read state from repo-memory\n state = read_program_state(name)\n if state:\n print(f\" {name}: last_run={state.get('last_run')}, iteration_count={state.get('iteration_count')}\")\n else:\n print(f\" {name}: no state found (first run)\")\n\n last_run = None\n lr = state.get(\"last_run\")\n if lr:\n try:\n last_run = datetime.fromisoformat(lr.replace(\"Z\", \"+00:00\"))\n except ValueError:\n pass\n\n # Check if completed (target metric was reached)\n if str(state.get(\"completed\", \"\")).lower() == \"true\":\n skipped.append({\"name\": name, \"reason\": f\"completed: target metric reached\"})\n continue\n\n # Check if paused (e.g., plateau or recurring errors)\n if state.get(\"paused\"):\n skipped.append({\"name\": name, \"reason\": f\"paused: {state.get('pause_reason', 'unknown')}\"})\n continue\n\n # Auto-pause on plateau: 5+ consecutive rejections\n recent = state.get(\"recent_statuses\", [])[-5:]\n if len(recent) >= 5 and all(s == \"rejected\" for s in recent):\n skipped.append({\"name\": name, \"reason\": \"plateau: 5 consecutive rejections\"})\n continue\n\n # Check if due based on per-program schedule\n if schedule_delta and last_run:\n if now - last_run < schedule_delta:\n skipped.append({\"name\": name, \"reason\": \"not due yet\",\n \"next_due\": (last_run + schedule_delta).isoformat()})\n continue\n\n due.append({\"name\": name, \"last_run\": lr, \"file\": pf, \"target_metric\": target_metric})\n\n# Pick the program to run\nselected = None\nselected_file = None\nselected_issue = None\nselected_target_metric = None\ndeferred = []\n\nif forced_program:\n # Manual dispatch requested a specific program — bypass scheduling\n # (paused, not-due, and plateau programs can still be forced)\n if forced_program not in all_programs:\n print(f\"ERROR: requested program '{forced_program}' not found.\")\n print(f\" Available programs: {list(all_programs.keys())}\")\n sys.exit(1)\n if forced_program in unconfigured:\n print(f\"ERROR: requested program '{forced_program}' is unconfigured (has placeholders).\")\n sys.exit(1)\n selected = forced_program\n selected_file = all_programs[forced_program]\n deferred = [p[\"name\"] for p in due if p[\"name\"] != forced_program]\n if selected in issue_programs:\n selected_issue = issue_programs[selected][\"issue_number\"]\n # Find target_metric: check the due list first, then parse from the program file\n for p in due:\n if p[\"name\"] == forced_program:\n selected_target_metric = p.get(\"target_metric\")\n break\n if selected_target_metric is None:\n # Program may have been skipped (completed/paused/plateau) — parse directly\n try:\n with open(selected_file) as _f:\n _content = _f.read()\n _content_stripped = re.sub(r'^(\\s*\\s*\\n)*', '', _content, flags=re.DOTALL)\n _fm = re.match(r\"^---\\s*\\n(.*?)\\n---\\s*\\n\", _content_stripped, re.DOTALL)\n if _fm:\n for _line in _fm.group(1).split(\"\\n\"):\n if _line.strip().startswith(\"target-metric:\"):\n selected_target_metric = float(_line.split(\":\", 1)[1].strip())\n break\n except (OSError, ValueError, TypeError):\n pass\n print(f\"FORCED: running program '{forced_program}' (manual dispatch)\")\nelif due:\n # Normal scheduling: pick the single most-overdue program\n due.sort(key=lambda p: p[\"last_run\"] or \"\") # None/empty sorts first (never run)\n selected = due[0][\"name\"]\n selected_file = due[0][\"file\"]\n selected_target_metric = due[0].get(\"target_metric\")\n deferred = [p[\"name\"] for p in due[1:]]\n # Check if the selected program is issue-based\n if selected in issue_programs:\n selected_issue = issue_programs[selected][\"issue_number\"]\n\n# Look up existing PR for the selected program's canonical branch\nexisting_pr = None\nhead_branch = None\n\ndef verify_pr_is_open(pr_number):\n \"\"\"Check if a PR is still open via the GitHub API. Returns True if open.\"\"\"\n try:\n verify_url = f\"https://api.github.com/repos/{repo}/pulls/{pr_number}\"\n verify_req = urllib.request.Request(verify_url, headers={\n \"Authorization\": f\"token {github_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n })\n with urllib.request.urlopen(verify_req, timeout=30) as verify_resp:\n pr_data = json.loads(verify_resp.read().decode())\n return pr_data.get(\"state\") == \"open\"\n except Exception:\n return True # If we can't verify, assume it's open (best effort)\n\nif selected:\n head_branch = f\"autoloop/{selected}\"\n owner = repo.split(\"/\")[0] if \"/\" in repo else \"\"\n if owner:\n # Strategy 1: exact branch match (works when branch has no framework suffix)\n try:\n pr_api_url = (\n f\"https://api.github.com/repos/{repo}/pulls\"\n f\"?state=open&head={owner}:{head_branch}&per_page=5\"\n )\n pr_req = urllib.request.Request(pr_api_url, headers={\n \"Authorization\": f\"token {github_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n })\n with urllib.request.urlopen(pr_req, timeout=30) as pr_resp:\n open_prs = json.loads(pr_resp.read().decode())\n if open_prs:\n existing_pr = open_prs[0][\"number\"]\n print(f\" Found existing PR #{existing_pr} for exact branch {head_branch}\")\n except Exception as e:\n print(f\" Warning: could not check for existing PRs by exact branch: {e}\")\n\n # Strategy 2: search by title and branch prefix (catches framework-generated\n # hash suffixes like autoloop/name-a1b2c3d4e5f6g7h8 created by create-pull-request)\n if existing_pr is None:\n try:\n title_marker = f\"[Autoloop: {selected}]\"\n branch_prefix = head_branch # e.g. autoloop/perf-comparison\n list_url = (\n f\"https://api.github.com/repos/{repo}/pulls\"\n f\"?state=open&per_page=100&sort=created&direction=desc\"\n )\n list_req = urllib.request.Request(list_url, headers={\n \"Authorization\": f\"token {github_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n })\n with urllib.request.urlopen(list_req, timeout=30) as list_resp:\n all_open_prs = json.loads(list_resp.read().decode())\n # Match branch names: exact canonical name or canonical + framework hash suffix\n branch_pattern = re.compile(r'^' + re.escape(branch_prefix) + r'(-[0-9a-f]{16})?$')\n for pr in all_open_prs:\n pr_title = pr.get(\"title\", \"\")\n pr_head_ref = pr.get(\"head\", {}).get(\"ref\", \"\")\n if title_marker in pr_title or branch_pattern.match(pr_head_ref):\n existing_pr = pr[\"number\"]\n print(f\" Found existing PR #{existing_pr} by title/branch-prefix (branch: {pr_head_ref})\")\n break\n if existing_pr is None:\n print(f\" No existing PR found for program {selected}\")\n except Exception as e:\n print(f\" Warning: could not search for existing PRs by title/prefix: {e}\")\n else:\n print(f\" Warning: could not parse owner from GITHUB_REPOSITORY='{repo}'\")\n\n # Strategy 3: check the state file for a recorded PR number as fallback\n if existing_pr is None:\n state = read_program_state(selected)\n pr_field = state.get(\"pr\") or \"\"\n pr_match = re.match(r'^#?(\\d+)$', pr_field.strip())\n if pr_match:\n pr_num = int(pr_match.group(1))\n if verify_pr_is_open(pr_num):\n existing_pr = pr_num\n print(f\" Found open PR #{existing_pr} from state file for {selected}\")\n else:\n print(f\" PR #{pr_num} from state file is no longer open — ignoring\")\n\nresult = {\n \"selected\": selected,\n \"selected_file\": selected_file,\n \"selected_issue\": selected_issue,\n \"selected_target_metric\": selected_target_metric,\n \"existing_pr\": existing_pr,\n \"head_branch\": head_branch,\n \"issue_programs\": {name: info[\"issue_number\"] for name, info in issue_programs.items()},\n \"deferred\": deferred,\n \"skipped\": skipped,\n \"unconfigured\": unconfigured,\n \"no_programs\": False,\n}\n\nos.makedirs(\"/tmp/gh-aw\", exist_ok=True)\nwith open(\"/tmp/gh-aw/autoloop.json\", \"w\") as f:\n json.dump(result, f, indent=2)\n\nprint(\"=== Autoloop Program Check ===\")\nprint(f\"Selected program: {selected or '(none)'} ({selected_file or 'n/a'})\")\nif existing_pr:\n print(f\"Existing PR: #{existing_pr} (branch: {head_branch})\")\nelse:\n print(f\"Existing PR: (none — will create on first accepted iteration)\")\nprint(f\"Deferred (next run): {deferred or '(none)'}\")\nprint(f\"Programs skipped: {[s['name'] for s in skipped] or '(none)'}\")\nprint(f\"Programs unconfigured: {unconfigured or '(none)'}\")\n\nif not selected and not unconfigured:\n print(\"\\nNo programs due this run. Exiting early.\")\n sys.exit(1) # Non-zero exit skips the agent step\nPYEOF\n"
+ run: python3 .github/workflows/scripts/autoloop_scheduler.py
# Repo memory git-based storage configuration from frontmatter processed below
- name: Clone repo-memory branch (default)
@@ -450,40 +500,43 @@ jobs:
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
+ 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"
+ 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
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
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);
+ 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: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21
+ env:
+ GH_HOST: github.com
- name: Install AWF binary
- run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20
- name: Determine automatic lockdown mode for GitHub MCP Server
id: determine-automatic-lockdown
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
@@ -492,293 +545,317 @@ jobs:
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 node:lts-alpine
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20 ghcr.io/github/gh-aw-mcpg:v0.2.19 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine
- name: Write Safe Outputs Config
run: |
- mkdir -p ${RUNNER_TEMP}/gh-aw/safeoutputs
+ 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_5d66c20dadf66eac_EOF'
- {"add_comment":{"hide_older_comments":false,"max":7,"target":"*"},"add_labels":{"max":2,"target":"*"},"create_issue":{"labels":["automation","autoloop"],"max":2,"title_prefix":"[Autoloop] "},"create_pull_request":{"draft":true,"labels":["automation","autoloop"],"max":1,"max_patch_size":1024,"preserve_branch_name":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"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Autoloop] "},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":30720,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Autoloop] "},"remove_labels":{"max":2,"target":"*"},"update_issue":{"allow_body":true,"max":3,"target":"*","title_prefix":"[Autoloop] "}}
- GH_AW_SAFE_OUTPUTS_CONFIG_5d66c20dadf66eac_EOF
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_68e10f13448bfacd_EOF'
+ {"add_comment":{"hide_older_comments":false,"max":7,"target":"*"},"add_labels":{"max":2,"target":"*"},"create_issue":{"labels":["automation","autoloop"],"max":2,"title_prefix":"[Autoloop] "},"create_pull_request":{"draft":true,"labels":["automation","autoloop"],"max":1,"max_patch_size":1024,"preserve_branch_name":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"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Autoloop] "},"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":30720,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Autoloop] "},"remove_labels":{"max":2,"target":"*"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":3,"target":"*","title_prefix":"[Autoloop] "}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_68e10f13448bfacd_EOF
- name: Write Safe Outputs Tools
- run: |
- cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/tools_meta.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_META_16ef9bfed78a922a_EOF'
- {
- "description_suffixes": {
- "add_comment": " CONSTRAINTS: Maximum 7 comment(s) can be added. Target: *.",
- "add_labels": " CONSTRAINTS: Maximum 2 label(s) can be added. Target: *.",
- "create_issue": " CONSTRAINTS: Maximum 2 issue(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added.",
- "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added. PRs will be created as drafts.",
- "push_to_pull_request_branch": " CONSTRAINTS: Maximum 1 push(es) can be made. The target pull request title must start with \"[Autoloop] \".",
- "remove_labels": " CONSTRAINTS: Maximum 2 label(s) can be removed. Target: *.",
- "update_issue": " CONSTRAINTS: Maximum 3 issue(s) can be updated. Target: *. The target issue title must start with \"[Autoloop] \"."
- },
- "repo_params": {},
- "dynamic_tools": []
- }
- GH_AW_SAFE_OUTPUTS_TOOLS_META_16ef9bfed78a922a_EOF
- cat > ${RUNNER_TEMP}/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_3b6ed30d6c4c74b9_EOF'
- {
- "add_comment": {
- "defaultMax": 1,
- "fields": {
- "body": {
- "required": true,
- "type": "string",
- "sanitize": true,
- "maxLength": 65000
- },
- "item_number": {
- "issueOrPRNumber": true
- },
- "repo": {
- "type": "string",
- "maxLength": 256
+ env:
+ GH_AW_TOOLS_META_JSON: |
+ {
+ "description_suffixes": {
+ "add_comment": " CONSTRAINTS: Maximum 7 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.",
+ "add_labels": " CONSTRAINTS: Maximum 2 label(s) can be added. Target: *.",
+ "create_issue": " CONSTRAINTS: Maximum 2 issue(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added.",
+ "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Autoloop] \". Labels [\"automation\" \"autoloop\"] will be automatically added. PRs will be created as drafts.",
+ "push_to_pull_request_branch": " CONSTRAINTS: Maximum 1 push(es) can be made. The target pull request title must start with \"[Autoloop] \".",
+ "remove_labels": " CONSTRAINTS: Maximum 2 label(s) can be removed. Target: *.",
+ "update_issue": " CONSTRAINTS: Maximum 3 issue(s) can be updated. Target: *. The target issue title must start with \"[Autoloop] \"."
+ },
+ "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
+ },
+ "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
- },
- "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_issue": {
+ "defaultMax": 1,
+ "fields": {
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "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": {
- "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
+ },
+ "create_pull_request": {
+ "defaultMax": 1,
+ "fields": {
+ "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_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
+ },
+ "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
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
}
- }
- },
- "push_to_pull_request_branch": {
- "defaultMax": 1,
- "fields": {
- "branch": {
- "required": true,
- "type": "string",
- "sanitize": true,
- "maxLength": 256
- },
- "message": {
- "required": true,
- "type": "string",
- "sanitize": true,
- "maxLength": 65000
- },
- "pull_request_number": {
- "issueOrPRNumber": true
+ },
+ "push_to_pull_request_branch": {
+ "defaultMax": 1,
+ "fields": {
+ "branch": {
+ "required": true,
+ "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
+ },
+ "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
+ }
}
- }
- },
- "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
+ },
+ "report_incomplete": {
+ "defaultMax": 5,
+ "fields": {
+ "details": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 1024
+ }
}
},
- "customValidation": "requiresOneOf:status,title,body"
+ "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"
+ }
}
- }
- GH_AW_SAFE_OUTPUTS_VALIDATION_3b6ed30d6c4c74b9_EOF
- node ${RUNNER_TEMP}/gh-aw/actions/generate_safe_outputs_tools.cjs
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ 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: |
@@ -817,7 +894,7 @@ jobs:
export GH_AW_SAFE_OUTPUTS_CONFIG_PATH
export GH_AW_MCP_LOG_DIR
- bash ${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh
+ bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh"
- name: Start MCP Gateway
id: start-mcp-gateway
@@ -844,10 +921,10 @@ jobs:
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 -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.2.11'
+ 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 -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.2.19'
mkdir -p /home/runner/.copilot
- cat << GH_AW_MCP_CONFIG_7abd76cd2350f90f_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh
+ cat << GH_AW_MCP_CONFIG_3a09067f39430b25_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh"
{
"mcpServers": {
"github": {
@@ -888,7 +965,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
- GH_AW_MCP_CONFIG_7abd76cd2350f90f_EOF
+ GH_AW_MCP_CONFIG_3a09067f39430b25_EOF
- name: Download activation artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -896,7 +973,7 @@ jobs:
path: /tmp/gh-aw
- name: Clean git credentials
continue-on-error: true
- run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh"
- name: Execute GitHub Copilot CLI
id: agentic_execution
# Copilot CLI tool arguments (sorted):
@@ -904,9 +981,10 @@ jobs:
run: |
set -o pipefail
touch /tmp/gh-aw/agent-step-summary.md
+ (umask 177 && touch /tmp/gh-aw/agent-stdio.log)
# 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 '*.gradle-enterprise.cloud,*.jsr.io,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --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
+ 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 '*.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' --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.20 --skip-pull --enable-api-proxy \
+ -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.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 "$(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: ${{ github.token }}
@@ -915,7 +993,7 @@ jobs:
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_VERSION: v0.65.6
+ GH_AW_VERSION: v0.68.3
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_AW: true
GITHUB_HEAD_REF: ${{ github.head_ref }}
@@ -930,27 +1008,28 @@ jobs:
GIT_COMMITTER_NAME: github-actions[bot]
S2STOKENS: true
XDG_CONFIG_HOME: /home/runner
- - name: Detect inference access error
- id: detect-inference-error
+ - name: Detect Copilot errors
+ id: detect-copilot-errors
if: always()
continue-on-error: true
- run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh
+ run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_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"
+ 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
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh"
- name: Stop MCP Gateway
if: always()
continue-on-error: true
@@ -959,14 +1038,14 @@ jobs:
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"
+ 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
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs');
await main();
env:
@@ -976,7 +1055,7 @@ jobs:
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
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh"
- name: Copy Safe Outputs
if: always()
env:
@@ -987,38 +1066,38 @@ jobs:
- name: Ingest agent output
id: collect_output
if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
- GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.jsr.io,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com"
+ 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_COMMAND: autoloop
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
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);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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
@@ -1039,7 +1118,13 @@ jobs:
- name: Parse token usage for step summary
if: always()
continue-on-error: true
- run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_token_usage.sh
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ 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: Write agent output placeholder if missing
if: always()
run: |
@@ -1049,7 +1134,7 @@ jobs:
# Upload repo memory as artifacts for push job
- name: Upload repo-memory artifact (default)
if: always()
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: repo-memory-default
path: /tmp/gh-aw/repo-memory/default
@@ -1058,7 +1143,7 @@ jobs:
- name: Upload agent artifacts
if: always()
continue-on-error: true
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: agent
path: |
@@ -1066,20 +1151,14 @@ jobs:
/tmp/gh-aw/sandbox/agent/logs/
/tmp/gh-aw/redacted-urls.log
/tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/agent_usage.json
/tmp/gh-aw/agent-stdio.log
/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
- 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
@@ -1091,7 +1170,9 @@ jobs:
- detection
- push_repo_memory
- safe_outputs
- if: always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true')
+ if: >
+ always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
+ needs.activation.outputs.stale_lock_file_failed == 'true')
runs-on: ubuntu-slim
permissions:
contents: write
@@ -1102,14 +1183,18 @@ jobs:
group: "gh-aw-conclusion-autoloop"
cancel-in-progress: false
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
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
- name: Download agent output artifact
id: download-agent-output
continue-on-error: true
@@ -1124,9 +1209,9 @@ jobs:
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: Process No-Op Messages
+ - name: Process no-op messages
id: noop
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_NOOP_MAX: "1"
@@ -1139,12 +1224,29 @@ jobs:
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);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs');
await main();
- - name: Record Missing Tool
+ - name: Log detection run
+ id: detection_runs
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Autoloop"
+ GH_AW_WORKFLOW_SOURCE: "githubnext/autoloop"
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_MISSING_TOOL_CREATE_ISSUE: "true"
@@ -1154,13 +1256,28 @@ jobs:
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);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs');
await main();
- - name: Handle Agent Failure
+ - name: Record incomplete
+ id: report_incomplete
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
+ 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: "Autoloop"
+ GH_AW_WORKFLOW_SOURCE: "githubnext/autoloop"
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Autoloop"
@@ -1171,9 +1288,13 @@ jobs:
GH_AW_ENGINE_ID: "copilot"
GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
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_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_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 }}
@@ -1185,12 +1306,12 @@ jobs:
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);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }}
@@ -1199,16 +1320,19 @@ jobs:
GH_AW_WORKFLOW_NAME: "Autoloop"
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
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);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/notify_comment_error.cjs');
await main();
detection:
- needs: agent
+ needs:
+ - activation
+ - agent
if: >
always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true')
runs-on: ubuntu-latest
@@ -1217,12 +1341,16 @@ jobs:
copilot-requests: write
outputs:
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
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
- name: Download agent output artifact
id: download-agent-output
continue-on-error: true
@@ -1243,8 +1371,12 @@ jobs:
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.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20 ghcr.io/github/gh-aw-firewall/squid:0.25.20
- name: Check if detection needed
id: detection_guard
if: always()
@@ -1281,15 +1413,15 @@ jobs:
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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
WORKFLOW_NAME: "Autoloop"
- WORKFLOW_DESCRIPTION: "An iterative optimization loop inspired by Karpathy's Autoresearch and Claude Code's /loop.\nRuns on a configurable schedule to autonomously improve a target artifact toward a measurable goal.\nEach iteration: reads the program definition, proposes a change, evaluates against a metric,\nand accepts or rejects the change. Tracks all iterations in a rolling GitHub issue.\n- User defines the optimization goal and evaluation criteria in a program.md file\n- Accepts changes only when they improve the metric (ratchet pattern)\n- Persists all state via repo-memory (human-readable, human-editable)\n- Commits accepted improvements to a long-running branch per program\n- Maintains a single draft PR per program that accumulates all accepted iterations\n- Maintains a living experiment log as a GitHub issue"
+ WORKFLOW_DESCRIPTION: "An iterative optimization loop inspired by Karpathy's Autoresearch and Claude Code's /loop.\nRuns on a configurable schedule to autonomously improve a target artifact toward a measurable goal.\nEach iteration: reads the program definition, proposes a change, evaluates against a metric,\nand accepts or rejects the change. Tracks all iterations in a rolling GitHub issue.\n- User defines the optimization goal and evaluation criteria in a program.md file\n- Accepts changes only when they improve the metric (ratchet pattern)\n- Persists all state via repo-memory (human-readable, human-editable)\n- Commits accepted improvements to a long-running branch per program\n- Maintains a single draft PR per program that accumulates all accepted iterations\n- Maintains a single GitHub issue per program where all status, iteration logs, and human steering live"
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);
+ 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
@@ -1298,9 +1430,11 @@ jobs:
mkdir -p /tmp/gh-aw/threat-detection
touch /tmp/gh-aw/threat-detection/detection.log
- name: Install GitHub Copilot CLI
- run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21
+ env:
+ GH_HOST: github.com
- name: Install AWF binary
- run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20
- name: Execute GitHub Copilot CLI
if: always() && steps.detection_guard.outputs.run_detection == 'true'
id: detection_agentic_execution
@@ -1309,16 +1443,17 @@ jobs:
run: |
set -o pipefail
touch /tmp/gh-aw/agent-step-summary.md
+ (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log)
# 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 --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.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 --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
+ 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 --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.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.20 --skip-pull --enable-api-proxy \
+ -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.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 "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
env:
COPILOT_AGENT_RUNNER_TYPE: STANDALONE
COPILOT_GITHUB_TOKEN: ${{ github.token }}
COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }}
GH_AW_PHASE: detection
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
- GH_AW_VERSION: v0.65.6
+ GH_AW_VERSION: v0.68.3
GITHUB_API_URL: ${{ github.api_url }}
GITHUB_AW: true
GITHUB_HEAD_REF: ${{ github.head_ref }}
@@ -1334,7 +1469,7 @@ jobs:
XDG_CONFIG_HOME: /home/runner
- name: Upload threat detection log
if: always() && steps.detection_guard.outputs.run_detection == 'true'
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: detection
path: /tmp/gh-aw/threat-detection/detection.log
@@ -1342,13 +1477,14 @@ jobs:
- name: Parse and conclude threat detection
id: detection_conclusion
if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
+ GH_AW_DETECTION_CONTINUE_ON_ERROR: "true"
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs');
await main();
@@ -1358,40 +1494,46 @@ jobs:
outputs:
activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_command_position.outputs.command_position_ok == 'true' }}
matched_command: ${{ steps.check_command_position.outputs.matched_command }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
steps:
- name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
- name: Check team membership for command workflow
id: check_membership
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
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);
+ 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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_COMMANDS: "[\"autoloop\"]"
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/check_command_position.cjs');
await main();
push_repo_memory:
needs:
+ - activation
- agent
- detection
- if: always() && (needs.detection.result == 'success' || needs.detection.result == 'skipped')
+ if: >
+ always() && (!cancelled()) && (needs.detection.result == 'success' || needs.detection.result == 'skipped') &&
+ needs.agent.result != 'skipped'
runs-on: ubuntu-slim
permissions:
contents: write
@@ -1404,9 +1546,12 @@ jobs:
validation_failed_default: ${{ steps.push_repo_memory_default.outputs.validation_failed }}
steps:
- name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -1416,13 +1561,14 @@ jobs:
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"
+ 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
@@ -1433,7 +1579,7 @@ jobs:
- name: Push repo-memory changes (default)
id: push_repo_memory_default
if: always()
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -1450,7 +1596,7 @@ jobs:
with:
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ setupGlobals(core, github, context, exec, io, getOctokit);
const { main } = require('${{ runner.temp }}/gh-aw/actions/push_repo_memory.cjs');
await main();
@@ -1469,6 +1615,8 @@ jobs:
timeout-minutes: 15
env:
GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/autoloop"
+ 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 }}
@@ -1492,9 +1640,12 @@ jobs:
push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }}
steps:
- name: Setup Scripts
- uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6
+ id: setup
+ uses: github/gh-aw-actions/setup@v0.68.3
with:
destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
- name: Download agent output artifact
id: download-agent-output
continue-on-error: true
@@ -1548,26 +1699,28 @@ jobs:
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Process Safe Outputs
id: process_safe_outputs
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
env:
GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
- GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.jsr.io,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com"
+ 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\":false,\"max\":7,\"target\":\"*\"},\"add_labels\":{\"max\":2,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"autoloop\"],\"max\":2,\"title_prefix\":\"[Autoloop] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"autoloop\"],\"max\":1,\"max_patch_size\":1024,\"preserve_branch_name\":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\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Autoloop] \"},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"},\"remove_labels\":{\"max\":2,\"target\":\"*\"},\"update_issue\":{\"allow_body\":true,\"max\":3,\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"}}"
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":false,\"max\":7,\"target\":\"*\"},\"add_labels\":{\"max\":2,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"autoloop\"],\"max\":2,\"title_prefix\":\"[Autoloop] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"autoloop\"],\"max\":1,\"max_patch_size\":1024,\"preserve_branch_name\":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\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Autoloop] \"},\"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\":1,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"},\"remove_labels\":{\"max\":2,\"target\":\"*\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":3,\"target\":\"*\",\"title_prefix\":\"[Autoloop] \"}}"
GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
- setupGlobals(core, github, context, exec, io);
+ 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 Output Items
+ - name: Upload Safe Outputs Items
if: always()
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: safe-output-items
- path: /tmp/gh-aw/safe-output-items.jsonl
+ 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/autoloop.md b/.github/workflows/autoloop.md
index 73a78c83..a4182229 100644
--- a/.github/workflows/autoloop.md
+++ b/.github/workflows/autoloop.md
@@ -117,515 +117,7 @@ steps:
GITHUB_REPOSITORY: ${{ github.repository }}
AUTOLOOP_PROGRAM: ${{ github.event.inputs.program }}
run: |
- python3 - << 'PYEOF'
- import os, json, re, glob, sys
- import urllib.request, urllib.error
- from datetime import datetime, timezone, timedelta
-
- programs_dir = ".autoloop/programs"
- autoloop_dir = ".autoloop/programs"
- template_file = os.path.join(autoloop_dir, "example.md")
-
- # Read program state from repo-memory (persistent git-backed storage)
- github_token = os.environ.get("GITHUB_TOKEN", "")
- repo = os.environ.get("GITHUB_REPOSITORY", "")
- forced_program = os.environ.get("AUTOLOOP_PROGRAM", "").strip()
-
- # Repo-memory files are cloned to /tmp/gh-aw/repo-memory/{id}/ where {id}
- # is derived from the branch-name configured in the tools section (memory/autoloop → autoloop)
- repo_memory_dir = "/tmp/gh-aw/repo-memory/autoloop"
-
- def parse_machine_state(content):
- """Parse the ⚙️ Machine State table from a state file. Returns a dict."""
- state = {}
- m = re.search(r'## ⚙️ Machine State.*?\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
- if not m:
- return state
- section = m.group(0)
- for row in re.finditer(r'\|\s*(.+?)\s*\|\s*(.+?)\s*\|', section):
- raw_key = row.group(1).strip()
- raw_val = row.group(2).strip()
- if raw_key.lower() in ("field", "---", ":---", ":---:", "---:"):
- continue
- key = raw_key.lower().replace(" ", "_")
- val = None if raw_val in ("—", "-", "") else raw_val
- state[key] = val
- # Coerce types
- for int_field in ("iteration_count", "consecutive_errors"):
- if int_field in state:
- try:
- state[int_field] = int(state[int_field])
- except (ValueError, TypeError):
- state[int_field] = 0
- if "paused" in state:
- state["paused"] = str(state.get("paused", "")).lower() == "true"
- if "completed" in state:
- state["completed"] = str(state.get("completed", "")).lower() == "true"
- # recent_statuses: stored as comma-separated words (e.g. "accepted, rejected, error")
- rs_raw = state.get("recent_statuses") or ""
- if rs_raw:
- state["recent_statuses"] = [s.strip().lower() for s in rs_raw.split(",") if s.strip()]
- else:
- state["recent_statuses"] = []
- return state
-
- def read_program_state(program_name):
- """Read scheduling state from the repo-memory state file."""
- state_file = os.path.join(repo_memory_dir, f"{program_name}.md")
- if not os.path.isfile(state_file):
- print(f" {program_name}: no state file found (first run)")
- return {}
- with open(state_file, encoding="utf-8") as f:
- content = f.read()
- return parse_machine_state(content)
-
- # Bootstrap: create autoloop programs directory and template if missing
- if not os.path.isdir(autoloop_dir):
- os.makedirs(autoloop_dir, exist_ok=True)
- bt = chr(96) # backtick — avoid literal backticks that break gh-aw compiler
- template = "\n".join([
- "",
- "",
- "",
- "",
- "# Autoloop Program",
- "",
- "",
- "",
- "## Goal",
- "",
- "",
- "",
- "REPLACE THIS with your optimization goal.",
- "",
- "## Target",
- "",
- "",
- "",
- "Only modify these files:",
- f"- {bt}REPLACE_WITH_FILE{bt} -- (describe what this file does)",
- "",
- "Do NOT modify:",
- "- (list files that must not be touched)",
- "",
- "## Evaluation",
- "",
- "",
- "",
- f"{bt}{bt}{bt}bash",
- "REPLACE_WITH_YOUR_EVALUATION_COMMAND",
- f"{bt}{bt}{bt}",
- "",
- f"The metric is {bt}REPLACE_WITH_METRIC_NAME{bt}. **Lower/Higher is better.** (pick one)",
- "",
- ])
- with open(template_file, "w") as f:
- f.write(template)
- # Leave the template unstaged — the agent will create a draft PR with it
- print(f"BOOTSTRAPPED: created {template_file} locally (agent will create a draft PR)")
-
- # Find all program files from all locations:
- # 1. Directory-based programs: .autoloop/programs//program.md (preferred)
- # 2. Bare markdown programs: .autoloop/programs/.md (simple)
- # 3. Issue-based programs: GitHub issues with the 'autoloop-program' label
- program_files = []
- issue_programs = {} # name -> {issue_number, file}
-
- # Scan .autoloop/programs/ for directory-based programs
- if os.path.isdir(programs_dir):
- for entry in sorted(os.listdir(programs_dir)):
- prog_dir = os.path.join(programs_dir, entry)
- if os.path.isdir(prog_dir):
- # Look for program.md inside the directory
- prog_file = os.path.join(prog_dir, "program.md")
- if os.path.isfile(prog_file):
- program_files.append(prog_file)
-
- # Scan .autoloop/programs/ for bare markdown programs
- bare_programs = sorted(glob.glob(os.path.join(autoloop_dir, "*.md")))
- for pf in bare_programs:
- program_files.append(pf)
-
- # Scan GitHub issues with the 'autoloop-program' label.
- # Each program (file-based or issue-based) has exactly one such issue —
- # it serves as the single source of truth for status, iteration log, and
- # human steering. For file-based programs the issue is auto-created by
- # the agent on first run with title "[Autoloop: {program-name}]".
- issue_programs_dir = "/tmp/gh-aw/issue-programs"
- os.makedirs(issue_programs_dir, exist_ok=True)
- # file_program_issues: name -> issue_number for file-based program issues
- # (auto-created by the agent, recognized here by title "[Autoloop: {name}]").
- file_program_issues = {}
- file_program_titles = set() # known file-based program names (to skip when slugifying)
- try:
- api_url = f"https://api.github.com/repos/{repo}/issues?labels=autoloop-program&state=open&per_page=100"
- req = urllib.request.Request(api_url, headers={
- "Authorization": f"token {github_token}",
- "Accept": "application/vnd.github.v3+json",
- })
- with urllib.request.urlopen(req, timeout=30) as resp:
- issues = json.loads(resp.read().decode())
- # First pass: identify file-based program issues by their conventional title.
- # We compute the set of known file-based program names from program_files first.
- known_file_program_names = set()
- for pf in program_files:
- # inline get_program_name (it's defined later in this script)
- if pf.endswith("/program.md"):
- known_file_program_names.add(os.path.basename(os.path.dirname(pf)))
- else:
- known_file_program_names.add(os.path.splitext(os.path.basename(pf))[0])
- file_program_issue_pattern = re.compile(r'^\s*\[Autoloop:\s*([^\]]+?)\s*\]\s*$')
- consumed_issue_numbers = set()
- for issue in issues:
- if issue.get("pull_request"):
- continue
- title = issue.get("title") or ""
- m = file_program_issue_pattern.match(title)
- if m and m.group(1) in known_file_program_names:
- file_program_issues[m.group(1)] = issue["number"]
- consumed_issue_numbers.add(issue["number"])
- print(f" Found program issue for file-based program '{m.group(1)}': #{issue['number']}")
-
- # Second pass: any remaining autoloop-program issue is an issue-based program.
- for issue in issues:
- if issue.get("pull_request"):
- continue # skip PRs
- if issue["number"] in consumed_issue_numbers:
- continue # already claimed as a file-based program's issue
- body = issue.get("body") or ""
- title = issue.get("title") or ""
- number = issue["number"]
- # Derive program name from issue title: slugify to lowercase with hyphens
- slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
- slug = re.sub(r'-+', '-', slug) # collapse consecutive hyphens
- if not slug:
- slug = f"issue-{number}"
- # Avoid slug collisions: if another issue already claimed this slug, append issue number
- if slug in issue_programs:
- print(f" Warning: slug '{slug}' (issue #{number}) collides with issue #{issue_programs[slug]['issue_number']}, appending issue number")
- slug = f"{slug}-{number}"
- # Write issue body to a temp file so the scheduling loop can process it
- issue_file = os.path.join(issue_programs_dir, f"{slug}.md")
- with open(issue_file, "w") as f:
- f.write(body)
- program_files.append(issue_file)
- issue_programs[slug] = {"issue_number": number, "file": issue_file, "title": title}
- print(f" Found issue-based program: '{slug}' (issue #{number})")
- except Exception as e:
- print(f" Warning: could not fetch issue-based programs: {e}")
-
- if not program_files:
- # Fallback to single-file locations
- for path in [".autoloop/program.md", "program.md"]:
- if os.path.isfile(path):
- program_files = [path]
- break
-
- if not program_files:
- print("NO_PROGRAMS_FOUND")
- os.makedirs("/tmp/gh-aw", exist_ok=True)
- with open("/tmp/gh-aw/autoloop.json", "w") as f:
- json.dump({"due": [], "skipped": [], "unconfigured": [], "no_programs": True}, f)
- sys.exit(0)
-
- os.makedirs("/tmp/gh-aw", exist_ok=True)
- now = datetime.now(timezone.utc)
- due = []
- skipped = []
- unconfigured = []
- all_programs = {} # name -> file path (populated during scanning)
-
- # Schedule string to timedelta
- def parse_schedule(s):
- s = s.strip().lower()
- m = re.match(r"every\s+(\d+)\s*h", s)
- if m:
- return timedelta(hours=int(m.group(1)))
- m = re.match(r"every\s+(\d+)\s*m", s)
- if m:
- return timedelta(minutes=int(m.group(1)))
- if s == "daily":
- return timedelta(hours=24)
- if s == "weekly":
- return timedelta(days=7)
- return None # No per-program schedule — always due
-
- def get_program_name(pf):
- """Extract program name from file path.
- Directory-based: .autoloop/programs//program.md ->
- Bare markdown: .autoloop/programs/.md ->
- Issue-based: /tmp/gh-aw/issue-programs/.md ->
- """
- if pf.endswith("/program.md"):
- # Directory-based program: name is the parent directory
- return os.path.basename(os.path.dirname(pf))
- else:
- # Bare markdown or issue-based program: name is the filename without .md
- return os.path.splitext(os.path.basename(pf))[0]
-
- for pf in program_files:
- name = get_program_name(pf)
- all_programs[name] = pf
- with open(pf) as f:
- content = f.read()
-
- # Check sentinel (skip for issue-based programs which use AUTOLOOP:ISSUE-PROGRAM)
- if "" in content:
- unconfigured.append(name)
- continue
-
- # Check for TODO/REPLACE placeholders
- if re.search(r'\bTODO\b|\bREPLACE', content):
- unconfigured.append(name)
- continue
-
- # Parse optional YAML frontmatter for schedule and target-metric
- # Strip leading HTML comments before checking (issue-based programs may have them)
- content_stripped = re.sub(r'^(\s*\s*\n)*', '', content, flags=re.DOTALL)
- schedule_delta = None
- target_metric = None
- fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content_stripped, re.DOTALL)
- if fm_match:
- for line in fm_match.group(1).split("\n"):
- if line.strip().startswith("schedule:"):
- schedule_str = line.split(":", 1)[1].strip()
- schedule_delta = parse_schedule(schedule_str)
- if line.strip().startswith("target-metric:"):
- try:
- target_metric = float(line.split(":", 1)[1].strip())
- except (ValueError, TypeError):
- print(f" Warning: {name} has invalid target-metric value: {line.split(':', 1)[1].strip()}")
-
- # Read state from repo-memory
- state = read_program_state(name)
- if state:
- print(f" {name}: last_run={state.get('last_run')}, iteration_count={state.get('iteration_count')}")
- else:
- print(f" {name}: no state found (first run)")
-
- last_run = None
- lr = state.get("last_run")
- if lr:
- try:
- last_run = datetime.fromisoformat(lr.replace("Z", "+00:00"))
- except ValueError:
- pass
-
- # Check if completed (target metric was reached)
- if str(state.get("completed", "")).lower() == "true":
- skipped.append({"name": name, "reason": f"completed: target metric reached"})
- continue
-
- # Check if paused (e.g., plateau or recurring errors)
- if state.get("paused"):
- skipped.append({"name": name, "reason": f"paused: {state.get('pause_reason', 'unknown')}"})
- continue
-
- # Auto-pause on plateau: 5+ consecutive rejections
- recent = state.get("recent_statuses", [])[-5:]
- if len(recent) >= 5 and all(s == "rejected" for s in recent):
- skipped.append({"name": name, "reason": "plateau: 5 consecutive rejections"})
- continue
-
- # Check if due based on per-program schedule
- if schedule_delta and last_run:
- if now - last_run < schedule_delta:
- skipped.append({"name": name, "reason": "not due yet",
- "next_due": (last_run + schedule_delta).isoformat()})
- continue
-
- due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric,
- "schedule_seconds": schedule_delta.total_seconds() if schedule_delta else None})
-
- # Pick the program to run
- selected = None
- selected_file = None
- selected_issue = None
- selected_target_metric = None
- deferred = []
-
- if forced_program:
- # Manual dispatch requested a specific program — bypass scheduling
- # (paused, not-due, and plateau programs can still be forced)
- if forced_program not in all_programs:
- print(f"ERROR: requested program '{forced_program}' not found.")
- print(f" Available programs: {list(all_programs.keys())}")
- sys.exit(1)
- if forced_program in unconfigured:
- print(f"ERROR: requested program '{forced_program}' is unconfigured (has placeholders).")
- sys.exit(1)
- selected = forced_program
- selected_file = all_programs[forced_program]
- deferred = [p["name"] for p in due if p["name"] != forced_program]
- if selected in issue_programs:
- selected_issue = issue_programs[selected]["issue_number"]
- elif selected in file_program_issues:
- # File-based program with an auto-created program issue.
- selected_issue = file_program_issues[selected]
- # Find target_metric: check the due list first, then parse from the program file
- for p in due:
- if p["name"] == forced_program:
- selected_target_metric = p.get("target_metric")
- break
- if selected_target_metric is None:
- # Program may have been skipped (completed/paused/plateau) — parse directly
- try:
- with open(selected_file) as _f:
- _content = _f.read()
- _content_stripped = re.sub(r'^(\s*\s*\n)*', '', _content, flags=re.DOTALL)
- _fm = re.match(r"^---\s*\n(.*?)\n---\s*\n", _content_stripped, re.DOTALL)
- if _fm:
- for _line in _fm.group(1).split("\n"):
- if _line.strip().startswith("target-metric:"):
- selected_target_metric = float(_line.split(":", 1)[1].strip())
- break
- except (OSError, ValueError, TypeError):
- pass
- print(f"FORCED: running program '{forced_program}' (manual dispatch)")
- elif due:
- # Normal scheduling: pick the single most-overdue program.
- # Tiebreaker rationale: programs that have never run (no last_run) take
- # priority over ever-run programs; among never-run programs, prefer the
- # shortest schedule (so "every 30m" beats "every 6h"), then alphabetical
- # by name. Programs with no parseable schedule sort last among never-run
- # programs (float('inf')). This avoids permanent starvation when state
- # is missing — see issue: "Autoloop pre-step can't read state files".
- def _due_sort_key(p):
- if p["last_run"]:
- return (1, p["last_run"], p["name"])
- sched = p.get("schedule_seconds")
- return (0, sched if sched is not None else float("inf"), p["name"])
- due.sort(key=_due_sort_key)
- selected = due[0]["name"]
- selected_file = due[0]["file"]
- selected_target_metric = due[0].get("target_metric")
- deferred = [p["name"] for p in due[1:]]
- # Check if the selected program is issue-based, or a file-based program
- # with an auto-created program issue.
- if selected in issue_programs:
- selected_issue = issue_programs[selected]["issue_number"]
- elif selected in file_program_issues:
- selected_issue = file_program_issues[selected]
-
- # Look up existing PR for the selected program's canonical branch
- existing_pr = None
- head_branch = None
-
- def verify_pr_is_open(pr_number):
- """Check if a PR is still open via the GitHub API. Returns True if open."""
- try:
- verify_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
- verify_req = urllib.request.Request(verify_url, headers={
- "Authorization": f"token {github_token}",
- "Accept": "application/vnd.github.v3+json",
- })
- with urllib.request.urlopen(verify_req, timeout=30) as verify_resp:
- pr_data = json.loads(verify_resp.read().decode())
- return pr_data.get("state") == "open"
- except Exception:
- return True # If we can't verify, assume it's open (best effort)
-
- if selected:
- head_branch = f"autoloop/{selected}"
- owner = repo.split("/")[0] if "/" in repo else ""
- if owner:
- # Strategy 1: exact branch match (works when branch has no framework suffix)
- try:
- pr_api_url = (
- f"https://api.github.com/repos/{repo}/pulls"
- f"?state=open&head={owner}:{head_branch}&per_page=5"
- )
- pr_req = urllib.request.Request(pr_api_url, headers={
- "Authorization": f"token {github_token}",
- "Accept": "application/vnd.github.v3+json",
- })
- with urllib.request.urlopen(pr_req, timeout=30) as pr_resp:
- open_prs = json.loads(pr_resp.read().decode())
- if open_prs:
- existing_pr = open_prs[0]["number"]
- print(f" Found existing PR #{existing_pr} for exact branch {head_branch}")
- except Exception as e:
- print(f" Warning: could not check for existing PRs by exact branch: {e}")
-
- # Strategy 2: search by title and branch prefix (catches framework-generated
- # hash suffixes like autoloop/name-a1b2c3d4e5f6g7h8 created by create-pull-request)
- if existing_pr is None:
- try:
- title_marker = f"[Autoloop: {selected}]"
- branch_prefix = head_branch # e.g. autoloop/perf-comparison
- list_url = (
- f"https://api.github.com/repos/{repo}/pulls"
- f"?state=open&per_page=100&sort=created&direction=desc"
- )
- list_req = urllib.request.Request(list_url, headers={
- "Authorization": f"token {github_token}",
- "Accept": "application/vnd.github.v3+json",
- })
- with urllib.request.urlopen(list_req, timeout=30) as list_resp:
- all_open_prs = json.loads(list_resp.read().decode())
- # Match branch names: exact canonical name or canonical + framework hash suffix
- branch_pattern = re.compile(r'^' + re.escape(branch_prefix) + r'(-[0-9a-f]{16})?$')
- for pr in all_open_prs:
- pr_title = pr.get("title", "")
- pr_head_ref = pr.get("head", {}).get("ref", "")
- if title_marker in pr_title or branch_pattern.match(pr_head_ref):
- existing_pr = pr["number"]
- print(f" Found existing PR #{existing_pr} by title/branch-prefix (branch: {pr_head_ref})")
- break
- if existing_pr is None:
- print(f" No existing PR found for program {selected}")
- except Exception as e:
- print(f" Warning: could not search for existing PRs by title/prefix: {e}")
- else:
- print(f" Warning: could not parse owner from GITHUB_REPOSITORY='{repo}'")
-
- # Strategy 3: check the state file for a recorded PR number as fallback
- if existing_pr is None:
- state = read_program_state(selected)
- pr_field = state.get("pr") or ""
- pr_match = re.match(r'^#?(\d+)$', pr_field.strip())
- if pr_match:
- pr_num = int(pr_match.group(1))
- if verify_pr_is_open(pr_num):
- existing_pr = pr_num
- print(f" Found open PR #{existing_pr} from state file for {selected}")
- else:
- print(f" PR #{pr_num} from state file is no longer open — ignoring")
-
- result = {
- "selected": selected,
- "selected_file": selected_file,
- "selected_issue": selected_issue,
- "selected_target_metric": selected_target_metric,
- "existing_pr": existing_pr,
- "head_branch": head_branch,
- "issue_programs": {name: info["issue_number"] for name, info in issue_programs.items()},
- "deferred": deferred,
- "skipped": skipped,
- "unconfigured": unconfigured,
- "no_programs": False,
- }
-
- os.makedirs("/tmp/gh-aw", exist_ok=True)
- with open("/tmp/gh-aw/autoloop.json", "w") as f:
- json.dump(result, f, indent=2)
-
- print("=== Autoloop Program Check ===")
- print(f"Selected program: {selected or '(none)'} ({selected_file or 'n/a'})")
- if existing_pr:
- print(f"Existing PR: #{existing_pr} (branch: {head_branch})")
- else:
- print(f"Existing PR: (none — will create on first accepted iteration)")
- print(f"Deferred (next run): {deferred or '(none)'}")
- print(f"Programs skipped: {[s['name'] for s in skipped] or '(none)'}")
- print(f"Programs unconfigured: {unconfigured or '(none)'}")
-
- if not selected and not unconfigured:
- print("\nNo programs due this run. Exiting early.")
- sys.exit(1) # Non-zero exit skips the agent step
- PYEOF
+ python3 .github/workflows/scripts/autoloop_scheduler.py
source: githubnext/autoloop
engine: copilot
diff --git a/.github/workflows/scripts/autoloop_scheduler.py b/.github/workflows/scripts/autoloop_scheduler.py
new file mode 100644
index 00000000..64395293
--- /dev/null
+++ b/.github/workflows/scripts/autoloop_scheduler.py
@@ -0,0 +1,515 @@
+#!/usr/bin/env python3
+"""Autoloop scheduler pre-step.
+
+Picks the next program to run (or detects unconfigured programs) and writes
+`/tmp/gh-aw/autoloop.json` for the agent step. Extracted from `autoloop.md`'s
+inline heredoc because the compiled `run:` expression exceeded GitHub
+Actions' 20.5 KB per-expression limit.
+"""
+import os, json, re, glob, sys
+import urllib.request, urllib.error
+from datetime import datetime, timezone, timedelta
+
+programs_dir = ".autoloop/programs"
+autoloop_dir = ".autoloop/programs"
+template_file = os.path.join(autoloop_dir, "example.md")
+
+# Read program state from repo-memory (persistent git-backed storage)
+github_token = os.environ.get("GITHUB_TOKEN", "")
+repo = os.environ.get("GITHUB_REPOSITORY", "")
+forced_program = os.environ.get("AUTOLOOP_PROGRAM", "").strip()
+
+# Repo-memory files are cloned to /tmp/gh-aw/repo-memory/{id}/ where {id}
+# is derived from the branch-name configured in the tools section (memory/autoloop → autoloop)
+repo_memory_dir = "/tmp/gh-aw/repo-memory/autoloop"
+
+def parse_machine_state(content):
+ """Parse the ⚙️ Machine State table from a state file. Returns a dict."""
+ state = {}
+ m = re.search(r'## ⚙️ Machine State.*?\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
+ if not m:
+ return state
+ section = m.group(0)
+ for row in re.finditer(r'\|\s*(.+?)\s*\|\s*(.+?)\s*\|', section):
+ raw_key = row.group(1).strip()
+ raw_val = row.group(2).strip()
+ if raw_key.lower() in ("field", "---", ":---", ":---:", "---:"):
+ continue
+ key = raw_key.lower().replace(" ", "_")
+ val = None if raw_val in ("—", "-", "") else raw_val
+ state[key] = val
+ # Coerce types
+ for int_field in ("iteration_count", "consecutive_errors"):
+ if int_field in state:
+ try:
+ state[int_field] = int(state[int_field])
+ except (ValueError, TypeError):
+ state[int_field] = 0
+ if "paused" in state:
+ state["paused"] = str(state.get("paused", "")).lower() == "true"
+ if "completed" in state:
+ state["completed"] = str(state.get("completed", "")).lower() == "true"
+ # recent_statuses: stored as comma-separated words (e.g. "accepted, rejected, error")
+ rs_raw = state.get("recent_statuses") or ""
+ if rs_raw:
+ state["recent_statuses"] = [s.strip().lower() for s in rs_raw.split(",") if s.strip()]
+ else:
+ state["recent_statuses"] = []
+ return state
+
+def read_program_state(program_name):
+ """Read scheduling state from the repo-memory state file."""
+ state_file = os.path.join(repo_memory_dir, f"{program_name}.md")
+ if not os.path.isfile(state_file):
+ print(f" {program_name}: no state file found (first run)")
+ return {}
+ with open(state_file, encoding="utf-8") as f:
+ content = f.read()
+ return parse_machine_state(content)
+
+# Bootstrap: create autoloop programs directory and template if missing
+if not os.path.isdir(autoloop_dir):
+ os.makedirs(autoloop_dir, exist_ok=True)
+ bt = chr(96) # backtick — avoid literal backticks that break gh-aw compiler
+ template = "\n".join([
+ "",
+ "",
+ "",
+ "",
+ "# Autoloop Program",
+ "",
+ "",
+ "",
+ "## Goal",
+ "",
+ "",
+ "",
+ "REPLACE THIS with your optimization goal.",
+ "",
+ "## Target",
+ "",
+ "",
+ "",
+ "Only modify these files:",
+ f"- {bt}REPLACE_WITH_FILE{bt} -- (describe what this file does)",
+ "",
+ "Do NOT modify:",
+ "- (list files that must not be touched)",
+ "",
+ "## Evaluation",
+ "",
+ "",
+ "",
+ f"{bt}{bt}{bt}bash",
+ "REPLACE_WITH_YOUR_EVALUATION_COMMAND",
+ f"{bt}{bt}{bt}",
+ "",
+ f"The metric is {bt}REPLACE_WITH_METRIC_NAME{bt}. **Lower/Higher is better.** (pick one)",
+ "",
+ ])
+ with open(template_file, "w") as f:
+ f.write(template)
+ # Leave the template unstaged — the agent will create a draft PR with it
+ print(f"BOOTSTRAPPED: created {template_file} locally (agent will create a draft PR)")
+
+# Find all program files from all locations:
+# 1. Directory-based programs: .autoloop/programs//program.md (preferred)
+# 2. Bare markdown programs: .autoloop/programs/.md (simple)
+# 3. Issue-based programs: GitHub issues with the 'autoloop-program' label
+program_files = []
+issue_programs = {} # name -> {issue_number, file}
+
+# Scan .autoloop/programs/ for directory-based programs
+if os.path.isdir(programs_dir):
+ for entry in sorted(os.listdir(programs_dir)):
+ prog_dir = os.path.join(programs_dir, entry)
+ if os.path.isdir(prog_dir):
+ # Look for program.md inside the directory
+ prog_file = os.path.join(prog_dir, "program.md")
+ if os.path.isfile(prog_file):
+ program_files.append(prog_file)
+
+# Scan .autoloop/programs/ for bare markdown programs
+bare_programs = sorted(glob.glob(os.path.join(autoloop_dir, "*.md")))
+for pf in bare_programs:
+ program_files.append(pf)
+
+# Scan GitHub issues with the 'autoloop-program' label.
+# Each program (file-based or issue-based) has exactly one such issue —
+# it serves as the single source of truth for status, iteration log, and
+# human steering. For file-based programs the issue is auto-created by
+# the agent on first run with title "[Autoloop: {program-name}]".
+issue_programs_dir = "/tmp/gh-aw/issue-programs"
+os.makedirs(issue_programs_dir, exist_ok=True)
+# file_program_issues: name -> issue_number for file-based program issues
+# (auto-created by the agent, recognized here by title "[Autoloop: {name}]").
+file_program_issues = {}
+file_program_titles = set() # known file-based program names (to skip when slugifying)
+try:
+ api_url = f"https://api.github.com/repos/{repo}/issues?labels=autoloop-program&state=open&per_page=100"
+ req = urllib.request.Request(api_url, headers={
+ "Authorization": f"token {github_token}",
+ "Accept": "application/vnd.github.v3+json",
+ })
+ with urllib.request.urlopen(req, timeout=30) as resp:
+ issues = json.loads(resp.read().decode())
+ # First pass: identify file-based program issues by their conventional title.
+ # We compute the set of known file-based program names from program_files first.
+ known_file_program_names = set()
+ for pf in program_files:
+ # inline get_program_name (it's defined later in this script)
+ if pf.endswith("/program.md"):
+ known_file_program_names.add(os.path.basename(os.path.dirname(pf)))
+ else:
+ known_file_program_names.add(os.path.splitext(os.path.basename(pf))[0])
+ file_program_issue_pattern = re.compile(r'^\s*\[Autoloop:\s*([^\]]+?)\s*\]\s*$')
+ consumed_issue_numbers = set()
+ for issue in issues:
+ if issue.get("pull_request"):
+ continue
+ title = issue.get("title") or ""
+ m = file_program_issue_pattern.match(title)
+ if m and m.group(1) in known_file_program_names:
+ file_program_issues[m.group(1)] = issue["number"]
+ consumed_issue_numbers.add(issue["number"])
+ print(f" Found program issue for file-based program '{m.group(1)}': #{issue['number']}")
+
+ # Second pass: any remaining autoloop-program issue is an issue-based program.
+ for issue in issues:
+ if issue.get("pull_request"):
+ continue # skip PRs
+ if issue["number"] in consumed_issue_numbers:
+ continue # already claimed as a file-based program's issue
+ body = issue.get("body") or ""
+ title = issue.get("title") or ""
+ number = issue["number"]
+ # Derive program name from issue title: slugify to lowercase with hyphens
+ slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
+ slug = re.sub(r'-+', '-', slug) # collapse consecutive hyphens
+ if not slug:
+ slug = f"issue-{number}"
+ # Avoid slug collisions: if another issue already claimed this slug, append issue number
+ if slug in issue_programs:
+ print(f" Warning: slug '{slug}' (issue #{number}) collides with issue #{issue_programs[slug]['issue_number']}, appending issue number")
+ slug = f"{slug}-{number}"
+ # Write issue body to a temp file so the scheduling loop can process it
+ issue_file = os.path.join(issue_programs_dir, f"{slug}.md")
+ with open(issue_file, "w") as f:
+ f.write(body)
+ program_files.append(issue_file)
+ issue_programs[slug] = {"issue_number": number, "file": issue_file, "title": title}
+ print(f" Found issue-based program: '{slug}' (issue #{number})")
+except Exception as e:
+ print(f" Warning: could not fetch issue-based programs: {e}")
+
+if not program_files:
+ # Fallback to single-file locations
+ for path in [".autoloop/program.md", "program.md"]:
+ if os.path.isfile(path):
+ program_files = [path]
+ break
+
+if not program_files:
+ print("NO_PROGRAMS_FOUND")
+ os.makedirs("/tmp/gh-aw", exist_ok=True)
+ with open("/tmp/gh-aw/autoloop.json", "w") as f:
+ json.dump({"due": [], "skipped": [], "unconfigured": [], "no_programs": True}, f)
+ sys.exit(0)
+
+os.makedirs("/tmp/gh-aw", exist_ok=True)
+now = datetime.now(timezone.utc)
+due = []
+skipped = []
+unconfigured = []
+all_programs = {} # name -> file path (populated during scanning)
+
+# Schedule string to timedelta
+def parse_schedule(s):
+ s = s.strip().lower()
+ m = re.match(r"every\s+(\d+)\s*h", s)
+ if m:
+ return timedelta(hours=int(m.group(1)))
+ m = re.match(r"every\s+(\d+)\s*m", s)
+ if m:
+ return timedelta(minutes=int(m.group(1)))
+ if s == "daily":
+ return timedelta(hours=24)
+ if s == "weekly":
+ return timedelta(days=7)
+ return None # No per-program schedule — always due
+
+def get_program_name(pf):
+ """Extract program name from file path.
+ Directory-based: .autoloop/programs//program.md ->
+ Bare markdown: .autoloop/programs/.md ->
+ Issue-based: /tmp/gh-aw/issue-programs/.md ->
+ """
+ if pf.endswith("/program.md"):
+ # Directory-based program: name is the parent directory
+ return os.path.basename(os.path.dirname(pf))
+ else:
+ # Bare markdown or issue-based program: name is the filename without .md
+ return os.path.splitext(os.path.basename(pf))[0]
+
+for pf in program_files:
+ name = get_program_name(pf)
+ all_programs[name] = pf
+ with open(pf) as f:
+ content = f.read()
+
+ # Check sentinel (skip for issue-based programs which use AUTOLOOP:ISSUE-PROGRAM)
+ if "" in content:
+ unconfigured.append(name)
+ continue
+
+ # Check for TODO/REPLACE placeholders
+ if re.search(r'\bTODO\b|\bREPLACE', content):
+ unconfigured.append(name)
+ continue
+
+ # Parse optional YAML frontmatter for schedule and target-metric
+ # Strip leading HTML comments before checking (issue-based programs may have them)
+ content_stripped = re.sub(r'^(\s*\s*\n)*', '', content, flags=re.DOTALL)
+ schedule_delta = None
+ target_metric = None
+ fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content_stripped, re.DOTALL)
+ if fm_match:
+ for line in fm_match.group(1).split("\n"):
+ if line.strip().startswith("schedule:"):
+ schedule_str = line.split(":", 1)[1].strip()
+ schedule_delta = parse_schedule(schedule_str)
+ if line.strip().startswith("target-metric:"):
+ try:
+ target_metric = float(line.split(":", 1)[1].strip())
+ except (ValueError, TypeError):
+ print(f" Warning: {name} has invalid target-metric value: {line.split(':', 1)[1].strip()}")
+
+ # Read state from repo-memory
+ state = read_program_state(name)
+ if state:
+ print(f" {name}: last_run={state.get('last_run')}, iteration_count={state.get('iteration_count')}")
+ else:
+ print(f" {name}: no state found (first run)")
+
+ last_run = None
+ lr = state.get("last_run")
+ if lr:
+ try:
+ last_run = datetime.fromisoformat(lr.replace("Z", "+00:00"))
+ except ValueError:
+ pass
+
+ # Check if completed (target metric was reached)
+ if str(state.get("completed", "")).lower() == "true":
+ skipped.append({"name": name, "reason": f"completed: target metric reached"})
+ continue
+
+ # Check if paused (e.g., plateau or recurring errors)
+ if state.get("paused"):
+ skipped.append({"name": name, "reason": f"paused: {state.get('pause_reason', 'unknown')}"})
+ continue
+
+ # Auto-pause on plateau: 5+ consecutive rejections
+ recent = state.get("recent_statuses", [])[-5:]
+ if len(recent) >= 5 and all(s == "rejected" for s in recent):
+ skipped.append({"name": name, "reason": "plateau: 5 consecutive rejections"})
+ continue
+
+ # Check if due based on per-program schedule
+ if schedule_delta and last_run:
+ if now - last_run < schedule_delta:
+ skipped.append({"name": name, "reason": "not due yet",
+ "next_due": (last_run + schedule_delta).isoformat()})
+ continue
+
+ due.append({"name": name, "last_run": lr, "file": pf, "target_metric": target_metric,
+ "schedule_seconds": schedule_delta.total_seconds() if schedule_delta else None})
+
+# Pick the program to run
+selected = None
+selected_file = None
+selected_issue = None
+selected_target_metric = None
+deferred = []
+
+if forced_program:
+ # Manual dispatch requested a specific program — bypass scheduling
+ # (paused, not-due, and plateau programs can still be forced)
+ if forced_program not in all_programs:
+ print(f"ERROR: requested program '{forced_program}' not found.")
+ print(f" Available programs: {list(all_programs.keys())}")
+ sys.exit(1)
+ if forced_program in unconfigured:
+ print(f"ERROR: requested program '{forced_program}' is unconfigured (has placeholders).")
+ sys.exit(1)
+ selected = forced_program
+ selected_file = all_programs[forced_program]
+ deferred = [p["name"] for p in due if p["name"] != forced_program]
+ if selected in issue_programs:
+ selected_issue = issue_programs[selected]["issue_number"]
+ elif selected in file_program_issues:
+ # File-based program with an auto-created program issue.
+ selected_issue = file_program_issues[selected]
+ # Find target_metric: check the due list first, then parse from the program file
+ for p in due:
+ if p["name"] == forced_program:
+ selected_target_metric = p.get("target_metric")
+ break
+ if selected_target_metric is None:
+ # Program may have been skipped (completed/paused/plateau) — parse directly
+ try:
+ with open(selected_file) as _f:
+ _content = _f.read()
+ _content_stripped = re.sub(r'^(\s*\s*\n)*', '', _content, flags=re.DOTALL)
+ _fm = re.match(r"^---\s*\n(.*?)\n---\s*\n", _content_stripped, re.DOTALL)
+ if _fm:
+ for _line in _fm.group(1).split("\n"):
+ if _line.strip().startswith("target-metric:"):
+ selected_target_metric = float(_line.split(":", 1)[1].strip())
+ break
+ except (OSError, ValueError, TypeError):
+ pass
+ print(f"FORCED: running program '{forced_program}' (manual dispatch)")
+elif due:
+ # Normal scheduling: pick the single most-overdue program.
+ # Tiebreaker rationale: programs that have never run (no last_run) take
+ # priority over ever-run programs; among never-run programs, prefer the
+ # shortest schedule (so "every 30m" beats "every 6h"), then alphabetical
+ # by name. Programs with no parseable schedule sort last among never-run
+ # programs (float('inf')). This avoids permanent starvation when state
+ # is missing — see issue: "Autoloop pre-step can't read state files".
+ def _due_sort_key(p):
+ if p["last_run"]:
+ return (1, p["last_run"], p["name"])
+ sched = p.get("schedule_seconds")
+ return (0, sched if sched is not None else float("inf"), p["name"])
+ due.sort(key=_due_sort_key)
+ selected = due[0]["name"]
+ selected_file = due[0]["file"]
+ selected_target_metric = due[0].get("target_metric")
+ deferred = [p["name"] for p in due[1:]]
+ # Check if the selected program is issue-based, or a file-based program
+ # with an auto-created program issue.
+ if selected in issue_programs:
+ selected_issue = issue_programs[selected]["issue_number"]
+ elif selected in file_program_issues:
+ selected_issue = file_program_issues[selected]
+
+# Look up existing PR for the selected program's canonical branch
+existing_pr = None
+head_branch = None
+
+def verify_pr_is_open(pr_number):
+ """Check if a PR is still open via the GitHub API. Returns True if open."""
+ try:
+ verify_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
+ verify_req = urllib.request.Request(verify_url, headers={
+ "Authorization": f"token {github_token}",
+ "Accept": "application/vnd.github.v3+json",
+ })
+ with urllib.request.urlopen(verify_req, timeout=30) as verify_resp:
+ pr_data = json.loads(verify_resp.read().decode())
+ return pr_data.get("state") == "open"
+ except Exception:
+ return True # If we can't verify, assume it's open (best effort)
+
+if selected:
+ head_branch = f"autoloop/{selected}"
+ owner = repo.split("/")[0] if "/" in repo else ""
+ if owner:
+ # Strategy 1: exact branch match (works when branch has no framework suffix)
+ try:
+ pr_api_url = (
+ f"https://api.github.com/repos/{repo}/pulls"
+ f"?state=open&head={owner}:{head_branch}&per_page=5"
+ )
+ pr_req = urllib.request.Request(pr_api_url, headers={
+ "Authorization": f"token {github_token}",
+ "Accept": "application/vnd.github.v3+json",
+ })
+ with urllib.request.urlopen(pr_req, timeout=30) as pr_resp:
+ open_prs = json.loads(pr_resp.read().decode())
+ if open_prs:
+ existing_pr = open_prs[0]["number"]
+ print(f" Found existing PR #{existing_pr} for exact branch {head_branch}")
+ except Exception as e:
+ print(f" Warning: could not check for existing PRs by exact branch: {e}")
+
+ # Strategy 2: search by title and branch prefix (catches framework-generated
+ # hash suffixes like autoloop/name-a1b2c3d4e5f6g7h8 created by create-pull-request)
+ if existing_pr is None:
+ try:
+ title_marker = f"[Autoloop: {selected}]"
+ branch_prefix = head_branch # e.g. autoloop/perf-comparison
+ list_url = (
+ f"https://api.github.com/repos/{repo}/pulls"
+ f"?state=open&per_page=100&sort=created&direction=desc"
+ )
+ list_req = urllib.request.Request(list_url, headers={
+ "Authorization": f"token {github_token}",
+ "Accept": "application/vnd.github.v3+json",
+ })
+ with urllib.request.urlopen(list_req, timeout=30) as list_resp:
+ all_open_prs = json.loads(list_resp.read().decode())
+ # Match branch names: exact canonical name or canonical + framework hash suffix
+ branch_pattern = re.compile(r'^' + re.escape(branch_prefix) + r'(-[0-9a-f]{16})?$')
+ for pr in all_open_prs:
+ pr_title = pr.get("title", "")
+ pr_head_ref = pr.get("head", {}).get("ref", "")
+ if title_marker in pr_title or branch_pattern.match(pr_head_ref):
+ existing_pr = pr["number"]
+ print(f" Found existing PR #{existing_pr} by title/branch-prefix (branch: {pr_head_ref})")
+ break
+ if existing_pr is None:
+ print(f" No existing PR found for program {selected}")
+ except Exception as e:
+ print(f" Warning: could not search for existing PRs by title/prefix: {e}")
+ else:
+ print(f" Warning: could not parse owner from GITHUB_REPOSITORY='{repo}'")
+
+ # Strategy 3: check the state file for a recorded PR number as fallback
+ if existing_pr is None:
+ state = read_program_state(selected)
+ pr_field = state.get("pr") or ""
+ pr_match = re.match(r'^#?(\d+)$', pr_field.strip())
+ if pr_match:
+ pr_num = int(pr_match.group(1))
+ if verify_pr_is_open(pr_num):
+ existing_pr = pr_num
+ print(f" Found open PR #{existing_pr} from state file for {selected}")
+ else:
+ print(f" PR #{pr_num} from state file is no longer open — ignoring")
+
+result = {
+ "selected": selected,
+ "selected_file": selected_file,
+ "selected_issue": selected_issue,
+ "selected_target_metric": selected_target_metric,
+ "existing_pr": existing_pr,
+ "head_branch": head_branch,
+ "issue_programs": {name: info["issue_number"] for name, info in issue_programs.items()},
+ "deferred": deferred,
+ "skipped": skipped,
+ "unconfigured": unconfigured,
+ "no_programs": False,
+}
+
+os.makedirs("/tmp/gh-aw", exist_ok=True)
+with open("/tmp/gh-aw/autoloop.json", "w") as f:
+ json.dump(result, f, indent=2)
+
+print("=== Autoloop Program Check ===")
+print(f"Selected program: {selected or '(none)'} ({selected_file or 'n/a'})")
+if existing_pr:
+ print(f"Existing PR: #{existing_pr} (branch: {head_branch})")
+else:
+ print(f"Existing PR: (none — will create on first accepted iteration)")
+print(f"Deferred (next run): {deferred or '(none)'}")
+print(f"Programs skipped: {[s['name'] for s in skipped] or '(none)'}")
+print(f"Programs unconfigured: {unconfigured or '(none)'}")
+
+if not selected and not unconfigured:
+ print("\nNo programs due this run. Exiting early.")
+ sys.exit(1) # Non-zero exit skips the agent step