framework-agent actions-1-c325747b-turn-1 #9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # The workflow ActionsDriver dispatches (#610): one run is one `prompt()` turn. | |
| # | |
| # The driver cannot learn a run id from the dispatch API (it answers 204 with no body), so | |
| # the correlation id it generates is echoed into `run-name` and into the artifact name. That | |
| # is how the driver finds its own run, and it is why both must keep interpolating it. | |
| name: framework-agent | |
| run-name: framework-agent ${{ inputs.correlation_id }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| prompt: | |
| description: What the agent should do. Passed to the action verbatim, never through a shell. | |
| required: true | |
| correlation_id: | |
| description: How the driver finds this run. Must appear in run-name and in the artifact name. | |
| required: true | |
| model: | |
| description: Model id, e.g. claude-opus-4-8. Empty means the action's default. | |
| required: false | |
| resume_session_id: | |
| description: A prior agent session id to continue instead of starting fresh. | |
| required: false | |
| branch: | |
| description: Branch to push the run's work to, so the driver can read it back and continue on it (#1085). | |
| required: false | |
| # The workflow pushes the run branch and the agent may open a PR. `id-token: write` is not | |
| # optional: the action exchanges an OIDC token to authenticate the subscription OAuth token, | |
| # and without it every run fails with "Could not fetch an OIDC token". | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| jobs: | |
| agent: | |
| runs-on: ubuntu-latest | |
| # Well under the 6h job cap. A turn that runs this long has gone wrong. | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # Full history: the agent reads the log to understand what it is changing. | |
| fetch-depth: 0 | |
| # Built from environment variables rather than by interpolating ${{ }} into the shell, | |
| # so a crafted input cannot become a command. The driver validates them too. | |
| - name: Compose agent arguments | |
| id: args | |
| env: | |
| MODEL: ${{ inputs.model }} | |
| RESUME: ${{ inputs.resume_session_id }} | |
| run: | | |
| set -euo pipefail | |
| # Agent mode grants no permissions by default, so an unattended run cannot edit | |
| # or run anything without this. The runner is disposable, which is what makes it safe. | |
| args="--dangerously-skip-permissions" | |
| # Written as `if` rather than `[ .. ] && ..`, which returns non-zero when the input | |
| # is empty and would fail the step under `set -e`. | |
| if [ -n "$MODEL" ]; then args="$args --model $MODEL"; fi | |
| if [ -n "$RESUME" ]; then args="$args --resume $RESUME"; fi | |
| echo "value=$args" >> "$GITHUB_OUTPUT" | |
| - name: Run the agent | |
| id: claude | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| # A `claude setup-token` OAuth token: the run spends the subscription, not an API key (#495). | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| prompt: ${{ inputs.prompt }} | |
| claude_args: ${{ steps.args.outputs.value }} | |
| # The runner and its checkout vanish when the job ends, so the only way the driver can | |
| # read the agent's work (diffs, file contents) or run the next turn on top of it is a | |
| # branch on origin. The action does not create one for a workflow_dispatch run, so we | |
| # push it here, to the name the driver chose. This mirrors the local flow, where the | |
| # framework (not the agent) pushes the session branch (#799); the agent just commits. | |
| - name: Push the run branch | |
| if: always() | |
| id: runbranch | |
| env: | |
| RUN_BRANCH: ${{ inputs.branch }} | |
| DISPATCH_REF: ${{ github.ref_name }} | |
| # Authenticate the push explicitly. The agent step runs its own git setup and leaves | |
| # the checkout's persisted credentials unusable, so a plain `git push origin` fails | |
| # with "Authentication failed"; push through a tokenized URL instead. | |
| GH_PUSH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$RUN_BRANCH" ]; then echo "no run branch requested"; exit 0; fi | |
| git config user.name "framework-agent" | |
| git config user.email "framework-agent@users.noreply.github.com" | |
| # Commit anything the agent left uncommitted so it is not lost with the runner. | |
| if [ -n "$(git status --porcelain)" ]; then | |
| git add -A | |
| git commit -m "framework agent run ($RUN_BRANCH)" --quiet | |
| fi | |
| # Don't create an empty branch for a no-op turn: only push when the run advanced | |
| # past the ref it checked out. If that ref can't be resolved, err toward pushing. | |
| base="$(git rev-parse --verify --quiet "origin/$DISPATCH_REF" || true)" | |
| if [ -n "$base" ] && [ "$(git rev-parse HEAD)" = "$base" ]; then | |
| echo "no new commits; nothing to push" | |
| exit 0 | |
| fi | |
| git push "https://x-access-token:${GH_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:refs/heads/$RUN_BRANCH" | |
| echo "branch=$RUN_BRANCH" >> "$GITHUB_OUTPUT" | |
| # The only REST-readable channel out of a run. `always()` so a failed turn still | |
| # returns its transcript, which is exactly when we most want to read it. | |
| - name: Collect the transcript | |
| if: always() | |
| env: | |
| EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} | |
| # The branch we actually pushed above, not the action's branch_name, which stays | |
| # empty for a workflow_dispatch agent run (#1085). | |
| BRANCH: ${{ steps.runbranch.outputs.branch }} | |
| SESSION_ID: ${{ steps.claude.outputs.session_id }} | |
| run: | | |
| set -euo pipefail | |
| # Not dot-prefixed: upload-artifact@v4 defaults include-hidden-files to false and | |
| # drops every file under a hidden path segment, so a `.framework-run/` dir uploads | |
| # nothing and the driver finds no artifact to read. | |
| mkdir -p framework-run | |
| # An empty array still parses, so a crashed action yields an empty turn, not a driver error. | |
| if [ -n "$EXECUTION_FILE" ] && [ -f "$EXECUTION_FILE" ]; then | |
| cp "$EXECUTION_FILE" framework-run/execution.json | |
| else | |
| echo '[]' > framework-run/execution.json | |
| fi | |
| jq -n --arg branch "$BRANCH" --arg session_id "$SESSION_ID" \ | |
| '{branch: $branch, session_id: $session_id}' > framework-run/meta.json | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: framework-run-${{ inputs.correlation_id }} | |
| path: framework-run | |
| retention-days: 7 |