From ee239b0e100e557529bd7545e5f5e0a9bf1b414c Mon Sep 17 00:00:00 2001 From: mrjf Date: Thu, 21 May 2026 07:28:06 -0700 Subject: [PATCH 1/3] Install Crane Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/crane-migration.md | 68 + .github/agents/agentic-workflows.agent.md | 79 +- .github/aw/actions-lock.json | 11 +- .github/mcp.json | 11 + .github/workflows/agentics-maintenance.yml | 116 +- .github/workflows/crane.lock.yml | 1950 ++++++++++++++++++ .github/workflows/crane.md | 854 ++++++++ .github/workflows/scripts/crane_scheduler.py | 754 +++++++ .github/workflows/shared/reporting.md | 45 + 9 files changed, 3850 insertions(+), 38 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/crane-migration.md create mode 100644 .github/mcp.json create mode 100644 .github/workflows/crane.lock.yml create mode 100644 .github/workflows/crane.md create mode 100644 .github/workflows/scripts/crane_scheduler.py create mode 100644 .github/workflows/shared/reporting.md diff --git a/.github/ISSUE_TEMPLATE/crane-migration.md b/.github/ISSUE_TEMPLATE/crane-migration.md new file mode 100644 index 00000000..77af0d66 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crane-migration.md @@ -0,0 +1,68 @@ +--- +name: Crane Migration +about: Create a new Crane code-migration +title: '' +labels: crane-migration +--- + + + + + + +--- +schedule: every 6h +strategy: auto # in-place | greenfield | auto +source-language: REPLACE # e.g. python, ruby, perl +target-languages: [REPLACE] # e.g. [typescript], [typescript, go], [kotlin] +target-metric: 1.0 # migration is complete when health score reaches this +metric_direction: higher +--- + +# Migration Name + +## Source + + + +- **Language**: REPLACE (e.g. Python 3.11) +- **Runtime**: REPLACE (e.g. CPython) +- **Paths**: + - `REPLACE/path/to/source` — (what lives here) + - `REPLACE/path/to/source-tests` — existing test suite + +## Target + + + +- **Languages**: REPLACE (e.g. TypeScript, Go) +- **Runtime**: REPLACE (e.g. Node 22 / Bun 1.x) +- **Paths**: + - `REPLACE/path/to/target` — (what should live here) +- **Bridge** *(if polyglot)*: REPLACE (e.g. "Go core compiled to WASM, called from TypeScript through a thin wrapper") + +## Strategy + + + +REPLACE — explain why this strategy fits. + +- `in-place` (strangler-fig): system stays live throughout. Each milestone ports one unit and re-routes callers. Preferred for production code or anything with external consumers. +- `greenfield`: target built in parallel; cutover after parity is total. Best for small, self-contained sources. +- `auto`: let Crane pick on first iteration. + +## Verification + + + +```bash +REPLACE_WITH_YOUR_VERIFICATION_COMMAND +``` + +The metric is `migration_score` (0.0–1.0). **Higher is better.** Optional companion fields: `progress`, `parity_passing`, `parity_total`, `source_tests_passing`, `target_tests_passing`, `perf_ratio`. + +## Out of scope + + + +- (list paths that are off-limits even if they share a parent with source/target paths) diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md index c0f21877..98908e39 100644 --- a/.github/agents/agentic-workflows.agent.md +++ b/.github/agents/agentic-workflows.agent.md @@ -19,6 +19,13 @@ This is a **dispatcher agent** that routes your request to the appropriate speci - **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt - **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes - **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs +- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions +- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command +- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments +- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows + +> [!IMPORTANT] +> For architecture/pattern-selection requests, load `https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/patterns.md` first. Workflows may optionally include: @@ -30,7 +37,7 @@ Workflows may optionally include: - Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` - Workflow lock files: `.github/workflows/*.lock.yml` - Shared components: `.github/workflows/shared/*.md` -- Configuration: https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md +- Configuration: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/github-agentic-workflows.md ## Problems This Solves @@ -52,7 +59,7 @@ When you interact with this agent, it will: ### Create New Workflow **Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/create-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/create-agentic-workflow.md **Use cases**: - "Create a workflow that triages issues" @@ -62,7 +69,7 @@ When you interact with this agent, it will: ### Update Existing Workflow **Load when**: User wants to modify, improve, or refactor an existing workflow -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/update-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/update-agentic-workflow.md **Use cases**: - "Add web-fetch tool to the issue-classifier workflow" @@ -72,7 +79,7 @@ When you interact with this agent, it will: ### Debug Workflow **Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/debug-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/debug-agentic-workflow.md **Use cases**: - "Why is this workflow failing?" @@ -82,7 +89,7 @@ When you interact with this agent, it will: ### Upgrade Agentic Workflows **Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/upgrade-agentic-workflows.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/upgrade-agentic-workflows.md **Use cases**: - "Upgrade all workflows to the latest version" @@ -92,7 +99,7 @@ When you interact with this agent, it will: ### Create a Report-Generating Workflow **Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/report.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/report.md **Use cases**: - "Create a weekly CI health report" @@ -102,7 +109,7 @@ When you interact with this agent, it will: ### Create Shared Agentic Workflow **Load when**: User wants to create a reusable workflow component or wrap an MCP server -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/create-shared-agentic-workflow.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/create-shared-agentic-workflow.md **Use cases**: - "Create a shared component for Notion integration" @@ -112,7 +119,7 @@ When you interact with this agent, it will: ### Fix Dependabot PRs **Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`) -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/dependabot.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/dependabot.md **Use cases**: - "Fix the open Dependabot PRs for npm dependencies" @@ -122,13 +129,58 @@ When you interact with this agent, it will: ### Analyze Test Coverage **Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. -**Prompt file**: https://github.com/github/gh-aw/blob/main/.github/aw/test-coverage.md +**Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/test-coverage.md **Use cases**: - "Create a workflow that comments coverage on PRs" - "Analyze coverage trends over time" - "Add a coverage gate that blocks PRs below a threshold" +### Render ASCII Charts in Markdown +**Load when**: The workflow needs in-markdown charts (sparklines, bars, table+trend views) that must align cleanly and render reliably across GitHub surfaces, including mobile. + +**Reference file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/asciicharts.md + +**Use cases**: +- "Show a compact trend chart in an issue comment" +- "Render a dashboard table with sparkline trends" +- "Generate aligned ASCII bars for service metrics" + +### CLI Commands Reference +**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access. + +**Reference file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/cli-commands.md + +**Use cases**: +- "How do I trigger workflow X on the main branch?" +- "What's the MCP equivalent of `gh aw logs`?" +- "I'm in Copilot Cloud — how do I compile a workflow?" +- "Show me all available gh aw commands" + +### Token Consumption Optimization +**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes. + +**Reference file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/token-optimization.md + +**Use cases**: +- "How do I reduce the token cost of this workflow?" +- "My workflow is too expensive — how do I optimize it?" +- "How do I compare token usage between two runs?" +- "Should I use gh-proxy or the MCP server?" +- "How do I use sub-agents to reduce costs?" +- "How do I measure the impact of a prompt change?" + +### Workflow Pattern Selection +**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows. + +**Reference file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/patterns.md + +**Use cases**: +- "Which pattern should I use for multi-repo rollout?" +- "How should I structure this workflow architecture?" +- "What pattern fits slash-command triage?" +- "Should this be DispatchOps or DailyOps?" + ## Instructions When a user interacts with you: @@ -147,6 +199,10 @@ gh aw init # Generate the lock file for a workflow gh aw compile [workflow-name] +# Trigger a workflow on demand (preferred over gh workflow run) +gh aw run # interactive input collection +gh aw run --ref main # run on a specific branch + # Debug workflow runs gh aw logs [workflow-name] gh aw audit @@ -169,9 +225,12 @@ gh aw compile --validate ## Important Notes -- Always reference the instructions file at https://github.com/github/gh-aw/blob/main/.github/aw/github-agentic-workflows.md for complete documentation +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/github-agentic-workflows.md for complete documentation - Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud - Workflows must be compiled to `.lock.yml` files before running in GitHub Actions - **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF - Follow security best practices: minimal permissions, explicit network access, no template injection +- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. - **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. +- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. +- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/cli-commands.md diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 7dbae0ca..44b9929d 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -30,15 +30,10 @@ "version": "v7", "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, - "github/gh-aw-actions/setup-cli@v0.71.5": { - "repo": "github/gh-aw-actions/setup-cli", - "version": "v0.71.5", - "sha": "b8068426813005612b960b5ab0b8bd2c27142323" - }, - "github/gh-aw-actions/setup@v0.71.5": { + "github/gh-aw-actions/setup@v0.74.4": { "repo": "github/gh-aw-actions/setup", - "version": "v0.71.5", - "sha": "b8068426813005612b960b5ab0b8bd2c27142323" + "version": "v0.74.4", + "sha": "d3abfe96a194bce3a523ed2093ddedd5704cdf62" }, "github/gh-aw/actions/setup@v0.50.6": { "repo": "github/gh-aw/actions/setup", diff --git a/.github/mcp.json b/.github/mcp.json new file mode 100644 index 00000000..b953af26 --- /dev/null +++ b/.github/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "github-agentic-workflows": { + "command": "gh", + "args": [ + "aw", + "mcp-server" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index 024fa17a..4b3cc172 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -12,7 +12,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.71.5). DO NOT EDIT. +# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.74.4). DO NOT EDIT. # # To regenerate this workflow, run: # gh aw compile @@ -55,6 +55,7 @@ on: - 'clean_cache_memories' - 'update_pull_request_branches' - 'validate' + - 'forecast' run_url: description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.' required: false @@ -63,7 +64,7 @@ on: workflow_call: inputs: operation: - description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, update_pull_request_branches, validate)' + description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, update_pull_request_branches, validate, forecast)' required: false type: string default: '' @@ -92,7 +93,7 @@ jobs: pull-requests: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -130,7 +131,7 @@ jobs: actions: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -144,7 +145,7 @@ jobs: await main(); run_operation: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'update_pull_request_branches' && inputs.operation != 'validate' && (!(github.event.repository.fork)) }} + if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'update_pull_request_branches' && inputs.operation != 'validate' && inputs.operation != 'forecast' && (!(github.event.repository.fork)) }} runs-on: ubuntu-slim permissions: actions: write @@ -159,7 +160,7 @@ jobs: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -174,9 +175,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup-cli@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: - version: v0.71.5 + version: v0.74.4 - name: Run operation uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -204,7 +205,7 @@ jobs: pull-requests: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -250,7 +251,7 @@ jobs: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -294,7 +295,7 @@ jobs: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -309,9 +310,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup-cli@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: - version: v0.71.5 + version: v0.74.4 - name: Create missing labels uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -340,7 +341,7 @@ jobs: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -355,9 +356,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup-cli@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: - version: v0.71.5 + version: v0.74.4 - name: Restore activity report logs cache id: activity_report_logs_cache @@ -430,6 +431,81 @@ jobs: }); core.info('Created issue #' + createdIssue.data.number + ': ' + createdIssue.data.html_url); + forecast_report: + if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'forecast' && (!(github.event.repository.fork)) }} + runs-on: ubuntu-slim + timeout-minutes: 60 + permissions: + actions: read + contents: read + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Scripts + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + + - name: Check admin/maintainer permissions + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_team_member.cjs'); + await main(); + + - name: Install gh-aw + uses: github/gh-aw-actions/setup-cli@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + version: v0.74.4 + + - name: Restore forecast report logs cache + id: forecast_report_logs_cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: .github/aw/logs + key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-forecast-report-logs-${{ github.repository }}- + ${{ runner.os }}-forecast-report-logs- + + - name: Generate forecast report + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_AW_CMD_PREFIX: gh aw + run: | + mkdir -p ./.cache/gh-aw/forecast + ${GH_AW_CMD_PREFIX} logs --repo "${{ github.repository }}" --start-date -30d --count 1500 > /dev/null + if ! compgen -G ".github/aw/logs/run-*/run_summary.json" > /dev/null; then + echo "::error::Missing run summary cache in .github/aw/logs after gh aw logs warm-up; cannot run forecast." + exit 1 + fi + ${GH_AW_CMD_PREFIX} forecast --repo "${{ github.repository }}" --json 2> >(grep -Fv "forecast is an experimental command and may change without notice" >&2) > ./.cache/gh-aw/forecast/report.json + + - name: Save forecast report logs cache + if: ${{ always() }} + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: .github/aw/logs + key: ${{ steps.forecast_report_logs_cache.outputs.cache-primary-key }} + + - name: Generate forecast issue + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/create_forecast_issue.cjs'); + await main(); + close_agentic_workflows_issues: if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues' && (!(github.event.repository.fork)) }} runs-on: ubuntu-slim @@ -437,7 +513,7 @@ jobs: issues: write steps: - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -474,7 +550,7 @@ jobs: persist-credentials: false - name: Setup Scripts - uses: github/gh-aw-actions/setup@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: destination: ${{ runner.temp }}/gh-aw/actions @@ -489,9 +565,9 @@ jobs: await main(); - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5 + uses: github/gh-aw-actions/setup-cli@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 with: - version: v0.71.5 + version: v0.74.4 - name: Validate workflows and file issue on findings uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/.github/workflows/crane.lock.yml b/.github/workflows/crane.lock.yml new file mode 100644 index 00000000..8a7dc597 --- /dev/null +++ b/.github/workflows/crane.lock.yml @@ -0,0 +1,1950 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"d595f76dd8789f76fa60f31d413e42cd29d54f574a148705e0eb72b604277be9","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"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/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"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":"d3abfe96a194bce3a523ed2093ddedd5704cdf62","version":"v0.74.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.74.4). DO NOT EDIT. +# +# To update this file, edit githubnext/crane and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Crane runs planned, verified code migrations from one language (or runtime) to another. +# It is a sibling of autoloop, specialized for migration. Each iteration advances a living +# migration plan by one step, verifies that the system still works, and keeps the change +# only if correctness is preserved. +# - User defines source, target, strategy, and verification in a migration.md file +# - First iteration produces the plan (inventory + strategy + milestones) +# - Subsequent iterations execute one milestone at a time +# - Accepts changes only when the health score does not regress (ratchet on correctness) +# - Persists all state via repo-memory (human-readable, human-editable) +# - Commits accepted changes to a long-running branch per migration +# - Maintains a single draft PR per migration that accumulates all accepted iterations +# +# Source: githubnext/crane +# +# Resolved workflow manifest: +# Imports: +# - shared/reporting.md +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 +# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 +# - actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.46 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.46 +# - ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 +# - ghcr.io/github/github-mcp-server:v1.0.4 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "Crane" +on: + discussion: + types: + - created + - edited + discussion_comment: + types: + - created + - edited + issue_comment: + types: + - created + - edited + issues: + types: + - opened + - edited + - reopened + pull_request: + types: + - opened + - edited + - reopened + pull_request_review_comment: + types: + - created + - edited + schedule: + - cron: "25 */6 * * *" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + migration: + description: Run a specific migration by name (bypasses scheduling) + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}" + +run-name: "Crane" + +jobs: + activation: + needs: pre_activation + 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, '/crane ') || startsWith(github.event.issue.body, '/crane\n') || github.event.issue.body == '/crane') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/crane ') || startsWith(github.event.pull_request.body, '/crane\n') || github.event.pull_request.body == '/crane') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/crane ') || startsWith(github.event.discussion.body, '/crane\n') || github.event.discussion.body == '/crane') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane')) || (!(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 + pull-requests: write + outputs: + body: ${{ steps.sanitized.outputs.body }} + comment_id: ${{ steps.add-comment.outputs.comment-id }} + comment_repo: ${{ steps.add-comment.outputs.comment-repo }} + comment_url: ${{ steps.add-comment.outputs.comment-url }} + engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + slash_command: ${{ needs.pre_activation.outputs.matched_command }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_AGENT_VERSION: "1.0.48" + GH_AW_INFO_CLI_VERSION: "v0.74.4" + GH_AW_INFO_WORKFLOW_NAME: "Crane" + 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","go"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.46" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/crane" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: 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 || github.event_name == 'pull_request_review' && github.event.pull_request.head.repo.id == github.repository_id + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REACTION: "eyes" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs'); + await main(); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + .claude + .codex + .crush + .gemini + .opencode + .pi + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Save agent config folders for base branch restoration + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_FILE: "crane.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMPILED_VERSION: "v0.74.4" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Compute current body text + id: sanitized + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,go.dev,golang.org,googleapis.deno.dev,googlechromelabs.github.io,goproxy.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,pkg.go.dev,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,proxy.golang.org,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,sum.golang.org,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + await main(); + - name: Add comment with workflow run link + id: add-comment + if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id || github.event_name == 'pull_request_review' && github.event.pull_request.head.repo.id == github.repository_id + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_WORKFLOW_NAME: "Crane" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/add_workflow_run_comment.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} + GH_AW_WIKI_NOTE: ${{ '' }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_e9f449cc43378e39_EOF' + + GH_AW_PROMPT_e9f449cc43378e39_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_e9f449cc43378e39_EOF' + + Tools: add_comment(max:7), create_issue, 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_e9f449cc43378e39_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_e9f449cc43378e39_EOF' + + GH_AW_PROMPT_e9f449cc43378e39_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" + cat << 'GH_AW_PROMPT_e9f449cc43378e39_EOF' + + The following GitHub context information is available for this workflow: + {{#if github.actor}} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if github.repository}} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if github.workspace}} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} + - **issue-number**: #__GH_AW_EXPR_802A9F6A__ + {{/if}} + {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} + - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ + {{/if}} + {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} + - **pull-request-number**: #__GH_AW_EXPR_463A214A__ + {{/if}} + {{#if github.event.comment.id || github.aw.context.comment_id}} + - **comment-id**: __GH_AW_EXPR_FF1D34CE__ + {{/if}} + {{#if github.run_id}} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - `$GITHUB_WORKSPACE` → `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it 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_e9f449cc43378e39_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then + cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" + fi + if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then + cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" + fi + cat << 'GH_AW_PROMPT_e9f449cc43378e39_EOF' + + {{#runtime-import .github/workflows/shared/reporting.md}} + {{#runtime-import .github/workflows/crane.md}} + GH_AW_PROMPT_e9f449cc43378e39_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ENGINE_ID: "copilot" + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} + GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_MEMORY_BRANCH_NAME: 'memory/crane' + GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Allowed Files**: Only files matching patterns: *.md\n- **Max File Size**: 40960 bytes (0.04 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 1024 KB)\n" + GH_AW_MEMORY_DESCRIPTION: '' + GH_AW_MEMORY_DIR: '/tmp/gh-aw/repo-memory/default/' + GH_AW_MEMORY_TARGET_REPO: ' of the current repository' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} + GH_AW_WIKI_NOTE: '' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, + GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, + GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, + GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_SERVER_URL: process.env.GH_AW_GITHUB_SERVER_URL, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT, + GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, + GH_AW_MEMORY_BRANCH_NAME: process.env.GH_AW_MEMORY_BRANCH_NAME, + GH_AW_MEMORY_CONSTRAINTS: process.env.GH_AW_MEMORY_CONSTRAINTS, + GH_AW_MEMORY_DESCRIPTION: process.env.GH_AW_MEMORY_DESCRIPTION, + GH_AW_MEMORY_DIR: process.env.GH_AW_MEMORY_DIR, + GH_AW_MEMORY_TARGET_REPO: process.env.GH_AW_MEMORY_TARGET_REPO, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND, + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: process.env.GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT, + GH_AW_WIKI_NOTE: process.env.GH_AW_WIKI_NOTE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + include-hidden-files: true + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw-prompts/prompt-template.txt + /tmp/gh-aw/aw-prompts/prompt-import-tree.json + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/base + /tmp/gh-aw/.github/agents + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: read-all + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: crane + 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 }} + effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: Fetch additional refs + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: 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" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + name: Clone repo-memory for scheduling + run: "# Clone the repo-memory branch so the scheduling step can read persisted state\n# from previous runs. The framework-managed repo-memory clone happens after\n# pre-steps, so we perform an early shallow clone here.\nMEMORY_DIR=\"/tmp/gh-aw/repo-memory/crane\"\nBRANCH=\"memory/crane\"\nmkdir -p \"$(dirname \"$MEMORY_DIR\")\"\nREPO_URL=\"${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git\"\nAUTH_URL=\"$(echo \"$REPO_URL\" | sed \"s|https://|https://x-access-token:${GH_TOKEN}@|\")\"\nif git ls-remote --exit-code --heads \"$AUTH_URL\" \"$BRANCH\" > /dev/null 2>&1; then\n git clone --single-branch --branch \"$BRANCH\" --depth 1 \"$AUTH_URL\" \"$MEMORY_DIR\" 2>&1\n echo \"Cloned repo-memory branch to $MEMORY_DIR\"\nelse\n mkdir -p \"$MEMORY_DIR\"\n echo \"No repo-memory branch found yet (first run). Created empty directory.\"\nfi\n" + - env: + CRANE_MIGRATION: ${{ github.event.inputs.migration }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ github.token }} + name: Check which migrations are due + run: python3 .github/workflows/scripts/crane_scheduler.py + + # Repo memory git-based storage configuration from frontmatter processed below + - name: Clone repo-memory branch (default) + env: + GH_TOKEN: ${{ github.token }} + GITHUB_SERVER_URL: ${{ github.server_url }} + BRANCH_NAME: memory/crane + TARGET_REPO: ${{ github.repository }} + MEMORY_DIR: /tmp/gh-aw/repo-memory/default + CREATE_ORPHAN: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clone_repo_memory_branch.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.48 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + 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 }} + with: + script: | + const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Restore agent config folders from base branch + if: steps.checkout-pr.outcome == 'success' + env: + GH_AW_AGENT_FOLDERS: ".agents .claude .codex .crush .gemini .github .opencode .pi" + GH_AW_AGENT_FILES: ".crush.json AGENTS.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" + - name: Restore inline sub-agents from activation artifact + env: + GH_AW_SUB_AGENT_DIR: ".github/agents" + GH_AW_SUB_AGENT_EXT: ".agent.md" + run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.46 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 ghcr.io/github/gh-aw-firewall/squid:0.25.46 ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388 ghcr.io/github/github-mcp-server:v1.0.4 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Generate Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_51640d2c18350e01_EOF' + {"add_comment":{"hide_older_comments":false,"max":7,"target":"*"},"add_labels":{"max":2,"target":"*"},"create_issue":{"labels":["automation","crane"],"max":1},"create_pull_request":{"draft":true,"labels":["automation","crane"],"max":1,"max_patch_files":100,"max_patch_size":10240,"preserve_branch_name":true,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"fallback-to-issue"},"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":40960,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":1,"max_patch_size":10240,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"target":"*","title_prefix":"[Crane"},"remove_labels":{"max":2,"target":"*"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":3,"target":"*","title_prefix":"[Crane"}} + GH_AW_SAFE_OUTPUTS_CONFIG_51640d2c18350e01_EOF + - name: Generate Safe Outputs Tools + 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 1 issue(s) can be created. Labels [\"automation\" \"crane\"] will be automatically added.", + "create_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be created. Labels [\"automation\" \"crane\"] 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 \"[Crane\".", + "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 \"[Crane\"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "fields": { + "type": "array" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "base": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "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 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + }, + "update_issue": { + "defaultMax": 1, + "fields": { + "assignees": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 39 + }, + "body": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "issue_number": { + "issueOrPRNumber": true + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "milestone": { + "optionalPositiveInteger": true + }, + "operation": { + "type": "string", + "enum": [ + "replace", + "append", + "prepend", + "replace-island" + ] + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "status": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + }, + "customValidation": "requiresOneOf:status,title,body" + } + } + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} + GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="8080" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + export MCP_GATEWAY_HOST_DOMAIN="localhost" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') + MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.9' + + mkdir -p /home/runner/.copilot + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + cat << GH_AW_MCP_CONFIG_7b93a431f7e92e86_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v1.0.4", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "all" + }, + "guard-policies": { + "allow-only": { + "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", + "repos": "$GITHUB_MCP_GUARD_REPOS" + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_7b93a431f7e92e86_EOF + - name: Mount MCP servers as CLIs + id: mount-mcp-clis + continue-on-error: true + env: + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + await main(); + - name: Clean credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Audit pre-agent workspace + id: pre_agent_audit + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 45 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.46/awf-config.schema.json","network":{"allowDomains":["*.gradle-enterprise.cloud","*.pythonhosted.org","*.vsblob.vsassets.io","adoptium.net","anaconda.org","api.adoptium.net","api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.foojay.io","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.npms.io","api.nuget.org","api.snapcraft.io","archive.apache.org","archive.ubuntu.com","azure.archive.ubuntu.com","azuresearch-usnc.nuget.org","azuresearch-ussc.nuget.org","binstar.org","bootstrap.pypa.io","builds.dotnet.microsoft.com","bun.sh","cdn.azul.com","cdn.jsdelivr.net","central.sonatype.com","ci.dot.net","conda.anaconda.org","conda.binstar.org","crates.io","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","dc.services.visualstudio.com","deb.nodesource.com","deno.land","develocity.apache.org","dist.nuget.org","dl.google.com","dlcdn.apache.org","dot.net","dotnet.microsoft.com","dotnetcli.blob.core.windows.net","download.eclipse.org","download.java.net","download.oracle.com","downloads.gradle-dn.com","esm.sh","files.pythonhosted.org","ge.spockframework.org","get.pnpm.io","github.com","go.dev","golang.org","googleapis.deno.dev","googlechromelabs.github.io","goproxy.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","pkg.go.dev","pkgs.dev.azure.com","plugins-artifacts.gradle.org","plugins.gradle.org","ppa.launchpad.net","proxy.golang.org","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","sum.golang.org","telemetry.enterprise.githubcopilot.com","telemetry.vercel.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com","www.java.com","www.microsoft.com","www.npmjs.com","www.npmjs.org","yarnpkg.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"auto":["large"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.46"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.74.4 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + 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" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,go.dev,golang.org,googleapis.deno.dev,googlechromelabs.github.io,goproxy.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,pkg.go.dev,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,proxy.golang.org,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,sum.golang.org,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: crane + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Print AWF reflect summary + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + # Upload repo memory as artifacts for push job + - name: Sanitize repo-memory filenames (default) + if: always() + continue-on-error: true + env: + MEMORY_DIR: /tmp/gh-aw/repo-memory/default + run: bash "${RUNNER_TEMP}/gh-aw/actions/sanitize_repo_memory_filenames.sh" + - name: Upload repo-memory artifact (default) + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: repo-memory-default + path: /tmp/gh-aw/repo-memory/default + retention-days: 1 + if-no-files-found: ignore + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/pre-agent-audit.txt + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/awf-config.json + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + /tmp/gh-aw/sandbox/firewall/awf-reflect.json + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - push_repo_memory + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-crane" + cancel-in-progress: false + queue: max + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_WORKFLOW_SOURCE: "githubnext/crane" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_WORKFLOW_SOURCE: "githubnext/crane" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_WORKFLOW_SOURCE: "githubnext/crane" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_WORKFLOW_SOURCE: "githubnext/crane" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_WORKFLOW_SOURCE: "githubnext/crane" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "crane" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} + GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_PUSH_REPO_MEMORY_RESULT: ${{ needs.push_repo_memory.result }} + GH_AW_REPO_MEMORY_VALIDATION_FAILED_default: ${{ needs.push_repo_memory.outputs.validation_failed_default }} + GH_AW_REPO_MEMORY_VALIDATION_ERROR_default: ${{ needs.push_repo_memory.outputs.validation_error_default }} + GH_AW_REPO_MEMORY_PATCH_SIZE_EXCEEDED_default: ${{ needs.push_repo_memory.outputs.patch_size_exceeded_default }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" + GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" + GH_AW_TIMEOUT_MINUTES: "45" + GH_AW_MAX_EFFECTIVE_TOKENS: "25000000" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_SAFE_OUTPUTS_RESULT: ${{ needs.safe_outputs.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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.46 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46 ghcr.io/github/gh-aw-firewall/squid:0.25.46 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP Config for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + WORKFLOW_NAME: "Crane" + WORKFLOW_DESCRIPTION: "Crane runs planned, verified code migrations from one language (or runtime) to another.\nIt is a sibling of autoloop, specialized for migration. Each iteration advances a living\nmigration plan by one step, verifies that the system still works, and keeps the change\nonly if correctness is preserved.\n- User defines source, target, strategy, and verification in a migration.md file\n- First iteration produces the plan (inventory + strategy + milestones)\n- Subsequent iterations execute one milestone at a time\n- Accepts changes only when the health score does not regress (ratchet on correctness)\n- Persists all state via repo-memory (human-readable, human-editable)\n- Commits accepted changes to a long-running branch per migration\n- Maintains a single draft PR per migration that accumulates all accepted iterations" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.48 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.46 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + touch /tmp/gh-aw/agent-step-summary.md + GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) + export GH_AW_NODE_BIN + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.46/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.46"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw" + fi + # shellcheck disable=SC1003 + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + AWF_REFLECT_ENABLED: 1 + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_API_KEY: dummy-byok-key-for-offline-mode + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || 'claude-sonnet-4.6' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.74.4 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + try { + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + } catch (loadErr) { + const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; + const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; + const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); + core.error(msg); + core.setOutput('reason', 'parse_error'); + if (continueOnError && !detectionExecutionFailed) { + core.warning('\u26A0\uFE0F ' + msg); + core.setOutput('conclusion', 'warning'); + core.setOutput('success', 'false'); + } else { + core.setOutput('conclusion', 'failure'); + core.setOutput('success', 'false'); + core.setFailed(msg); + } + } + + pre_activation: + if: "(github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && ((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, '/crane ') || startsWith(github.event.issue.body, '/crane\n') || github.event.issue.body == '/crane') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/crane ') || startsWith(github.event.pull_request.body, '/crane\n') || github.event.pull_request.body == '/crane') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/crane ') || startsWith(github.event.discussion.body, '/crane\n') || github.event.discussion.body == '/crane') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/crane ') || startsWith(github.event.comment.body, '/crane\n') || github.event.comment.body == '/crane')) || (!(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 + 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-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Check team membership for command workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + - name: Check command position + id: check_command_position + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_COMMANDS: "[\"crane\"]" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_command_position.cjs'); + await main(); + + push_repo_memory: + needs: + - activation + - agent + - detection + if: > + always() && (!cancelled()) && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && + needs.agent.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + concurrency: + group: "push-repo-memory-${{ github.repository }}|memory/crane" + cancel-in-progress: false + outputs: + patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} + validation_error_default: ${{ steps.push_repo_memory_default.outputs.validation_error }} + validation_failed_default: ${{ steps.push_repo_memory_default.outputs.validation_failed }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: . + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Download repo-memory artifact (default) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: repo-memory-default + path: /tmp/gh-aw/repo-memory/default + - name: Push repo-memory changes (default) + id: push_repo_memory_default + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + GITHUB_SERVER_URL: ${{ github.server_url }} + ARTIFACT_DIR: /tmp/gh-aw/repo-memory/default + MEMORY_ID: default + TARGET_REPO: ${{ github.repository }} + BRANCH_NAME: memory/crane + MAX_FILE_SIZE: 40960 + MAX_FILE_COUNT: 100 + MAX_PATCH_SIZE: 10240 + ALLOWED_EXTENSIONS: '[]' + FILE_GLOB_FILTER: "*.md" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/push_repo_memory.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/crane" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_ENGINE_VERSION: "1.0.48" + GH_AW_WORKFLOW_ID: "crane" + GH_AW_WORKFLOW_NAME: "Crane" + GH_AW_WORKFLOW_SOURCE: "githubnext/crane" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Crane" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/crane.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.48" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Extract base branch from agent output + id: extract-base-branch + if: steps.download-agent-output.outcome == 'success' + shell: bash + run: | + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) + BASE_BRANCH=$("$GH_AW_NODE" -e " + try { + const data = JSON.parse(require('fs').readFileSync('/tmp/gh-aw/agent_output.json', 'utf8')); + const item = (data.items || []).find(i => + (i.type === 'create_pull_request' || i.type === 'push_to_pull_request_branch') && + i.base_branch + ); + if (item) process.stdout.write(item.base_branch); + } catch(e) {} + " 2>/dev/null || true) + # Validate: only allow safe git branch name characters + if [[ "$BASE_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]] && [ ${#BASE_BRANCH} -le 255 ]; then + printf 'base-branch=%s\n' "$BASE_BRANCH" >> "$GITHUB_OUTPUT" + echo "Extracted base branch from safe output: $BASE_BRANCH" + fi + fi + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_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,go.dev,golang.org,googleapis.deno.dev,googlechromelabs.github.io,goproxy.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,pkg.go.dev,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,proxy.golang.org,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,sum.golang.org,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\",\"crane\"],\"max\":1},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"crane\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":10240,\"preserve_branch_name\":true,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\"},\"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\":10240,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"target\":\"*\",\"title_prefix\":\"[Crane\"},\"remove_labels\":{\"max\":2,\"target\":\"*\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":3,\"target\":\"*\",\"title_prefix\":\"[Crane\"}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/crane.md b/.github/workflows/crane.md new file mode 100644 index 00000000..99e0850a --- /dev/null +++ b/.github/workflows/crane.md @@ -0,0 +1,854 @@ +--- +description: | + Crane runs planned, verified code migrations from one language (or runtime) to another. + It is a sibling of autoloop, specialized for migration. Each iteration advances a living + migration plan by one step, verifies that the system still works, and keeps the change + only if correctness is preserved. + - User defines source, target, strategy, and verification in a migration.md file + - First iteration produces the plan (inventory + strategy + milestones) + - Subsequent iterations execute one milestone at a time + - Accepts changes only when the health score does not regress (ratchet on correctness) + - Persists all state via repo-memory (human-readable, human-editable) + - Commits accepted changes to a long-running branch per migration + - Maintains a single draft PR per migration that accumulates all accepted iterations + +on: + schedule: every 6h + workflow_dispatch: + inputs: + migration: + description: "Run a specific migration by name (bypasses scheduling)" + required: false + type: string + slash_command: + name: crane + +permissions: read-all + +timeout-minutes: 45 + +network: + allowed: + - defaults + - node + - python + - rust + - java + - dotnet + - go + +safe-outputs: + max-patch-size: 10240 + add-comment: + max: 7 + target: "*" + hide-older-comments: false + create-pull-request: + draft: true + labels: [automation, crane] + protected-files: fallback-to-issue + preserve-branch-name: true + max: 1 + push-to-pull-request-branch: + target: "*" + title-prefix: "[Crane" + max: 1 + create-issue: + labels: [automation, crane] + max: 1 + update-issue: + target: "*" + title-prefix: "[Crane" + max: 3 + add-labels: + target: "*" + max: 2 + remove-labels: + target: "*" + max: 2 + +checkout: + fetch: ["*"] + fetch-depth: 0 + +tools: + web-fetch: + github: + toolsets: [all] + bash: true + repo-memory: + branch-name: memory/crane + file-glob: ["*.md"] + # 40 KB per state file. Crane state files carry a Migration Plan in addition + # to iteration history, so the budget is a bit larger than autoloop's 30 KB. + # The rolling-compaction rule in "Update Rules" keeps files under this budget. + max-file-size: 40960 + +imports: + - shared/reporting.md + +steps: + - name: Clone repo-memory for scheduling + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_SERVER_URL: ${{ github.server_url }} + run: | + # Clone the repo-memory branch so the scheduling step can read persisted state + # from previous runs. The framework-managed repo-memory clone happens after + # pre-steps, so we perform an early shallow clone here. + MEMORY_DIR="/tmp/gh-aw/repo-memory/crane" + BRANCH="memory/crane" + mkdir -p "$(dirname "$MEMORY_DIR")" + REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" + AUTH_URL="$(echo "$REPO_URL" | sed "s|https://|https://x-access-token:${GH_TOKEN}@|")" + if git ls-remote --exit-code --heads "$AUTH_URL" "$BRANCH" > /dev/null 2>&1; then + git clone --single-branch --branch "$BRANCH" --depth 1 "$AUTH_URL" "$MEMORY_DIR" 2>&1 + echo "Cloned repo-memory branch to $MEMORY_DIR" + else + mkdir -p "$MEMORY_DIR" + echo "No repo-memory branch found yet (first run). Created empty directory." + fi + + - name: Check which migrations are due + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + CRANE_MIGRATION: ${{ github.event.inputs.migration }} + run: | + python3 .github/workflows/scripts/crane_scheduler.py + +source: githubnext/crane +engine: copilot +--- + +# Crane + +A planned, verified code-migration agent. It inventories the source, picks a strategy, breaks the migration into milestones, executes one milestone per iteration, and verifies that the system still works — all autonomously, on a schedule. + +## Command Mode + +Take heed of **instructions**: "${{ steps.sanitized.outputs.text }}" + +If these are non-empty (not ""), then you have been triggered via `/crane `. The instructions may be: + +- **A one-off directive targeting a specific migration**: e.g., `/crane stats_py_to_ts: port the quantile family next`. The text before the colon is the migration name (matching a directory in `.crane/migrations/` or an issue with the `crane-migration` label). Execute it as a single iteration for that migration, then report results. +- **A general directive**: e.g., `/crane focus on the parsing module`. If no migration name prefix is given and only one migration exists, use that one. If multiple exist, ask which to target. +- **A configuration change**: e.g., `/crane stats_py_to_ts: switch the strategy to greenfield`. Update the relevant migration file and confirm. +- **A plan change**: e.g., `/crane mark milestone "tokenizer" done` or `/crane add milestone "port quantile family"`. Update the plan in the state file and confirm. + +Then exit — do not run the normal loop after completing the instructions. + +## Migration Locations + +Crane supports three migration layouts: + +### Directory-based migrations (preferred when there's a parity corpus or evaluator) + +Each migration is a directory under `.crane/migrations/` containing a `migration.md` and supporting files: + +``` +.crane/migrations/ +├── stats_py_to_ts/ +│ ├── migration.md ← migration definition (source, target, strategy, verification) +│ └── code/ ← evaluator, parity corpus, staging directories +│ ├── evaluate.py +│ ├── parity/ +│ └── ... +``` + +The **migration name** is the directory name (e.g., `stats_py_to_ts`). + +### Bare-markdown migrations (simple/legacy) + +For simpler migrations whose verification is an existing repo command: + +``` +.crane/migrations/ +├── flask_to_fastapi.md +└── cjs_to_esm.md +``` + +The **migration name** is the filename without `.md`. + +### Issue-based migrations + +Migrations can also be defined as GitHub issues with the `crane-migration` label. The issue body uses the same format as a `migration.md` file (Source, Target, Strategy, Verification sections). The **migration name** is derived from the issue title (slugified to lowercase with hyphens). + +The pre-step fetches open issues with the `crane-migration` label via the GitHub API and writes each issue body to a temporary file for scheduling. Issue-based migrations participate in the same scheduling and selection logic as file-based migrations. + +When a migration is issue-based, `/tmp/gh-aw/crane.json` includes: +- **`selected_issue`**: The issue number (e.g., `42`) if the selected migration came from an issue, or `null` if it came from a file. +- **`issue_migrations`**: A mapping of migration name → issue number for all issue-based migrations found. + +### Reading Migrations + +The pre-step has already determined which migration to run. Read `/tmp/gh-aw/crane.json` at the start of your run to get: + +- **`selected`**: The single migration name to run this iteration, or `null` if none are due. +- **`selected_file`**: The full path to the migration's markdown file (either `.crane/migrations//migration.md`, `.crane/migrations/.md`, or `/tmp/gh-aw/issue-migrations/.md` for issue-based migrations). +- **`selected_issue`**: The GitHub issue number if the selected migration came from an issue, or `null` if it came from a file. +- **`selected_target_metric`**: The `target-metric` value from the migration's frontmatter (a number, typically `1.0`), or `null` if the migration is open-ended. Used to check the [halting condition](#halting-condition) after each accepted iteration. +- **`selected_metric_direction`**: One of `"higher"` (default) or `"lower"`, parsed from the migration's `metric_direction` frontmatter field. Determines whether **larger** or **smaller** health-score values count as improvement. +- **`selected_strategy`**: The `strategy` value from the migration's frontmatter — one of `"in-place"`, `"greenfield"`, or `"auto"`. If `"auto"`, the agent must pick on the first iteration and write the chosen strategy back into the state file's Machine State table. +- **`state_file_size_bytes`** / **`state_file_max_bytes`**: For rolling-compaction decisions (see [Update Rules](#update-rules)). +- **`issue_migrations`**: A mapping of migration name → issue number for all discovered issue-based migrations. +- **`deferred`**: Other migrations that were due but will be handled in future runs. +- **`unconfigured`**: Migrations that still have the sentinel or placeholder content. +- **`skipped`**: Migrations not due yet based on their per-migration schedule, or completed/paused. +- **`no_migrations`**: If `true`, no migration files exist at all. +- **`not_due`**: If `true`, migrations exist but none are due for this run. +- **`head_branch`**: The canonical long-running branch name for the selected migration — always exactly `crane/{migration-name}`, never with a suffix or hash. Use this value verbatim. +- **`existing_pr`**: The number of the open draft PR for `crane/{migration-name}`, or `null` if no PR exists yet. + +If `selected` is not null: +1. Read the migration file from the `selected_file` path. +2. Parse the sections: Source, Target, Strategy, Verification. +3. Read the current state of all source and target paths. +4. Read the state file `{selected}.md` from the repo-memory folder. This contains the Machine State table, the Migration Plan, lessons, blockers, and iteration history. +5. If `selected_issue` is not null, also read the issue comments for any human steering input. + +## Multiple Migrations + +Crane supports **multiple independent migrations** in the same repository. Each is defined by a directory in `.crane/migrations/`, a markdown file in `.crane/migrations/`, or a GitHub issue with the `crane-migration` label. + +Each migration runs independently with its own: +- Source paths, target paths, strategy, and verification command +- Health score and best-score history +- Migration issue: `[Crane: {migration-name}]` (a single GitHub issue labeled `crane-migration` — created automatically for file-based migrations, the source issue for issue-based migrations — that hosts the status comment, per-iteration comments, and human steering) +- Long-running branch: `crane/{migration-name}` (persists across iterations) +- Single draft PR per migration: `[Crane: {migration-name}]` +- State file: `{migration-name}.md` in repo-memory (Machine State + Plan + history) + +**One migration per run**: On each scheduled trigger, the pre-step selects the **single most-overdue migration** (oldest `last_run`, with never-run migrations first). The agent runs one iteration for that migration only. + +### Per-Migration Schedule + +Migrations can specify their own schedule in YAML frontmatter: + +```markdown +--- +schedule: every 1h +--- + +# Migration +... +``` + +### Target Metric (Halting Condition) + +Migrations should usually specify `target-metric: 1.0` in the frontmatter — the typical "completed when fully migrated and verified" setting. When the health score reaches the target, the migration completes: the `crane-migration` label is removed, `crane-completed` is added (for issue-based migrations), and the state file is marked `Completed: true`. + +Migrations without a `target-metric` are **open-ended** and run indefinitely (rare for migrations — usually a sign you actually want goal-oriented). + +### Metric Direction + +By default, **higher is better** — `best_metric` is ratcheted up each accepted iteration. The recommended convention for `migration_score` is "higher is better" (0.0 = nothing migrated or correctness broken, 1.0 = fully migrated and verified), so the default suits Crane out of the box. + +Migrations whose verification naturally produces a "lower is better" metric (e.g. byte-diff against a parity corpus) can opt into reversed semantics with `metric_direction: lower`. Allowed values are `higher` (default) and `lower`. + +## Migration Definition + +Each migration file defines four things: + +1. **Source**: language, version, runtime, and paths being migrated *from* +2. **Target**: language(s), runtime, and paths being migrated *to* (a migration can have multiple target languages, e.g. TypeScript + Go core) +3. **Strategy**: `in-place`, `greenfield`, or `auto` +4. **Verification**: A command that outputs JSON containing `migration_score` + +### Setup Guard + +A template migration file is installed at `.crane/migrations/example.md`. **Migrations will not run until the user has edited them.** Each template contains a sentinel line: + +``` + +``` + +At the start of every run, check each migration file for this sentinel. For any migration where it is present: + +1. **Skip that migration — do not run any iterations for it.** +2. If no setup issue exists for that migration, create one titled `[Crane: {migration-name}] Action required: configure your migration`. + +## Strategy: in-place vs greenfield + +Crane operates in one of two strategy modes. The strategy lives in the migration's frontmatter and is mirrored in the state file's Machine State table. + +### `in-place` (strangler-fig) + +The system stays live and shippable throughout. Each milestone: + +1. Ports one unit (module, function family, route, layer) into the target language +2. Routes its callers through the new implementation — via direct imports, FFI, WASM, native add-on, or an HTTP/IPC bridge as appropriate +3. Deletes the old source-language implementation in the same commit +4. Leaves the build green, tests passing, and behavior unchanged for outside observers + +A milestone is **only** done when callers go through the new implementation and the old one is gone. Leaving both implementations in place "for safety" is forbidden — it accumulates dead code and defeats the strangler pattern. + +### `greenfield` + +The target is built up in parallel in separate paths. Each milestone: + +1. Ports one unit into the target paths +2. Adds parity tests that exercise the ported unit against the source-language equivalent on a corpus of inputs +3. Records the parity score in iteration history + +Cutover (switching real traffic from source to target) is a separate event that happens once parity is total. The source is **not** modified during the migration — it stays as the reference implementation until cutover. + +### `auto` + +If the migration's frontmatter sets `strategy: auto`, the agent picks on the first iteration. Decision rules: + +- Default to `in-place` for anything with external consumers, anything in production, anything large (>10 modules in scope), or anything where the test suite is the only safety net. +- Choose `greenfield` only when the source is self-contained, has no external consumers, is small (≤10 modules in scope), or is so tangled that interleaving the new language inside it would create more risk than a parallel rebuild. + +Write the chosen strategy and a one-paragraph rationale into the state file's **🧭 Strategy & Rationale** section, and update the Machine State table's `Strategy` field to the concrete choice (no longer `auto`). + +## Branching Model + +Each migration uses a **single long-running branch** named `crane/{migration-name}`. This branch persists across iterations — every accepted change is committed to it, building up the migration as a sequence of small, verified commits. + +### Branch Naming Convention + +``` +crane/{migration-name} +``` + +Examples: +- `crane/stats_py_to_ts` +- `crane/flask_to_fastapi` +- `crane/cjs_to_esm` + +> ⚠️ **CRITICAL — Branch Name Must Be Exact** +> +> The branch name is ALWAYS exactly `crane/{migration-name}` — **no suffixes, no hashes, no run IDs, no iteration numbers, no random tokens**. Never create branches like: +> - ❌ `crane/stats_py_to_ts-abc123` +> - ❌ `crane/stats_py_to_ts-iter42-deadbeef` +> - ❌ `crane/stats_py_to_ts-1234567890` +> +> **Never let the gh-aw framework auto-generate a branch name.** The pre-step provides the canonical name in the `head_branch` field of `/tmp/gh-aw/crane.json` — always use that value verbatim. + +### How It Works + +1. On the **first accepted iteration**, the branch is created from the default branch. +2. On **subsequent iterations**, the agent checks out the existing branch and ensures it is up to date with the default branch (fast-forward when possible, merge when truly diverged — see Step 3). +3. **Accepted iterations** are committed and pushed. Each commit message references the GitHub Actions run URL. +4. **Rejected or errored iterations** do not commit — changes are discarded. +5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR. +6. The branch may be **merged into the default branch** at any time. After merging, the branch continues to accumulate future iterations. + +### Cross-Linking + +Each migration has three coordinated resources: +- **Branch + PR**: `crane/{migration-name}` with a single draft PR +- **Migration Issue**: `[Crane: {migration-name}]` — a single GitHub issue (labeled `crane-migration`) that hosts the status comment, per-iteration comments, and human steering +- **State File**: `{migration-name}.md` in repo-memory — all state, plan, history, and lessons + +All three reference each other. The migration issue is created (or, for issue-based migrations, adopted) on the first run and updated with links to the PR and state file. + +## Iteration Loop + +Each run executes **one iteration for the single selected migration**. + +### Step 0: First-Iteration Bootstrap (Planning) + +**Only on the first iteration** — detected by `iteration_count == 0` in the state file (or the state file not existing yet): + +1. **Inventory the source**: list the modules under the migration's source paths, their inter-dependencies, their external consumers, and their test coverage. Record this in the state file's **🗺️ Inventory** section. +2. **Pick a strategy** if `selected_strategy == "auto"`: apply the decision rules in [Strategy: in-place vs greenfield](#strategy-in-place-vs-greenfield), write the choice and rationale into the state file's **🧭 Strategy & Rationale** section, and update the `Strategy` field in the Machine State table to the concrete choice. +3. **Generate the initial plan**: break the migration into ordered milestones in the **🪜 Milestones** section. Each milestone has: + - A short name (e.g. "Port `quantile` family") + - A scope (which functions/files/routes) + - An acceptance criterion (what verification it must pass — typically the parity test for that unit plus no regression in `migration_score`) + - A status (initially `todo`) +4. **Set the current focus**: pick the first milestone and put it in **🎯 Current Focus**. +5. **Do not yet implement any porting on iteration 0** — planning *is* the work for this iteration. Commit the plan and exit through Step 5c with `migration_score = 0.0` recorded but the iteration accepted as a planning step (skip the metric-improvement check on iteration 0). + +This Step 0 produces the plan and ships it as commit #1 on the migration branch (the plan file lives in `.crane/migrations//plan.md` — written to the migration branch — *and* in the state file on the memory branch, so it's visible both in the PR and on the memory branch). + +### Step 1: Read State + +1. Read the migration file to understand source, target, strategy, and verification. +2. Read the state file `{migration-name}.md` from the repo-memory folder. This contains: + - **⚙️ Machine State** table: scheduling and control fields the pre-step parses. + - **📋 Migration Info**: high-level summary (source, target, strategy, branch, PR, issue). + - **🗺️ Inventory**: modules, dependencies, consumers, test coverage, risk. + - **🧭 Strategy & Rationale**: chosen strategy and why. + - **🪜 Milestones**: ordered list of units to port, each with status. + - **🎯 Current Focus**: the milestone the next iteration will work on. + - **📚 Lessons Learned**. + - **🚧 Blockers & Foreclosed Approaches**. + - **🔭 Future Work**. + - **📊 Iteration History**. + + If the state file does not exist yet, this is the first iteration — go to Step 0. + +### Step 2: Analyze and Propose + +1. Read the source and target paths and the current Milestones. +2. Review **Lessons Learned**, **Blockers**, and **Current Focus** — what worked, what didn't, what the maintainer wants next. +3. **Pick the next concrete change**: + - Normally: implement whatever the **Current Focus** milestone calls for. Keep it small — one milestone, one iteration. Splitting a milestone into sub-iterations is fine and often necessary. + - If the **Current Focus** turns out to be too large for one iteration, split it: add sub-milestones to the **🪜 Milestones** section before implementing. + - If the **Current Focus** is blocked by something concrete (missing dependency, ambiguous behavior in the source, unclear API on the target side), move it to **🚧 Blockers** with a clear reason and pick a different milestone to focus on. +4. Describe the proposed change in your reasoning before implementing it. + +### Step 3: Implement + +1. Check out the migration's long-running branch `crane/{migration-name}`, syncing it with the default branch using the four-case decision tree below. Substitute `{migration-name}`: + + ```bash + git fetch origin main + if git ls-remote --exit-code origin crane/{migration-name}; then + git fetch origin crane/{migration-name} + ahead=$(git rev-list --count origin/main..origin/crane/{migration-name}) + behind=$(git rev-list --count origin/crane/{migration-name}..origin/main) + if [ "$ahead" = "0" ] && [ "$behind" != "0" ]; then + # Branch's commits already in main (typical after PR merge). Fast-forward + # the canonical branch to main to avoid noisy merge commits. + git checkout -B crane/{migration-name} origin/main + git push --force-with-lease origin crane/{migration-name} + elif [ "$ahead" != "0" ] && [ "$behind" != "0" ]; then + git checkout -B crane/{migration-name} origin/crane/{migration-name} + git merge origin/main --no-edit -m "Merge main into crane/{migration-name}" + else + git checkout -B crane/{migration-name} origin/crane/{migration-name} + fi + else + git checkout -b crane/{migration-name} origin/main + fi + ``` + + Use `--force-with-lease` (not `--force`) so concurrent pushes are rejected rather than overwritten. + +2. Make the proposed changes — restricted to: + - Files inside the source paths declared in the migration's **Source** section + - Files inside the target paths declared in the migration's **Target** section + - The migration's own `code/` directory (evaluator, parity corpus) — **only** if you're updating fixtures or adding new parity cases. **Never** modify the evaluator script after the migration's first iteration. + - The migration's `plan.md` if the migration is directory-based and you have a `plan.md` (mirrored from the state file's Plan sections) + +3. **Respect the migration constraints**: do not modify files outside the declared source/target paths. + +### Step 4: Verify + +1. Run the verification command specified in the migration file. +2. Parse the JSON output. The required field is `migration_score`. Optional fields (`progress`, `parity_passing`, `parity_total`, `source_tests_passing`, `target_tests_passing`, `perf_ratio`) are logged in iteration history. +3. Compare `migration_score` against `best_metric` from the state file. + +### Step 5: Accept or Reject + +Verification is necessary but **not sufficient** for acceptance. The agent's sandbox cannot reliably install many project toolchains, so a "score improved" signal from the sandbox can mask broken commits CI would catch. Acceptance must therefore be gated on **CI green** for the pushed HEAD commit. If CI fails, attempt to fix-and-retry within the same iteration rather than reverting. + +The accept path is split into three sub-steps: **5a (push and wait for CI)**, **5b (fix loop)**, **5c (accept)**. + +**If the score did not improve** (or held flat below `best_metric`), jump straight to the "score did not improve" path below — no push, no CI gate. + +#### Step 5a: Push and wait for CI + +**Only entered if the score improved**, or this is the first iteration establishing a baseline (Step 0 planning iteration), or `iteration_count == 0`. + +Improvement is **direction-aware**: +- If `selected_metric_direction` is `"higher"` (default): improved when `new_score > best_metric`. +- If `selected_metric_direction` is `"lower"`: improved when `new_score < best_metric`. + +The first run (no `best_metric` yet) always counts as an improvement. + +1. Commit the changes to the long-running branch with a commit message: + - Subject: `[Crane: {migration-name}] Iteration : ` + - Body (after a blank line): `Run: {run_url}` +2. Push the commit to the long-running branch. +3. **Find or create the PR** so CI runs and `gh pr checks` has a target. Follow these steps in order: + a. Check `existing_pr` from `/tmp/gh-aw/crane.json`. If it is not null, that is the existing draft PR — use it as `$EXISTING_PR` below; **never** call `create-pull-request`. + b. If `existing_pr` is null, also check the `PR` field in the state file's **⚙️ Machine State** table as a fallback. Verify it is still open via the GitHub API; if it has been closed or merged, treat it as if no PR exists and proceed to step (c). + c. If no PR exists (both sources are null): create one with `create-pull-request`, specifying `branch: crane/{migration-name}` (the value of `head_branch` from `crane.json`) explicitly. +4. Wait for CI on the new HEAD and reduce all check-runs to a single status — `success`, `failure`, or `pending`: + + ```bash + PR=${EXISTING_PR:-$(gh pr list --head crane/{migration-name} --json number -q '.[0].number')} + gh pr checks "$PR" --watch --interval 30 || true + status=$(gh pr checks "$PR" --json conclusion,state -q '.[] | (.conclusion // .state // "")' \ + | awk ' + BEGIN { r = "success" } + /^(FAILURE|CANCELLED|TIMED_OUT|ACTION_REQUIRED|STARTUP_FAILURE|STALE)$/ { r = "failure" } + /^(PENDING|QUEUED|IN_PROGRESS|WAITING|REQUESTED)$/ { if (r == "success") r = "pending" } + END { print r }') + ``` + + Treat `pending` as non-terminal: re-run `gh pr checks --watch` (subject to the wall-clock cap in Step 5b.7). + +5. If `status == "success"`, proceed to **Step 5c**. If `status == "failure"`, proceed to **Step 5b**. If `status == "pending"`, re-run this step. + +#### Step 5b: Fix loop (up to 5 attempts per iteration) + +If `status == "failure"`, **fix and retry — do not revert, do not accept**: + +1. **Fetch the failing check-run logs** for the pushed SHA. +2. **Extract a structured failure summary**: + - Failing job names and the first error line for each. + - **A failure signature** — a stable, normalized fingerprint (e.g., sorted failing-test names + the top error code). +3. **No-progress guard**: if this attempt's failure signature matches the previous attempt's signature, **stop**. Set `paused: true` with `pause_reason: "stuck in CI fix loop: "`, append `"ci-fix-exhausted"` to `recent_statuses`, comment on the migration issue, and end the iteration. +4. **Attempt the fix**: feed the structured failure summary back as the next sub-task. The agent commits the fix and pushes. +5. **Loop back to Step 5a** with the new HEAD. +6. **Budget: 5 fix attempts per iteration.** If the 5th attempt still leaves CI red, set `paused: true` with `pause_reason: "ci-fix-exhausted: "`, comment, end. +7. **Wall-clock cap: 60 min per iteration** including all CI waits. If exceeded mid-fix, set `paused: true` with `pause_reason: "ci-timeout"`, end the iteration. + +#### Step 5c: Accept + +**Only entered when `status == "success"`** from Step 5a (possibly after fix attempts in Step 5b). + +1. The commit(s) are already on the long-running branch. No further pushing needed. +2. If a draft PR does not already exist for this branch, create one — specify `branch: crane/{migration-name}` explicitly: + - Title: `[Crane: {migration-name}]` + - Body: summary of the migration (source → target, strategy), link to the migration issue, current best score and progress, AI disclosure: `🤖 *This PR is maintained by Crane. Each accepted iteration adds a commit to this branch.*` + If a draft PR already exists, use `push-to-pull-request-branch` (never `create-pull-request`). Update the PR body with the latest score and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what milestone was advanced, old score, new score, fix-attempt count if `> 0`, and a link to the actions run. +3. Ensure the migration issue exists (see [Migration Issue](#migration-issue) below) — for file-based migrations with no migration issue yet (`selected_issue` is null in `/tmp/gh-aw/crane.json`), create one and record its number in the state file's `Issue` field. +4. Update the state file `{migration-name}.md` in the repo-memory folder: + - **⚙️ Machine State** table: reset `consecutive_errors` to 0, set `best_metric` (the new `migration_score`), increment `iteration_count`, set `last_run` to current UTC, append `"accepted"` to `recent_statuses` (keep last 10), set `paused` to false. + - **🪜 Milestones**: update the relevant milestone's status — typically `done` if the milestone was fully completed, otherwise leave `in-progress` and update its notes. If the milestone is done, the next milestone in the list becomes the new **🎯 Current Focus**. + - Prepend an entry to **📊 Iteration History** with status ✅, score, **signed delta**, PR link, fix-attempt count if `> 0`, and a one-line summary of what milestone was advanced and how. + - Update **📚 Lessons Learned** if this iteration revealed something new (e.g. a bridging trick, a parity surprise, a perf trap). + - Update **🔭 Future Work** if this iteration opened new threads. +5. **Update the migration issue**: edit the status comment and post a per-iteration comment. +6. **Check halting condition** (see [Halting Condition](#halting-condition)): if `target-metric` is set, compare the new `best_metric` against it. For `higher` direction: completed when `best_metric >= target-metric`. When the target is met, mark the migration as completed. + +**If the score did not improve**: +1. Discard the code changes (do not commit them to the long-running branch). +2. Update the state file: + - **⚙️ Machine State**: increment `iteration_count`, set `last_run`, append `"rejected"` to `recent_statuses`. + - Prepend an entry to **📊 Iteration History** with status ❌, score, and a one-line summary of what was tried. + - If this approach is conclusively a dead end, add it to **🚧 Blockers & Foreclosed Approaches** with a clear explanation. Common foreclosed-approach patterns in migration: "tried to port X without first porting its dependency Y", "tried to bridge via Z but the boundary copies too much", "tried to inline the target into the source-side runtime but the type systems are incompatible". + - If the rejection points at a missing precondition (e.g. "this milestone needs Y to be ported first"), reorder the **🪜 Milestones** list — promote the precondition ahead of the current focus. +3. **Update the migration issue**. + +**If verification could not run** (build failure, missing dependencies, evaluator threw): +1. Discard the code changes. +2. Update the state file: + - **⚙️ Machine State**: increment `consecutive_errors`, increment `iteration_count`, set `last_run`, append `"error"` to `recent_statuses`. + - If `consecutive_errors` reaches 3+, set `paused: true` and `pause_reason: "consecutive errors"`, and create an issue describing the problem. + - Prepend an entry to **📊 Iteration History** with status ⚠️ and a brief error description. +3. **Update the migration issue**. + +#### Coordination with PR-health-keeper workflows + +If a repo ships a companion PR-health-keeper workflow, it can pick up paused Crane PRs using the `pause_reason` field — `ci-fix-exhausted: `, `stuck in CI fix loop: `, and `ci-timeout` are all signals the branch is red and needs an external nudge. Absent such a workflow, the loud pause + structured reason gives a human enough signal to intervene. + +## Migration Issue + +Each migration has **exactly one** open GitHub issue (labeled `crane-migration`) titled `[Crane: {migration-name}]`. This single issue is the source of truth for the migration — it hosts: + +- The **status comment** (the earliest bot comment, edited in place each iteration) — a dashboard of current state. +- A **per-iteration comment** for every iteration (accepted, rejected, or error) — the rolling log. +- **Human steering comments** — plain-prose comments from maintainers, treated by the agent as directives. + +### Auto-Creation for File-Based Migrations + +If `selected_issue` is `null` in `/tmp/gh-aw/crane.json`, the migration is file-based and has no migration issue yet. On the first run, create one with `create-issue`: + +- **Title**: `[Crane: {migration-name}]` +- **Body**: the contents of the migration file (`migration.md`) plus a placeholder for the status comment. +- **Labels**: `[crane-migration, automation, crane]`. + +Record the new issue number in the state file's `Issue` field. On subsequent runs, the pre-step discovers the existing migration issue automatically. + +For issue-based migrations, no creation is needed — the source issue is already the migration issue. + +### Status Comment + +On the **first iteration**, post a comment on the migration issue. On **every subsequent iteration**, update that same comment (edit it, do not post a new one). Find the status comment by searching for ``. If multiple comments contain this sentinel, use the earliest one. + +**Status comment format:** + +```markdown + +🤖 **Crane Status** + +| | | +|---|---| +| **Status** | 🟢 Active / ⏸️ Paused / ⚠️ Error / ✅ Completed | +| **Migration** | {source-language} → {target-languages} | +| **Strategy** | {in-place / greenfield} | +| **Best Score** | {best_metric} | +| **Progress** | {progress fraction or "—"} | +| **Milestones** | {done}/{total} done, {in_progress} in-progress, {blocked} blocked | +| **Target Metric** | {target_metric or "— (open-ended)"} | +| **Iterations** | {iteration_count} | +| **Last Run** | [{YYYY-MM-DD HH:MM UTC}]({run_url}) | +| **Branch** | [`crane/{migration-name}`](https://github.com/{owner}/{repo}/tree/crane/{migration-name}) | +| **Pull Request** | #{pr_number} | +| **State File** | [`{migration-name}.md`](https://github.com/{owner}/{repo}/blob/memory/crane/{migration-name}.md) | +| **Paused** | {true/false} ({pause_reason if paused}) | + +### Current Focus + +{milestone name and a one-sentence description of what the next iteration will tackle} + +### Summary + +{2-3 sentence summary of where the migration stands and what direction it is heading.} +``` + +### Per-Iteration Comment + +After **every iteration** (accepted, rejected, or error), post a **new comment**: + +```markdown +🤖 **Iteration {N}** — [{status_emoji} {status}]({run_url}) + +- **Milestone**: {milestone name, or "Planning" for iteration 0} +- **Change**: {one-line description of what was done} +- **Score**: {migration_score} (best: {best_metric}, delta: {+/-delta}) +- **Progress**: {progress fraction} {if provided by evaluator} +- **Parity**: {parity_passing}/{parity_total} {if provided} +- **Commit**: {short_sha} *(if accepted)* +- **Result**: {one-sentence summary of what this iteration revealed} +``` + +### Steering via Issue Comments + +**Human comments on the migration issue act as steering input** (in addition to the state file's Current Focus and Milestones sections). Before proposing a change, read all comments on the migration issue and treat any human (non-bot) comments since the last iteration as directives. + +### Migration Issue Rules + +- For issue-based migrations, the source issue body IS the migration definition — do not modify it (the user owns it). +- For file-based migrations, the migration issue body is informational and may be lightly updated, but the migration file (`migration.md`) remains the source of truth. +- The `crane-migration` label must remain on the issue for the migration to be discovered. When a migration completes, the label is removed and replaced with `crane-completed`. +- Closing the migration issue stops the migration from being discovered. Do NOT close the migration issue when the PR is merged — the branch continues to accumulate future iterations until the target metric is reached. +- Migration issues are labeled `[crane-migration, automation, crane]`. + +## Halting Condition + +Migrations are usually **goal-oriented** — you want to finish. Set `target-metric: 1.0` in the frontmatter and Crane stops the migration when the health score reaches 1.0 (which, with the recommended `correctness × progress` convention, means "fully migrated and verified"). + +### How It Works + +1. Parse the `target-metric` value from the migration's YAML frontmatter (if present). +2. After each **accepted** iteration, compare the new `best_metric` against the `target-metric`. +3. For `higher` direction (default): completed when `best_metric >= target-metric`. +4. For `lower` direction: completed when `best_metric <= target-metric`. +5. When completed: + - Set `Completed: true` in the Machine State table. + - Set `Completed Reason` to a human-readable message (e.g., `target metric 1.0 reached with value 1.0`). + - **For issue-based migrations**: remove the `crane-migration` label, add the `crane-completed` label. + - Update the status comment to ✅ Completed. + - Post a celebratory per-iteration comment: `🎉 **Migration complete!** {source} → {target} finished after {N} iterations.` + - The migration will not be selected for future runs. + +### Open-Ended Migrations + +Migrations that omit `target-metric` run indefinitely. Useful if you want Crane to keep optimizing a polyglot system long after the initial migration is done (e.g. continuously identifying new hot paths to lift into the native core), but unusual for a one-shot port. + +## State and Memory + +Crane uses the gh-aw **repo-memory** tool for persistent state. Each migration's state is a markdown file (`{migration-name}.md`) on the `memory/crane` branch. + +This means: +- Maintainers can see **everything** in the state file on the `memory/crane` branch: best score, last run, the migration plan, milestones, iteration history, lessons. +- Maintainers can **edit any section** to set priorities, add or reorder milestones, mark blockers as resolved, etc. +- The pre-step reads state files from the repo-memory directory to determine scheduling. +- The agent reads and writes state files in the repo-memory folder; changes are automatically committed and pushed after the workflow completes. + +## Repo Memory + +Crane uses the gh-aw `repo-memory` tool with branch `memory/crane` and file glob `*.md`. Each migration's state is stored as `{migration-name}.md` in the repo-memory folder. + +### Per-Migration State File + +When creating or updating a migration's state file, use this structure: + +```markdown +# Crane: {migration-name} + +🤖 *This file is maintained by the Crane agent. Maintainers may freely edit any section.* + +--- + +## ⚙️ Machine State + +> 🤖 *Updated automatically after each iteration. The pre-step scheduler reads this table — keep it accurate.* + +| Field | Value | +|-------|-------| +| Last Run | — | +| Iteration Count | 0 | +| Best Metric | — | +| Target Metric | — | +| Metric Direction | higher | +| Strategy | auto | +| Branch | `crane/{migration-name}` | +| PR | — | +| Issue | — | +| Paused | false | +| Pause Reason | — | +| Completed | false | +| Completed Reason | — | +| Consecutive Errors | 0 | +| Recent Statuses | — | + +--- + +## 📋 Migration Info + +**Source**: {source-language} ({version}) +**Target**: {target-languages joined} +**Strategy**: {chosen strategy} +**Branch**: [`crane/{migration-name}`](../../tree/crane/{migration-name}) +**Pull Request**: #{pr_number} +**Issue**: #{issue_number} + +--- + +## 🗺️ Inventory + +> Modules in scope, their dependencies and consumers, and notes on test coverage and risk. Generated on iteration 0, refined as the migration progresses. + +*(populated on first iteration)* + +--- + +## 🧭 Strategy & Rationale + +> Why `in-place` or `greenfield` was chosen. Refer to this whenever a milestone is unclear about whether to bridge or to fork. + +*(populated on first iteration)* + +--- + +## 🪜 Milestones + +> Ordered list of units to migrate. Each milestone has a name, scope, acceptance criterion, and status (`todo` / `in-progress` / `done` / `blocked`). Reorder freely as priorities shift. + +| # | Milestone | Scope | Acceptance | Status | +|---|---|---|---|---| +| 1 | *(populated on first iteration)* | | | todo | + +--- + +## 🎯 Current Focus + +The milestone the next iteration will work on, plus any human steering for it. + +*(populated on first iteration — defaults to the first `todo` milestone)* + +--- + +## 📚 Lessons Learned + +Key findings accumulated over iterations. + +- *(none yet)* + +--- + +## 🚧 Blockers & Foreclosed Approaches + +Approaches that have been tried and definitively ruled out, plus active blockers that have to be resolved before the relevant milestone can advance. + +- *(none yet)* + +--- + +## 🔭 Future Work + +Promising ideas surfaced but not yet promoted to milestones. Both the agent and maintainers contribute here. + +- *(none yet)* + +--- + +## 📊 Iteration History + +All iterations in reverse chronological order (newest first). + +*(No iterations yet.)* +``` + +### Machine State Field Reference + +| Field | Type | Description | +|-------|------|-------------| +| Last Run | ISO timestamp | UTC timestamp of the last iteration | +| Iteration Count | integer | Total iterations completed | +| Best Metric | number | Best `migration_score` achieved so far | +| Target Metric | number or `—` | Target score from frontmatter (halting condition). Typically `1.0` | +| Metric Direction | `higher` or `lower` | Whether larger or smaller values count as improvement. Defaults to `higher` | +| Strategy | `in-place` / `greenfield` / `auto` | The chosen strategy. After iteration 0 should never be `auto` | +| Branch | branch name | Long-running branch: `crane/{migration-name}` | +| PR | `#number` or `—` | Draft PR number | +| Issue | `#number` or `—` | Single migration issue | +| Paused | `true` or `false` | Whether the migration is paused | +| Pause Reason | text or `—` | `manual`, `consecutive errors`, `ci-fix-exhausted: `, `stuck in CI fix loop: `, `ci-timeout` | +| Completed | `true` or `false` | Whether the target metric has been reached | +| Completed Reason | text or `—` | e.g., `target metric 1.0 reached with value 1.0` | +| Consecutive Errors | integer | Count of consecutive verification failures | +| Recent Statuses | comma-separated | Last 10 outcomes: `accepted`, `rejected`, `error`, or `ci-fix-exhausted` | + +### Iteration History Entry Format + +After each iteration, prepend an entry to **📊 Iteration History**. Use `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}` for the run URL. + +```markdown +### Iteration {N} — {YYYY-MM-DD HH:MM UTC} — [Run](https://github.com/{owner}/{repo}/actions/runs/{run_id}) + +- **Status**: ✅ Accepted / ❌ Rejected / ⚠️ Error +- **Milestone**: {milestone name, or "Planning" for iteration 0} +- **Change**: {one-line description} +- **Score**: {value} (previous best: {previous_best}, delta: {signed-delta}) +- **Progress**: {fraction} *(if reported)* +- **Parity**: {parity_passing}/{parity_total} *(if reported)* +- **Commit**: {short_sha} *(if accepted)* +- **CI fix attempts**: {N} *(omit if 0)* +- **Notes**: {one or two sentences on what this iteration revealed} +``` + +### Update Rules + +- **Always** read the state file before proposing a change. It contains the plan you're executing. +- **Always** update the state file after each iteration. +- **Update the Machine State table first** — the scheduling pre-step depends on it. +- **Update Milestones** after every accepted iteration: mark `done`, promote sub-milestones, demote blocked ones. +- **Prepend** iteration history entries (newest first). +- **Accumulate** Lessons Learned — add new insights, don't overwrite existing ones. +- **Add to Blockers / Foreclosed Approaches** only when an approach is conclusively ruled out (not just rejected once) — and explain *why*. +- **Respect Current Focus** — if a maintainer has set or edited it, follow it in your next proposal. +- **Write the state file** to the repo-memory folder. Changes are automatically committed and pushed. +- **Keep the state file compact.** Must stay under `max-file-size` (default 40 KB — see `state_file_max_bytes` in `/tmp/gh-aw/crane.json`). When prepending a new iteration entry, collapse older iteration entries (beyond the most recent 10) into compressed summary lines: + + ```markdown + ### Iters 30–60 — ✅ (score 0.40→0.72, +12 milestones done): brief summary of what was ported across this range + ``` + + Also prune Lessons Learned to the most relevant entries, and consolidate similar Blockers entries. If `state_file_size_bytes` is already > 80% of `state_file_max_bytes`, **compact aggressively** this iteration: collapse to the most recent 5 detailed entries, merge older compressed ranges into broader bands, and trim verbose milestone notes. + +## Guidelines + +- **One milestone per iteration** when possible. Split big milestones into sub-milestones rather than landing huge commits. +- **Keep the build green every iteration.** For `in-place` migrations, the system must keep working — no half-ported modules left lying around between iterations. +- **The evaluator is sacred.** Never modify the verification script after the migration's first iteration. Updating fixtures or adding new parity cases is fine; rewriting the scoring is not. +- **Repo-memory state file is the single source of truth.** Plan, milestones, history, lessons — all live there. Keep it up to date. +- **Read the state file before every proposal.** Foreclosed Approaches and Lessons Learned exist to prevent repeating failures. +- **Respect human input.** Current Focus and any human comments on the migration issue are directives — follow them. +- **Diminishing returns.** If the last 5 consecutive iterations were rejected, post a comment suggesting the user review the milestone list or change the strategy. +- **Transparency.** Every PR and comment includes AI disclosure with 🤖. +- **Safety.** Never modify files outside the migration's declared source/target paths. Never modify the verification script after iteration 1. Never modify the migration definition (except via `/crane` command mode). +- **Read AGENTS.md first**: before starting work, read the repository's `AGENTS.md` file (if present) for project-specific conventions. + +## Common Mistakes to Avoid + +> ❌ **Do NOT create a new branch with a suffix for each iteration.** +> Correct: `crane/stats_py_to_ts` +> Wrong: `crane/stats_py_to_ts-abc123`, `crane/stats_py_to_ts-iter42` +> Use the `head_branch` field from `/tmp/gh-aw/crane.json` verbatim. + +> ❌ **Do NOT create a new PR if one already exists for `crane/{migration-name}`.** +> The pre-step provides `existing_pr` in `/tmp/gh-aw/crane.json`. If not null, **always** use `push-to-pull-request-branch`. + +> ❌ **Do NOT leave both source and target implementations in an `in-place` migration "for safety".** +> A milestone is only `done` when callers go through the new implementation and the old one is gone. Dual-implementation accumulates dead code and defeats the strangler-fig pattern. + +> ❌ **Do NOT modify the verification script after the first iteration.** +> The evaluator is the migration's scoreboard. Changing it mid-flight invalidates all prior iterations and breaks the ratchet. + +> ❌ **Do NOT skip the planning iteration (Step 0).** +> Crane's first job is to plan, not to port. The Step 0 commit is the migration's foundation — every later iteration reads from it. Trying to "just start porting" produces incoherent migrations. + +> ❌ **Do NOT modify files outside the migration's declared source/target paths.** +> The Source and Target sections are the allowlist. Touching anything else — including the migration definition itself — is forbidden outside command mode. diff --git a/.github/workflows/scripts/crane_scheduler.py b/.github/workflows/scripts/crane_scheduler.py new file mode 100644 index 00000000..ff83514f --- /dev/null +++ b/.github/workflows/scripts/crane_scheduler.py @@ -0,0 +1,754 @@ +#!/usr/bin/env python3 +"""Crane scheduler. + +Decides which Crane migration (if any) is due for an iteration. Reads +migration definitions from ``.crane/migrations/`` (directory- and bare- +markdown-based) and from open GitHub issues labelled ``crane-migration``, +combines them with persisted per-migration scheduling state from the +``memory/crane`` repo-memory branch, and writes the selection to +``/tmp/gh-aw/crane.json`` for the agent step to consume. + +Side effects: + * May bootstrap ``.crane/migrations/example.md`` on first run. + * May materialise issue-based migration bodies under + ``/tmp/gh-aw/issue-migrations/``. + * Always writes ``/tmp/gh-aw/crane.json``. + +Exit codes: + 0 - a migration was selected, or there are unconfigured migrations to + report on (the agent step should run). + 1 - nothing to do this run (no due migrations, no unconfigured + migrations); the workflow should skip the agent step. + +Environment variables: + GITHUB_TOKEN - token used to query the issues API. + GITHUB_REPOSITORY - ``owner/repo`` slug. + CRANE_MIGRATION - optional migration name to force (bypasses + scheduling, but unconfigured migrations are still + rejected). + +This file is the standalone counterpart of the scheduler used by +``workflows/crane.md``. Extracting it keeps the compiled ``run:`` step +small and makes the logic unit-testable. +""" + +from __future__ import annotations + +import glob +import json +import os +import re +import sys +import urllib.error +import urllib.parse +import urllib.request +from datetime import datetime, timedelta, timezone + +MIGRATIONS_DIR = ".crane/migrations" +TEMPLATE_FILE = os.path.join(MIGRATIONS_DIR, "example.md") + +# 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/crane -> crane). +REPO_MEMORY_DIR = "/tmp/gh-aw/repo-memory/crane" + +ISSUE_MIGRATIONS_DIR = "/tmp/gh-aw/issue-migrations" +OUTPUT_DIR = "/tmp/gh-aw" +OUTPUT_FILE = os.path.join(OUTPUT_DIR, "crane.json") + +# Default repo-memory ``max-file-size`` for state files. Mirrors the value +# configured under ``tools.repo-memory.max-file-size`` in +# ``workflows/crane.md``. Surfaced so the agent prompt can reason about the +# rolling-compaction budget without re-parsing workflow frontmatter. +STATE_FILE_MAX_BYTES = 40960 + + +# --------------------------------------------------------------------------- +# Pure helpers (unit-tested directly) +# --------------------------------------------------------------------------- + + +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 parse_schedule(s): + """Schedule string to a ``timedelta``; returns ``None`` for invalid input.""" + 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 + + +def get_migration_name(pf): + """Extract migration name from a migration file path. + + Directory-based: ``.crane/migrations//migration.md`` -> ```` + Bare markdown: ``.crane/migrations/.md`` -> ```` + Issue-based: ``/tmp/gh-aw/issue-migrations/.md`` -> ```` + """ + if pf.endswith("/migration.md"): + return os.path.basename(os.path.dirname(pf)) + return os.path.splitext(os.path.basename(pf))[0] + + +def slugify_issue_title(title, number=None): + """Slugify a GitHub issue title into a migration name.""" + slug = re.sub(r"[^a-z0-9]+", "-", (title or "").lower()).strip("-") + slug = re.sub(r"-+", "-", slug) # collapse consecutive hyphens + if not slug: + slug = "issue-{}".format(number) if number is not None else "issue" + return slug + + +def parse_link_header(header): + """Parse the GitHub API ``Link`` header and return the ``rel="next"`` URL.""" + if not header: + return None + for part in header.split(","): + section = part.strip() + m = re.match(r'^<([^>]+)>;\s*rel="next"$', section) + if m: + return m.group(1) + return None + + +def parse_migration_frontmatter(content): + """Parse YAML frontmatter for ``schedule``, ``target-metric``, ``metric_direction``, and ``strategy``. + + Returns a dict with these keys (any may be ``None``): + schedule_delta - timedelta or None + target_metric - float or None + target_metric_invalid - raw string (if value was invalid) + metric_direction - "higher" or "lower" (default "higher") + metric_direction_invalid - raw string (if value was invalid) + strategy - "in-place", "greenfield", or "auto" (default "auto") + strategy_invalid - raw string (if value was invalid) + """ + # Strip leading HTML comments before checking (issue-based migrations may have them). + content_stripped = re.sub(r"^(\s*\s*\n)*", "", content, flags=re.DOTALL) + result = { + "schedule_delta": None, + "target_metric": None, + "target_metric_invalid": None, + "metric_direction": "higher", + "metric_direction_invalid": None, + "strategy": "auto", + "strategy_invalid": None, + } + fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content_stripped, re.DOTALL) + if not fm_match: + return result + for line in fm_match.group(1).split("\n"): + stripped = line.strip() + if stripped.startswith("schedule:"): + schedule_str = line.split(":", 1)[1].strip() + result["schedule_delta"] = parse_schedule(schedule_str) + if stripped.startswith("target-metric:"): + raw = line.split(":", 1)[1].strip() + try: + result["target_metric"] = float(raw) + except (ValueError, TypeError): + result["target_metric_invalid"] = raw + if stripped.startswith("metric_direction:") or stripped.startswith("metric-direction:"): + raw = line.split(":", 1)[1].strip().strip('"').strip("'").lower() + if raw in ("higher", "lower"): + result["metric_direction"] = raw + else: + result["metric_direction_invalid"] = raw + if stripped.startswith("strategy:"): + raw = line.split(":", 1)[1].strip().strip('"').strip("'").lower() + if raw in ("in-place", "greenfield", "auto"): + result["strategy"] = raw + else: + result["strategy_invalid"] = raw + return result + + +def is_unconfigured(content): + """Return True if a migration file still contains the unconfigured sentinel + or any TODO/REPLACE placeholder.""" + if "" in content: + return True + if re.search(r"\bTODO\b|\bREPLACE", content): + return True + return False + + +def check_skip_conditions(state): + """Return ``(should_skip, reason)`` based on the migration state.""" + if str(state.get("completed", "")).lower() == "true" or state.get("completed") is True: + return True, "completed: target metric reached" + if state.get("paused"): + return True, "paused: {}".format(state.get("pause_reason", "unknown")) + recent = state.get("recent_statuses", [])[-5:] + if len(recent) >= 5 and all(s == "rejected" for s in recent): + return True, "plateau: 5 consecutive rejections" + return False, None + + +# --------------------------------------------------------------------------- +# I/O helpers +# --------------------------------------------------------------------------- + + +def read_migration_state(migration_name, repo_memory_dir=REPO_MEMORY_DIR): + """Read scheduling state from the repo-memory state file (or ``{}``).""" + state_file = os.path.join(repo_memory_dir, "{}.md".format(migration_name)) + if not os.path.isfile(state_file): + print(" {}: no state file found (first run)".format(migration_name)) + return {} + with open(state_file, encoding="utf-8") as f: + content = f.read() + return parse_machine_state(content) + + +def get_state_file_size(migration_name, repo_memory_dir=REPO_MEMORY_DIR): + """Return the size of the migration's state file in bytes (0 if missing).""" + state_file = os.path.join(repo_memory_dir, "{}.md".format(migration_name)) + try: + st = os.stat(state_file) + except OSError: + return 0 + return st.st_size + + +def _bootstrap_template_if_missing(): + """Create ``.crane/migrations/example.md`` if the directory is missing.""" + if os.path.isdir(MIGRATIONS_DIR): + return + os.makedirs(MIGRATIONS_DIR, exist_ok=True) + bt = chr(96) # backtick — keep gh-aw compiler happy if this ever gets inlined + template = "\n".join([ + "", + "", + "", + "", + "---", + "schedule: every 6h", + "strategy: auto", + "source-language: REPLACE", + "target-languages: [REPLACE]", + "target-metric: 1.0", + "metric_direction: higher", + "---", + "", + "# Crane Migration", + "", + "", + "", + "## Source", + "", + "- **Language**: REPLACE", + "- **Runtime**: REPLACE", + "- **Paths**:", + " - {bt}REPLACE/path{bt} -- (what lives here)".format(bt=bt), + "", + "## Target", + "", + "- **Languages**: REPLACE", + "- **Runtime**: REPLACE", + "- **Paths**:", + " - {bt}REPLACE/path{bt} -- (what should live here)".format(bt=bt), + "", + "## Strategy", + "", + "REPLACE -- explain choice (or leave as `auto` in frontmatter and let Crane decide).", + "", + "## Verification", + "", + "", + "", + "{bt}{bt}{bt}bash".format(bt=bt), + "REPLACE_WITH_YOUR_VERIFICATION_COMMAND", + "{bt}{bt}{bt}".format(bt=bt), + "", + "The metric is {bt}migration_score{bt}. **Higher is better.**".format(bt=bt), + "", + ]) + with open(TEMPLATE_FILE, "w") as f: + f.write(template) + # Leave the template unstaged — the agent will create a draft PR with it + print("BOOTSTRAPPED: created {} locally (agent will create a draft PR)".format(TEMPLATE_FILE)) + + +def _scan_directory_migrations(): + """Return paths of directory-based migrations under ``MIGRATIONS_DIR``.""" + out = [] + if not os.path.isdir(MIGRATIONS_DIR): + return out + for entry in sorted(os.listdir(MIGRATIONS_DIR)): + mig_dir = os.path.join(MIGRATIONS_DIR, entry) + if os.path.isdir(mig_dir): + mig_file = os.path.join(mig_dir, "migration.md") + if os.path.isfile(mig_file): + out.append(mig_file) + return out + + +def _scan_bare_migrations(): + """Return paths of bare-markdown migrations under ``MIGRATIONS_DIR``.""" + return sorted(glob.glob(os.path.join(MIGRATIONS_DIR, "*.md"))) + + +def _fetch_issue_migrations(repo, github_token): + """Fetch open issues with the ``crane-migration`` label and write their + bodies to ``ISSUE_MIGRATIONS_DIR``. Returns ``(migration_files, issue_migrations)``. + + Errors are swallowed (with a warning) so a transient API failure doesn't + block the run for non-issue-based migrations. + """ + migration_files = [] + issue_migrations = {} + os.makedirs(ISSUE_MIGRATIONS_DIR, exist_ok=True) + next_url = ( + "https://api.github.com/repos/{}/issues" + "?labels=crane-migration&state=open&per_page=100".format(repo) + ) + headers = { + "Authorization": "token {}".format(github_token), + "Accept": "application/vnd.github.v3+json", + } + issues = [] + try: + while next_url: + req = urllib.request.Request(next_url, headers=headers) + with urllib.request.urlopen(req, timeout=30) as resp: + page = json.loads(resp.read().decode()) + link_header = resp.headers.get("link") or resp.headers.get("Link") + issues.extend(page) + next_url = parse_link_header(link_header) + for issue in issues: + if issue.get("pull_request"): + continue # skip PRs + body = issue.get("body") or "" + title = issue.get("title") or "" + number = issue["number"] + slug = slugify_issue_title(title, number) + if slug in issue_migrations: + print( + " Warning: slug '{}' (issue #{}) collides with issue #{}, " + "appending issue number".format( + slug, number, issue_migrations[slug]["issue_number"] + ) + ) + slug = "{}-{}".format(slug, number) + issue_file = os.path.join(ISSUE_MIGRATIONS_DIR, "{}.md".format(slug)) + with open(issue_file, "w") as f: + f.write(body) + migration_files.append(issue_file) + issue_migrations[slug] = {"issue_number": number, "file": issue_file, "title": title} + print(" Found issue-based migration: '{}' (issue #{})".format(slug, number)) + except Exception as e: # noqa: BLE001 -- best-effort; logged below + print(" Warning: could not fetch issue-based migrations: {}".format(e)) + return migration_files, issue_migrations + + +def _parse_target_metric_from_file(path): + """Re-parse a migration file to extract its ``target-metric``, if any.""" + try: + with open(path) as f: + return parse_migration_frontmatter(f.read()).get("target_metric") + except (OSError, ValueError, TypeError): + return None + + +def _parse_metric_direction_from_file(path): + """Re-parse a migration file to extract its ``metric_direction``.""" + try: + with open(path) as f: + return parse_migration_frontmatter(f.read()).get("metric_direction") or "higher" + except (OSError, ValueError, TypeError): + return "higher" + + +def _parse_strategy_from_file(path): + """Re-parse a migration file to extract its ``strategy``.""" + try: + with open(path) as f: + return parse_migration_frontmatter(f.read()).get("strategy") or "auto" + except (OSError, ValueError, TypeError): + return "auto" + + +# --------------------------------------------------------------------------- +# Existing PR lookup (single-PR-per-migration invariant) +# --------------------------------------------------------------------------- + + +def _http_get_json(url, headers, timeout=30): + """Open ``url`` and return ``(parsed_body, link_header)``. + + Returns ``(None, None)`` on any HTTP/network error so callers can fall + through to the next strategy. + """ + try: + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as resp: + body = json.loads(resp.read().decode()) + link_header = resp.headers.get("link") or resp.headers.get("Link") + return body, link_header + except (urllib.error.URLError, urllib.error.HTTPError, ValueError, OSError): + return None, None + + +def find_existing_pr_for_branch(repo, migration_name, github_token, http_get_json=_http_get_json): + """Look up the open draft PR (if any) for ``crane/{migration_name}``.""" + if not repo or not migration_name or not github_token: + return None + owner = repo.split("/", 1)[0] + canonical_branch = "crane/{}".format(migration_name) + headers = { + "Authorization": "token {}".format(github_token), + "Accept": "application/vnd.github.v3+json", + } + # Strategy 1: exact canonical branch name via the head= filter. + head_q = urllib.parse.quote("{}:{}".format(owner, canonical_branch), safe="") + url = "https://api.github.com/repos/{}/pulls?head={}&state=open".format(repo, head_q) + body, _ = http_get_json(url, headers) + if isinstance(body, list) and body: + first = body[0] + if isinstance(first, dict) and first.get("number"): + return first["number"] + + # Strategy 2: paginate open PRs and match either a legacy framework-suffixed + # branch (``crane/{name}-<6-40 hex>``) or a ``[Crane: {name}]`` title prefix. + suffix_regex = re.compile( + r"^crane/" + re.escape(migration_name) + r"(-[0-9a-f]{6,40})?$" + ) + title_prefix = "[Crane: {}]".format(migration_name) + next_url = "https://api.github.com/repos/{}/pulls?state=open&per_page=100".format(repo) + while next_url: + body, link_header = http_get_json(next_url, headers) + if not isinstance(body, list): + break + for pr in body: + if not isinstance(pr, dict): + continue + head_ref = "" + head = pr.get("head") or {} + if isinstance(head, dict): + head_ref = head.get("ref") or "" + if suffix_regex.match(head_ref): + return pr.get("number") + title = pr.get("title") + if isinstance(title, str) and title.startswith(title_prefix): + return pr.get("number") + next_url = parse_link_header(link_header) + return None + + +# --------------------------------------------------------------------------- +# Selection +# --------------------------------------------------------------------------- + + +def select_migration(due, forced_migration=None, all_migrations=None, unconfigured=None, issue_migrations=None): + """Pick the migration to run. + + Returns ``(selected, selected_file, selected_issue, selected_target_metric, + selected_metric_direction, selected_strategy, deferred, error)``. + """ + all_migrations = all_migrations or {} + unconfigured = unconfigured or [] + issue_migrations = issue_migrations or {} + if forced_migration: + if forced_migration not in all_migrations: + return ( + None, None, None, None, "higher", "auto", [], + "requested migration '{}' not found. Available migrations: {}".format( + forced_migration, list(all_migrations.keys()) + ), + ) + if forced_migration in unconfigured: + return ( + None, None, None, None, "higher", "auto", [], + "requested migration '{}' is unconfigured (has placeholders).".format( + forced_migration + ), + ) + selected = forced_migration + selected_file = all_migrations[forced_migration] + deferred = [p["name"] for p in due if p["name"] != forced_migration] + selected_issue = ( + issue_migrations[selected]["issue_number"] if selected in issue_migrations else None + ) + selected_target_metric = None + selected_metric_direction = None + selected_strategy = None + for p in due: + if p["name"] == forced_migration: + selected_target_metric = p.get("target_metric") + selected_metric_direction = p.get("metric_direction") + selected_strategy = p.get("strategy") + break + if selected_target_metric is None: + selected_target_metric = _parse_target_metric_from_file(selected_file) + if selected_metric_direction is None: + selected_metric_direction = _parse_metric_direction_from_file(selected_file) + if selected_strategy is None: + selected_strategy = _parse_strategy_from_file(selected_file) + return ( + selected, + selected_file, + selected_issue, + selected_target_metric, + selected_metric_direction, + selected_strategy, + deferred, + None, + ) + + if due: + # Normal scheduling: pick the single most-overdue migration. + # ``last_run`` of None/empty sorts first (never run). + due_sorted = sorted(due, key=lambda p: p["last_run"] or "") + selected = due_sorted[0]["name"] + selected_file = due_sorted[0]["file"] + selected_target_metric = due_sorted[0].get("target_metric") + selected_metric_direction = due_sorted[0].get("metric_direction") or "higher" + selected_strategy = due_sorted[0].get("strategy") or "auto" + deferred = [p["name"] for p in due_sorted[1:]] + selected_issue = ( + issue_migrations[selected]["issue_number"] if selected in issue_migrations else None + ) + return ( + selected, + selected_file, + selected_issue, + selected_target_metric, + selected_metric_direction, + selected_strategy, + deferred, + None, + ) + + return None, None, None, None, "higher", "auto", [], None + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main(): + github_token = os.environ.get("GITHUB_TOKEN", "") + repo = os.environ.get("GITHUB_REPOSITORY", "") + forced_migration = os.environ.get("CRANE_MIGRATION", "").strip() + + _bootstrap_template_if_missing() + + # Find all migration files from all locations: + # 1. Directory-based: .crane/migrations//migration.md (preferred) + # 2. Bare markdown: .crane/migrations/.md (simple) + # 3. Issue-based: GitHub issues with the 'crane-migration' label + migration_files = [] + migration_files.extend(_scan_directory_migrations()) + migration_files.extend(_scan_bare_migrations()) + issue_files, issue_migrations = _fetch_issue_migrations(repo, github_token) + migration_files.extend(issue_files) + + if not migration_files: + # Fallback to single-file locations + for path in [".crane/migration.md", "migration.md"]: + if os.path.isfile(path): + migration_files = [path] + break + + os.makedirs(OUTPUT_DIR, exist_ok=True) + + if not migration_files: + print("NO_MIGRATIONS_FOUND") + with open(OUTPUT_FILE, "w") as f: + json.dump( + { + "due": [], + "skipped": [], + "unconfigured": [], + "no_migrations": True, + "head_branch": None, + "existing_pr": None, + }, + f, + ) + sys.exit(0) + + now = datetime.now(timezone.utc) + due = [] + skipped = [] + unconfigured = [] + all_migrations = {} # name -> file path + + for pf in migration_files: + name = get_migration_name(pf) + all_migrations[name] = pf + with open(pf) as f: + content = f.read() + + if is_unconfigured(content): + unconfigured.append(name) + continue + + fm = parse_migration_frontmatter(content) + schedule_delta = fm["schedule_delta"] + target_metric = fm["target_metric"] + metric_direction = fm["metric_direction"] + strategy = fm["strategy"] + if fm["target_metric_invalid"] is not None: + print(" Warning: {} has invalid target-metric value: {}".format(name, fm["target_metric_invalid"])) + if fm["metric_direction_invalid"] is not None: + print( + " Warning: {} has invalid metric_direction value: {!r} (must be 'higher' or 'lower'); defaulting to 'higher'".format( + name, fm["metric_direction_invalid"] + ) + ) + if fm["strategy_invalid"] is not None: + print( + " Warning: {} has invalid strategy value: {!r} (must be 'in-place', 'greenfield', or 'auto'); defaulting to 'auto'".format( + name, fm["strategy_invalid"] + ) + ) + + # Read state from repo-memory + state = read_migration_state(name) + if state: + print( + " {}: last_run={}, iteration_count={}".format( + name, state.get("last_run"), state.get("iteration_count") + ) + ) + else: + print(" {}: no state found (first run)".format(name)) + + last_run = None + lr = state.get("last_run") + if lr: + try: + last_run = datetime.fromisoformat(lr.replace("Z", "+00:00")) + except ValueError: + pass + + should_skip, reason = check_skip_conditions(state) + if should_skip: + skipped.append({"name": name, "reason": reason}) + continue + + # Check if due based on per-migration schedule + if schedule_delta and last_run and 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, + "metric_direction": metric_direction, + "strategy": strategy, + }) + + selected, selected_file, selected_issue, selected_target_metric, selected_metric_direction, selected_strategy, deferred, error = ( + select_migration(due, forced_migration, all_migrations, unconfigured, issue_migrations) + ) + + if error: + print("ERROR: {}".format(error)) + sys.exit(1) + + if forced_migration and selected: + print("FORCED: running migration '{}' (manual dispatch)".format(forced_migration)) + + # Look up the existing draft PR (if any) for the selected migration. + head_branch = None + existing_pr = None + if selected: + head_branch = "crane/{}".format(selected) + try: + existing_pr = find_existing_pr_for_branch(repo, selected, github_token) + except Exception as e: # noqa: BLE001 -- best-effort lookup + print(" Warning: existing PR lookup failed for {}: {}".format(selected, e)) + existing_pr = None + + result = { + "selected": selected, + "selected_file": selected_file, + "selected_issue": selected_issue, + "selected_target_metric": selected_target_metric, + "selected_metric_direction": selected_metric_direction, + "selected_strategy": selected_strategy, + "state_file_size_bytes": get_state_file_size(selected) if selected else 0, + "state_file_max_bytes": STATE_FILE_MAX_BYTES, + "issue_migrations": { + name: info["issue_number"] for name, info in issue_migrations.items() + }, + "deferred": deferred, + "skipped": skipped, + "unconfigured": unconfigured, + "no_migrations": False, + "head_branch": head_branch, + "existing_pr": existing_pr, + } + + with open(OUTPUT_FILE, "w") as f: + json.dump(result, f, indent=2) + + print("=== Crane Migration Check ===") + print("Selected migration: {} ({})".format(selected or "(none)", selected_file or "n/a")) + print("Deferred (next run): {}".format(deferred or "(none)")) + print("Migrations skipped: {}".format([s["name"] for s in skipped] or "(none)")) + print("Migrations unconfigured: {}".format(unconfigured or "(none)")) + + if not selected and not unconfigured: + print("\nNo migrations due this run. Exiting early.") + sys.exit(1) # Non-zero exit skips the agent step + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/shared/reporting.md b/.github/workflows/shared/reporting.md new file mode 100644 index 00000000..f1a4ddba --- /dev/null +++ b/.github/workflows/shared/reporting.md @@ -0,0 +1,45 @@ +## Report Formatting + +Follow the content structure and formatting guidelines from the imported formatting fragment above. + +## Reporting Workflow Run Information + +When analyzing workflow run logs or reporting information from GitHub Actions runs: + +### 1. Workflow Run ID Formatting + +**Always render workflow run IDs as clickable URLs** when mentioning them in your report. The workflow run data includes a `url` field that provides the full GitHub Actions run page URL. + +**Format:** + +`````markdown +[§12345](https://github.com/owner/repo/actions/runs/12345) +````` + +**Example:** + +`````markdown +Analysis based on [§456789](https://github.com/github/gh-aw/actions/runs/456789) +````` + +### 2. Document References for Workflow Runs + +When your analysis is based on information mined from one or more workflow runs, **include up to 3 workflow run URLs as document references** at the end of your report. + +**Format:** + +`````markdown +--- + +**References:** +- [§12345](https://github.com/owner/repo/actions/runs/12345) +- [§12346](https://github.com/owner/repo/actions/runs/12346) +- [§12347](https://github.com/owner/repo/actions/runs/12347) +````` + +**Guidelines:** + +- Include **maximum 3 references** to keep reports concise +- Choose the most relevant or representative runs (e.g., failed runs, high-cost runs, or runs with significant findings) +- Always use the actual URL from the workflow run data (specifically, use the `url` field from `RunData` or the `RunURL` field from `ErrorSummary`) +- If analyzing more than 3 runs, select the most important ones for references From 2b925e446acdbaf653eaee0bf1dd378a51d4ab5f Mon Sep 17 00:00:00 2001 From: mrjf Date: Thu, 21 May 2026 07:41:48 -0700 Subject: [PATCH 2/3] Fix review comments: replace all non-ASCII with ASCII equivalents - Replace emojis with ASCII bracket notation ([+], [x], [!], [*], etc.) - Replace em/en dashes with -- / - - Replace arrows with -> / <- - Replace box-drawing chars with ASCII art - Replace section sign with # - Add missing setup-cli lock entry in actions-lock.json - Recompile crane.lock.yml Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/crane-migration.md | 12 +- .github/agents/agentic-workflows.agent.md | 24 +- .github/aw/actions-lock.json | 5 + .github/workflows/crane.lock.yml | 8 +- .github/workflows/crane.md | 290 +++++++++---------- .github/workflows/scripts/crane_scheduler.py | 10 +- .github/workflows/shared/reporting.md | 10 +- 7 files changed, 182 insertions(+), 177 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/crane-migration.md b/.github/ISSUE_TEMPLATE/crane-migration.md index 77af0d66..ed31443d 100644 --- a/.github/ISSUE_TEMPLATE/crane-migration.md +++ b/.github/ISSUE_TEMPLATE/crane-migration.md @@ -28,8 +28,8 @@ metric_direction: higher - **Language**: REPLACE (e.g. Python 3.11) - **Runtime**: REPLACE (e.g. CPython) - **Paths**: - - `REPLACE/path/to/source` — (what lives here) - - `REPLACE/path/to/source-tests` — existing test suite + - `REPLACE/path/to/source` -- (what lives here) + - `REPLACE/path/to/source-tests` -- existing test suite ## Target @@ -38,14 +38,14 @@ metric_direction: higher - **Languages**: REPLACE (e.g. TypeScript, Go) - **Runtime**: REPLACE (e.g. Node 22 / Bun 1.x) - **Paths**: - - `REPLACE/path/to/target` — (what should live here) + - `REPLACE/path/to/target` -- (what should live here) - **Bridge** *(if polyglot)*: REPLACE (e.g. "Go core compiled to WASM, called from TypeScript through a thin wrapper") ## Strategy -REPLACE — explain why this strategy fits. +REPLACE -- explain why this strategy fits. - `in-place` (strangler-fig): system stays live throughout. Each milestone ports one unit and re-routes callers. Preferred for production code or anything with external consumers. - `greenfield`: target built in parallel; cutover after parity is total. Best for small, self-contained sources. @@ -53,13 +53,13 @@ REPLACE — explain why this strategy fits. ## Verification - + ```bash REPLACE_WITH_YOUR_VERIFICATION_COMMAND ``` -The metric is `migration_score` (0.0–1.0). **Higher is better.** Optional companion fields: `progress`, `parity_passing`, `parity_total`, `source_tests_passing`, `target_tests_passing`, `perf_ratio`. +The metric is `migration_score` (0.0-1.0). **Higher is better.** Optional companion fields: `progress`, `parity_passing`, `parity_total`, `source_tests_passing`, `target_tests_passing`, `perf_ratio`. ## Out of scope diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md index 98908e39..899a64c7 100644 --- a/.github/agents/agentic-workflows.agent.md +++ b/.github/agents/agentic-workflows.agent.md @@ -15,14 +15,14 @@ This is a **dispatcher agent** that routes your request to the appropriate speci - **Updating existing workflows**: Routes to `update` prompt - **Debugging workflows**: Routes to `debug` prompt - **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt -- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments +- **Creating report-generating workflows**: Routes to `report` prompt -- consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments - **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt -- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes -- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs -- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions -- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command -- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments -- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows +- **Fixing Dependabot PRs**: Routes to `dependabot` prompt -- use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes +- **Analyzing test coverage**: Routes to `test-coverage` prompt -- consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs +- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide -- consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions +- **CLI commands and triggering workflows**: Routes to `cli-commands` guide -- consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command +- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide -- consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments +- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide -- consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows > [!IMPORTANT] > For architecture/pattern-selection requests, load `https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/patterns.md` first. @@ -97,7 +97,7 @@ When you interact with this agent, it will: - "Apply breaking changes from the new release" ### Create a Report-Generating Workflow -**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment +**Load when**: The workflow being created or updated produces reports -- recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment **Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/report.md @@ -127,7 +127,7 @@ When you interact with this agent, it will: - "Update @playwright/test to fix the Dependabot PR" ### Analyze Test Coverage -**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. +**Load when**: The workflow reads, analyzes, or reports test coverage -- whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy. **Prompt file**: https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/test-coverage.md @@ -154,7 +154,7 @@ When you interact with this agent, it will: **Use cases**: - "How do I trigger workflow X on the main branch?" - "What's the MCP equivalent of `gh aw logs`?" -- "I'm in Copilot Cloud — how do I compile a workflow?" +- "I'm in Copilot Cloud -- how do I compile a workflow?" - "Show me all available gh aw commands" ### Token Consumption Optimization @@ -164,7 +164,7 @@ When you interact with this agent, it will: **Use cases**: - "How do I reduce the token cost of this workflow?" -- "My workflow is too expensive — how do I optimize it?" +- "My workflow is too expensive -- how do I optimize it?" - "How do I compare token usage between two runs?" - "Should I use gh-proxy or the MCP server?" - "How do I use sub-agents to reduce costs?" @@ -232,5 +232,5 @@ gh aw compile --validate - Follow security best practices: minimal permissions, explicit network access, no template injection - **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns. - **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself. -- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. +- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand -- not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch. - **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see https://github.com/github/gh-aw/blob/v0.74.4/.github/aw/cli-commands.md diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 44b9929d..e7258100 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -35,6 +35,11 @@ "version": "v0.74.4", "sha": "d3abfe96a194bce3a523ed2093ddedd5704cdf62" }, + "github/gh-aw-actions/setup-cli@v0.74.4": { + "repo": "github/gh-aw-actions/setup-cli", + "version": "v0.74.4", + "sha": "d3abfe96a194bce3a523ed2093ddedd5704cdf62" + }, "github/gh-aw/actions/setup@v0.50.6": { "repo": "github/gh-aw/actions/setup", "version": "v0.50.6", diff --git a/.github/workflows/crane.lock.yml b/.github/workflows/crane.lock.yml index 8a7dc597..955092b5 100644 --- a/.github/workflows/crane.lock.yml +++ b/.github/workflows/crane.lock.yml @@ -334,7 +334,7 @@ jobs: - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - **checkouts**: The following repositories have been checked out and are available in the workspace: - - `$GITHUB_WORKSPACE` → `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] + - `$GITHUB_WORKSPACE` -> `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it 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). @@ -381,7 +381,7 @@ jobs: GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} - GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' + GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` -- run `safeoutputs --help` to see available tools' GH_AW_MEMORY_BRANCH_NAME: 'memory/crane' GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Allowed Files**: Only files matching patterns: *.md\n- **Max File Size**: 40960 bytes (0.04 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 1024 KB)\n" GH_AW_MEMORY_DESCRIPTION: '' @@ -1082,7 +1082,7 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner -- check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE @@ -1603,7 +1603,7 @@ jobs: fi # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner -- check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE diff --git a/.github/workflows/crane.md b/.github/workflows/crane.md index 99e0850a..1d9e9359 100644 --- a/.github/workflows/crane.md +++ b/.github/workflows/crane.md @@ -124,7 +124,7 @@ engine: copilot # Crane -A planned, verified code-migration agent. It inventories the source, picks a strategy, breaks the migration into milestones, executes one milestone per iteration, and verifies that the system still works — all autonomously, on a schedule. +A planned, verified code-migration agent. It inventories the source, picks a strategy, breaks the migration into milestones, executes one milestone per iteration, and verifies that the system still works -- all autonomously, on a schedule. ## Command Mode @@ -137,7 +137,7 @@ If these are non-empty (not ""), then you have been triggered via `/crane issue number for all issue-based migrations found. ### Reading Migrations @@ -190,15 +190,15 @@ The pre-step has already determined which migration to run. Read `/tmp/gh-aw/cra - **`selected_issue`**: The GitHub issue number if the selected migration came from an issue, or `null` if it came from a file. - **`selected_target_metric`**: The `target-metric` value from the migration's frontmatter (a number, typically `1.0`), or `null` if the migration is open-ended. Used to check the [halting condition](#halting-condition) after each accepted iteration. - **`selected_metric_direction`**: One of `"higher"` (default) or `"lower"`, parsed from the migration's `metric_direction` frontmatter field. Determines whether **larger** or **smaller** health-score values count as improvement. -- **`selected_strategy`**: The `strategy` value from the migration's frontmatter — one of `"in-place"`, `"greenfield"`, or `"auto"`. If `"auto"`, the agent must pick on the first iteration and write the chosen strategy back into the state file's Machine State table. +- **`selected_strategy`**: The `strategy` value from the migration's frontmatter -- one of `"in-place"`, `"greenfield"`, or `"auto"`. If `"auto"`, the agent must pick on the first iteration and write the chosen strategy back into the state file's Machine State table. - **`state_file_size_bytes`** / **`state_file_max_bytes`**: For rolling-compaction decisions (see [Update Rules](#update-rules)). -- **`issue_migrations`**: A mapping of migration name → issue number for all discovered issue-based migrations. +- **`issue_migrations`**: A mapping of migration name -> issue number for all discovered issue-based migrations. - **`deferred`**: Other migrations that were due but will be handled in future runs. - **`unconfigured`**: Migrations that still have the sentinel or placeholder content. - **`skipped`**: Migrations not due yet based on their per-migration schedule, or completed/paused. - **`no_migrations`**: If `true`, no migration files exist at all. - **`not_due`**: If `true`, migrations exist but none are due for this run. -- **`head_branch`**: The canonical long-running branch name for the selected migration — always exactly `crane/{migration-name}`, never with a suffix or hash. Use this value verbatim. +- **`head_branch`**: The canonical long-running branch name for the selected migration -- always exactly `crane/{migration-name}`, never with a suffix or hash. Use this value verbatim. - **`existing_pr`**: The number of the open draft PR for `crane/{migration-name}`, or `null` if no PR exists yet. If `selected` is not null: @@ -215,7 +215,7 @@ Crane supports **multiple independent migrations** in the same repository. Each Each migration runs independently with its own: - Source paths, target paths, strategy, and verification command - Health score and best-score history -- Migration issue: `[Crane: {migration-name}]` (a single GitHub issue labeled `crane-migration` — created automatically for file-based migrations, the source issue for issue-based migrations — that hosts the status comment, per-iteration comments, and human steering) +- Migration issue: `[Crane: {migration-name}]` (a single GitHub issue labeled `crane-migration` -- created automatically for file-based migrations, the source issue for issue-based migrations -- that hosts the status comment, per-iteration comments, and human steering) - Long-running branch: `crane/{migration-name}` (persists across iterations) - Single draft PR per migration: `[Crane: {migration-name}]` - State file: `{migration-name}.md` in repo-memory (Machine State + Plan + history) @@ -237,13 +237,13 @@ schedule: every 1h ### Target Metric (Halting Condition) -Migrations should usually specify `target-metric: 1.0` in the frontmatter — the typical "completed when fully migrated and verified" setting. When the health score reaches the target, the migration completes: the `crane-migration` label is removed, `crane-completed` is added (for issue-based migrations), and the state file is marked `Completed: true`. +Migrations should usually specify `target-metric: 1.0` in the frontmatter -- the typical "completed when fully migrated and verified" setting. When the health score reaches the target, the migration completes: the `crane-migration` label is removed, `crane-completed` is added (for issue-based migrations), and the state file is marked `Completed: true`. -Migrations without a `target-metric` are **open-ended** and run indefinitely (rare for migrations — usually a sign you actually want goal-oriented). +Migrations without a `target-metric` are **open-ended** and run indefinitely (rare for migrations -- usually a sign you actually want goal-oriented). ### Metric Direction -By default, **higher is better** — `best_metric` is ratcheted up each accepted iteration. The recommended convention for `migration_score` is "higher is better" (0.0 = nothing migrated or correctness broken, 1.0 = fully migrated and verified), so the default suits Crane out of the box. +By default, **higher is better** -- `best_metric` is ratcheted up each accepted iteration. The recommended convention for `migration_score` is "higher is better" (0.0 = nothing migrated or correctness broken, 1.0 = fully migrated and verified), so the default suits Crane out of the box. Migrations whose verification naturally produces a "lower is better" metric (e.g. byte-diff against a parity corpus) can opt into reversed semantics with `metric_direction: lower`. Allowed values are `higher` (default) and `lower`. @@ -266,7 +266,7 @@ A template migration file is installed at `.crane/migrations/example.md`. **Migr At the start of every run, check each migration file for this sentinel. For any migration where it is present: -1. **Skip that migration — do not run any iterations for it.** +1. **Skip that migration -- do not run any iterations for it.** 2. If no setup issue exists for that migration, create one titled `[Crane: {migration-name}] Action required: configure your migration`. ## Strategy: in-place vs greenfield @@ -278,11 +278,11 @@ Crane operates in one of two strategy modes. The strategy lives in the migration The system stays live and shippable throughout. Each milestone: 1. Ports one unit (module, function family, route, layer) into the target language -2. Routes its callers through the new implementation — via direct imports, FFI, WASM, native add-on, or an HTTP/IPC bridge as appropriate +2. Routes its callers through the new implementation -- via direct imports, FFI, WASM, native add-on, or an HTTP/IPC bridge as appropriate 3. Deletes the old source-language implementation in the same commit 4. Leaves the build green, tests passing, and behavior unchanged for outside observers -A milestone is **only** done when callers go through the new implementation and the old one is gone. Leaving both implementations in place "for safety" is forbidden — it accumulates dead code and defeats the strangler pattern. +A milestone is **only** done when callers go through the new implementation and the old one is gone. Leaving both implementations in place "for safety" is forbidden -- it accumulates dead code and defeats the strangler pattern. ### `greenfield` @@ -292,20 +292,20 @@ The target is built up in parallel in separate paths. Each milestone: 2. Adds parity tests that exercise the ported unit against the source-language equivalent on a corpus of inputs 3. Records the parity score in iteration history -Cutover (switching real traffic from source to target) is a separate event that happens once parity is total. The source is **not** modified during the migration — it stays as the reference implementation until cutover. +Cutover (switching real traffic from source to target) is a separate event that happens once parity is total. The source is **not** modified during the migration -- it stays as the reference implementation until cutover. ### `auto` If the migration's frontmatter sets `strategy: auto`, the agent picks on the first iteration. Decision rules: - Default to `in-place` for anything with external consumers, anything in production, anything large (>10 modules in scope), or anything where the test suite is the only safety net. -- Choose `greenfield` only when the source is self-contained, has no external consumers, is small (≤10 modules in scope), or is so tangled that interleaving the new language inside it would create more risk than a parallel rebuild. +- Choose `greenfield` only when the source is self-contained, has no external consumers, is small (<=10 modules in scope), or is so tangled that interleaving the new language inside it would create more risk than a parallel rebuild. -Write the chosen strategy and a one-paragraph rationale into the state file's **🧭 Strategy & Rationale** section, and update the Machine State table's `Strategy` field to the concrete choice (no longer `auto`). +Write the chosen strategy and a one-paragraph rationale into the state file's **[compass] Strategy & Rationale** section, and update the Machine State table's `Strategy` field to the concrete choice (no longer `auto`). ## Branching Model -Each migration uses a **single long-running branch** named `crane/{migration-name}`. This branch persists across iterations — every accepted change is committed to it, building up the migration as a sequence of small, verified commits. +Each migration uses a **single long-running branch** named `crane/{migration-name}`. This branch persists across iterations -- every accepted change is committed to it, building up the migration as a sequence of small, verified commits. ### Branch Naming Convention @@ -318,21 +318,21 @@ Examples: - `crane/flask_to_fastapi` - `crane/cjs_to_esm` -> ⚠️ **CRITICAL — Branch Name Must Be Exact** +> [!] **CRITICAL -- Branch Name Must Be Exact** > -> The branch name is ALWAYS exactly `crane/{migration-name}` — **no suffixes, no hashes, no run IDs, no iteration numbers, no random tokens**. Never create branches like: -> - ❌ `crane/stats_py_to_ts-abc123` -> - ❌ `crane/stats_py_to_ts-iter42-deadbeef` -> - ❌ `crane/stats_py_to_ts-1234567890` +> The branch name is ALWAYS exactly `crane/{migration-name}` -- **no suffixes, no hashes, no run IDs, no iteration numbers, no random tokens**. Never create branches like: +> - [x] `crane/stats_py_to_ts-abc123` +> - [x] `crane/stats_py_to_ts-iter42-deadbeef` +> - [x] `crane/stats_py_to_ts-1234567890` > -> **Never let the gh-aw framework auto-generate a branch name.** The pre-step provides the canonical name in the `head_branch` field of `/tmp/gh-aw/crane.json` — always use that value verbatim. +> **Never let the gh-aw framework auto-generate a branch name.** The pre-step provides the canonical name in the `head_branch` field of `/tmp/gh-aw/crane.json` -- always use that value verbatim. ### How It Works 1. On the **first accepted iteration**, the branch is created from the default branch. -2. On **subsequent iterations**, the agent checks out the existing branch and ensures it is up to date with the default branch (fast-forward when possible, merge when truly diverged — see Step 3). +2. On **subsequent iterations**, the agent checks out the existing branch and ensures it is up to date with the default branch (fast-forward when possible, merge when truly diverged -- see Step 3). 3. **Accepted iterations** are committed and pushed. Each commit message references the GitHub Actions run URL. -4. **Rejected or errored iterations** do not commit — changes are discarded. +4. **Rejected or errored iterations** do not commit -- changes are discarded. 5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR. 6. The branch may be **merged into the default branch** at any time. After merging, the branch continues to accumulate future iterations. @@ -340,8 +340,8 @@ Examples: Each migration has three coordinated resources: - **Branch + PR**: `crane/{migration-name}` with a single draft PR -- **Migration Issue**: `[Crane: {migration-name}]` — a single GitHub issue (labeled `crane-migration`) that hosts the status comment, per-iteration comments, and human steering -- **State File**: `{migration-name}.md` in repo-memory — all state, plan, history, and lessons +- **Migration Issue**: `[Crane: {migration-name}]` -- a single GitHub issue (labeled `crane-migration`) that hosts the status comment, per-iteration comments, and human steering +- **State File**: `{migration-name}.md` in repo-memory -- all state, plan, history, and lessons All three reference each other. The migration issue is created (or, for issue-based migrations, adopted) on the first run and updated with links to the PR and state file. @@ -351,45 +351,45 @@ Each run executes **one iteration for the single selected migration**. ### Step 0: First-Iteration Bootstrap (Planning) -**Only on the first iteration** — detected by `iteration_count == 0` in the state file (or the state file not existing yet): +**Only on the first iteration** -- detected by `iteration_count == 0` in the state file (or the state file not existing yet): -1. **Inventory the source**: list the modules under the migration's source paths, their inter-dependencies, their external consumers, and their test coverage. Record this in the state file's **🗺️ Inventory** section. -2. **Pick a strategy** if `selected_strategy == "auto"`: apply the decision rules in [Strategy: in-place vs greenfield](#strategy-in-place-vs-greenfield), write the choice and rationale into the state file's **🧭 Strategy & Rationale** section, and update the `Strategy` field in the Machine State table to the concrete choice. -3. **Generate the initial plan**: break the migration into ordered milestones in the **🪜 Milestones** section. Each milestone has: +1. **Inventory the source**: list the modules under the migration's source paths, their inter-dependencies, their external consumers, and their test coverage. Record this in the state file's **[map] Inventory** section. +2. **Pick a strategy** if `selected_strategy == "auto"`: apply the decision rules in [Strategy: in-place vs greenfield](#strategy-in-place-vs-greenfield), write the choice and rationale into the state file's **[compass] Strategy & Rationale** section, and update the `Strategy` field in the Machine State table to the concrete choice. +3. **Generate the initial plan**: break the migration into ordered milestones in the **[ladder] Milestones** section. Each milestone has: - A short name (e.g. "Port `quantile` family") - A scope (which functions/files/routes) - - An acceptance criterion (what verification it must pass — typically the parity test for that unit plus no regression in `migration_score`) + - An acceptance criterion (what verification it must pass -- typically the parity test for that unit plus no regression in `migration_score`) - A status (initially `todo`) -4. **Set the current focus**: pick the first milestone and put it in **🎯 Current Focus**. -5. **Do not yet implement any porting on iteration 0** — planning *is* the work for this iteration. Commit the plan and exit through Step 5c with `migration_score = 0.0` recorded but the iteration accepted as a planning step (skip the metric-improvement check on iteration 0). +4. **Set the current focus**: pick the first milestone and put it in **[target] Current Focus**. +5. **Do not yet implement any porting on iteration 0** -- planning *is* the work for this iteration. Commit the plan and exit through Step 5c with `migration_score = 0.0` recorded but the iteration accepted as a planning step (skip the metric-improvement check on iteration 0). -This Step 0 produces the plan and ships it as commit #1 on the migration branch (the plan file lives in `.crane/migrations//plan.md` — written to the migration branch — *and* in the state file on the memory branch, so it's visible both in the PR and on the memory branch). +This Step 0 produces the plan and ships it as commit #1 on the migration branch (the plan file lives in `.crane/migrations//plan.md` -- written to the migration branch -- *and* in the state file on the memory branch, so it's visible both in the PR and on the memory branch). ### Step 1: Read State 1. Read the migration file to understand source, target, strategy, and verification. 2. Read the state file `{migration-name}.md` from the repo-memory folder. This contains: - - **⚙️ Machine State** table: scheduling and control fields the pre-step parses. - - **📋 Migration Info**: high-level summary (source, target, strategy, branch, PR, issue). - - **🗺️ Inventory**: modules, dependencies, consumers, test coverage, risk. - - **🧭 Strategy & Rationale**: chosen strategy and why. - - **🪜 Milestones**: ordered list of units to port, each with status. - - **🎯 Current Focus**: the milestone the next iteration will work on. - - **📚 Lessons Learned**. - - **🚧 Blockers & Foreclosed Approaches**. - - **🔭 Future Work**. - - **📊 Iteration History**. - - If the state file does not exist yet, this is the first iteration — go to Step 0. + - **[*] Machine State** table: scheduling and control fields the pre-step parses. + - **[list] Migration Info**: high-level summary (source, target, strategy, branch, PR, issue). + - **[map] Inventory**: modules, dependencies, consumers, test coverage, risk. + - **[compass] Strategy & Rationale**: chosen strategy and why. + - **[ladder] Milestones**: ordered list of units to port, each with status. + - **[target] Current Focus**: the milestone the next iteration will work on. + - **[docs] Lessons Learned**. + - **[wip] Blockers & Foreclosed Approaches**. + - **[scope] Future Work**. + - **[chart] Iteration History**. + + If the state file does not exist yet, this is the first iteration -- go to Step 0. ### Step 2: Analyze and Propose 1. Read the source and target paths and the current Milestones. -2. Review **Lessons Learned**, **Blockers**, and **Current Focus** — what worked, what didn't, what the maintainer wants next. +2. Review **Lessons Learned**, **Blockers**, and **Current Focus** -- what worked, what didn't, what the maintainer wants next. 3. **Pick the next concrete change**: - - Normally: implement whatever the **Current Focus** milestone calls for. Keep it small — one milestone, one iteration. Splitting a milestone into sub-iterations is fine and often necessary. - - If the **Current Focus** turns out to be too large for one iteration, split it: add sub-milestones to the **🪜 Milestones** section before implementing. - - If the **Current Focus** is blocked by something concrete (missing dependency, ambiguous behavior in the source, unclear API on the target side), move it to **🚧 Blockers** with a clear reason and pick a different milestone to focus on. + - Normally: implement whatever the **Current Focus** milestone calls for. Keep it small -- one milestone, one iteration. Splitting a milestone into sub-iterations is fine and often necessary. + - If the **Current Focus** turns out to be too large for one iteration, split it: add sub-milestones to the **[ladder] Milestones** section before implementing. + - If the **Current Focus** is blocked by something concrete (missing dependency, ambiguous behavior in the source, unclear API on the target side), move it to **[wip] Blockers** with a clear reason and pick a different milestone to focus on. 4. Describe the proposed change in your reasoning before implementing it. ### Step 3: Implement @@ -420,10 +420,10 @@ This Step 0 produces the plan and ships it as commit #1 on the migration branch Use `--force-with-lease` (not `--force`) so concurrent pushes are rejected rather than overwritten. -2. Make the proposed changes — restricted to: +2. Make the proposed changes -- restricted to: - Files inside the source paths declared in the migration's **Source** section - Files inside the target paths declared in the migration's **Target** section - - The migration's own `code/` directory (evaluator, parity corpus) — **only** if you're updating fixtures or adding new parity cases. **Never** modify the evaluator script after the migration's first iteration. + - The migration's own `code/` directory (evaluator, parity corpus) -- **only** if you're updating fixtures or adding new parity cases. **Never** modify the evaluator script after the migration's first iteration. - The migration's `plan.md` if the migration is directory-based and you have a `plan.md` (mirrored from the state file's Plan sections) 3. **Respect the migration constraints**: do not modify files outside the declared source/target paths. @@ -440,7 +440,7 @@ Verification is necessary but **not sufficient** for acceptance. The agent's san The accept path is split into three sub-steps: **5a (push and wait for CI)**, **5b (fix loop)**, **5c (accept)**. -**If the score did not improve** (or held flat below `best_metric`), jump straight to the "score did not improve" path below — no push, no CI gate. +**If the score did not improve** (or held flat below `best_metric`), jump straight to the "score did not improve" path below -- no push, no CI gate. #### Step 5a: Push and wait for CI @@ -457,10 +457,10 @@ The first run (no `best_metric` yet) always counts as an improvement. - Body (after a blank line): `Run: {run_url}` 2. Push the commit to the long-running branch. 3. **Find or create the PR** so CI runs and `gh pr checks` has a target. Follow these steps in order: - a. Check `existing_pr` from `/tmp/gh-aw/crane.json`. If it is not null, that is the existing draft PR — use it as `$EXISTING_PR` below; **never** call `create-pull-request`. - b. If `existing_pr` is null, also check the `PR` field in the state file's **⚙️ Machine State** table as a fallback. Verify it is still open via the GitHub API; if it has been closed or merged, treat it as if no PR exists and proceed to step (c). + a. Check `existing_pr` from `/tmp/gh-aw/crane.json`. If it is not null, that is the existing draft PR -- use it as `$EXISTING_PR` below; **never** call `create-pull-request`. + b. If `existing_pr` is null, also check the `PR` field in the state file's **[*] Machine State** table as a fallback. Verify it is still open via the GitHub API; if it has been closed or merged, treat it as if no PR exists and proceed to step (c). c. If no PR exists (both sources are null): create one with `create-pull-request`, specifying `branch: crane/{migration-name}` (the value of `head_branch` from `crane.json`) explicitly. -4. Wait for CI on the new HEAD and reduce all check-runs to a single status — `success`, `failure`, or `pending`: +4. Wait for CI on the new HEAD and reduce all check-runs to a single status -- `success`, `failure`, or `pending`: ```bash PR=${EXISTING_PR:-$(gh pr list --head crane/{migration-name} --json number -q '.[0].number')} @@ -479,12 +479,12 @@ The first run (no `best_metric` yet) always counts as an improvement. #### Step 5b: Fix loop (up to 5 attempts per iteration) -If `status == "failure"`, **fix and retry — do not revert, do not accept**: +If `status == "failure"`, **fix and retry -- do not revert, do not accept**: 1. **Fetch the failing check-run logs** for the pushed SHA. 2. **Extract a structured failure summary**: - Failing job names and the first error line for each. - - **A failure signature** — a stable, normalized fingerprint (e.g., sorted failing-test names + the top error code). + - **A failure signature** -- a stable, normalized fingerprint (e.g., sorted failing-test names + the top error code). 3. **No-progress guard**: if this attempt's failure signature matches the previous attempt's signature, **stop**. Set `paused: true` with `pause_reason: "stuck in CI fix loop: "`, append `"ci-fix-exhausted"` to `recent_statuses`, comment on the migration issue, and end the iteration. 4. **Attempt the fix**: feed the structured failure summary back as the next sub-task. The agent commits the fix and pushes. 5. **Loop back to Step 5a** with the new HEAD. @@ -496,48 +496,48 @@ If `status == "failure"`, **fix and retry — do not revert, do not accept**: **Only entered when `status == "success"`** from Step 5a (possibly after fix attempts in Step 5b). 1. The commit(s) are already on the long-running branch. No further pushing needed. -2. If a draft PR does not already exist for this branch, create one — specify `branch: crane/{migration-name}` explicitly: +2. If a draft PR does not already exist for this branch, create one -- specify `branch: crane/{migration-name}` explicitly: - Title: `[Crane: {migration-name}]` - - Body: summary of the migration (source → target, strategy), link to the migration issue, current best score and progress, AI disclosure: `🤖 *This PR is maintained by Crane. Each accepted iteration adds a commit to this branch.*` + - Body: summary of the migration (source -> target, strategy), link to the migration issue, current best score and progress, AI disclosure: `[bot] *This PR is maintained by Crane. Each accepted iteration adds a commit to this branch.*` If a draft PR already exists, use `push-to-pull-request-branch` (never `create-pull-request`). Update the PR body with the latest score and a summary of the most recent accepted iteration. Add a comment to the PR summarizing the iteration: what milestone was advanced, old score, new score, fix-attempt count if `> 0`, and a link to the actions run. -3. Ensure the migration issue exists (see [Migration Issue](#migration-issue) below) — for file-based migrations with no migration issue yet (`selected_issue` is null in `/tmp/gh-aw/crane.json`), create one and record its number in the state file's `Issue` field. +3. Ensure the migration issue exists (see [Migration Issue](#migration-issue) below) -- for file-based migrations with no migration issue yet (`selected_issue` is null in `/tmp/gh-aw/crane.json`), create one and record its number in the state file's `Issue` field. 4. Update the state file `{migration-name}.md` in the repo-memory folder: - - **⚙️ Machine State** table: reset `consecutive_errors` to 0, set `best_metric` (the new `migration_score`), increment `iteration_count`, set `last_run` to current UTC, append `"accepted"` to `recent_statuses` (keep last 10), set `paused` to false. - - **🪜 Milestones**: update the relevant milestone's status — typically `done` if the milestone was fully completed, otherwise leave `in-progress` and update its notes. If the milestone is done, the next milestone in the list becomes the new **🎯 Current Focus**. - - Prepend an entry to **📊 Iteration History** with status ✅, score, **signed delta**, PR link, fix-attempt count if `> 0`, and a one-line summary of what milestone was advanced and how. - - Update **📚 Lessons Learned** if this iteration revealed something new (e.g. a bridging trick, a parity surprise, a perf trap). - - Update **🔭 Future Work** if this iteration opened new threads. + - **[*] Machine State** table: reset `consecutive_errors` to 0, set `best_metric` (the new `migration_score`), increment `iteration_count`, set `last_run` to current UTC, append `"accepted"` to `recent_statuses` (keep last 10), set `paused` to false. + - **[ladder] Milestones**: update the relevant milestone's status -- typically `done` if the milestone was fully completed, otherwise leave `in-progress` and update its notes. If the milestone is done, the next milestone in the list becomes the new **[target] Current Focus**. + - Prepend an entry to **[chart] Iteration History** with status [+], score, **signed delta**, PR link, fix-attempt count if `> 0`, and a one-line summary of what milestone was advanced and how. + - Update **[docs] Lessons Learned** if this iteration revealed something new (e.g. a bridging trick, a parity surprise, a perf trap). + - Update **[scope] Future Work** if this iteration opened new threads. 5. **Update the migration issue**: edit the status comment and post a per-iteration comment. 6. **Check halting condition** (see [Halting Condition](#halting-condition)): if `target-metric` is set, compare the new `best_metric` against it. For `higher` direction: completed when `best_metric >= target-metric`. When the target is met, mark the migration as completed. **If the score did not improve**: 1. Discard the code changes (do not commit them to the long-running branch). 2. Update the state file: - - **⚙️ Machine State**: increment `iteration_count`, set `last_run`, append `"rejected"` to `recent_statuses`. - - Prepend an entry to **📊 Iteration History** with status ❌, score, and a one-line summary of what was tried. - - If this approach is conclusively a dead end, add it to **🚧 Blockers & Foreclosed Approaches** with a clear explanation. Common foreclosed-approach patterns in migration: "tried to port X without first porting its dependency Y", "tried to bridge via Z but the boundary copies too much", "tried to inline the target into the source-side runtime but the type systems are incompatible". - - If the rejection points at a missing precondition (e.g. "this milestone needs Y to be ported first"), reorder the **🪜 Milestones** list — promote the precondition ahead of the current focus. + - **[*] Machine State**: increment `iteration_count`, set `last_run`, append `"rejected"` to `recent_statuses`. + - Prepend an entry to **[chart] Iteration History** with status [x], score, and a one-line summary of what was tried. + - If this approach is conclusively a dead end, add it to **[wip] Blockers & Foreclosed Approaches** with a clear explanation. Common foreclosed-approach patterns in migration: "tried to port X without first porting its dependency Y", "tried to bridge via Z but the boundary copies too much", "tried to inline the target into the source-side runtime but the type systems are incompatible". + - If the rejection points at a missing precondition (e.g. "this milestone needs Y to be ported first"), reorder the **[ladder] Milestones** list -- promote the precondition ahead of the current focus. 3. **Update the migration issue**. **If verification could not run** (build failure, missing dependencies, evaluator threw): 1. Discard the code changes. 2. Update the state file: - - **⚙️ Machine State**: increment `consecutive_errors`, increment `iteration_count`, set `last_run`, append `"error"` to `recent_statuses`. + - **[*] Machine State**: increment `consecutive_errors`, increment `iteration_count`, set `last_run`, append `"error"` to `recent_statuses`. - If `consecutive_errors` reaches 3+, set `paused: true` and `pause_reason: "consecutive errors"`, and create an issue describing the problem. - - Prepend an entry to **📊 Iteration History** with status ⚠️ and a brief error description. + - Prepend an entry to **[chart] Iteration History** with status [!] and a brief error description. 3. **Update the migration issue**. #### Coordination with PR-health-keeper workflows -If a repo ships a companion PR-health-keeper workflow, it can pick up paused Crane PRs using the `pause_reason` field — `ci-fix-exhausted: `, `stuck in CI fix loop: `, and `ci-timeout` are all signals the branch is red and needs an external nudge. Absent such a workflow, the loud pause + structured reason gives a human enough signal to intervene. +If a repo ships a companion PR-health-keeper workflow, it can pick up paused Crane PRs using the `pause_reason` field -- `ci-fix-exhausted: `, `stuck in CI fix loop: `, and `ci-timeout` are all signals the branch is red and needs an external nudge. Absent such a workflow, the loud pause + structured reason gives a human enough signal to intervene. ## Migration Issue -Each migration has **exactly one** open GitHub issue (labeled `crane-migration`) titled `[Crane: {migration-name}]`. This single issue is the source of truth for the migration — it hosts: +Each migration has **exactly one** open GitHub issue (labeled `crane-migration`) titled `[Crane: {migration-name}]`. This single issue is the source of truth for the migration -- it hosts: -- The **status comment** (the earliest bot comment, edited in place each iteration) — a dashboard of current state. -- A **per-iteration comment** for every iteration (accepted, rejected, or error) — the rolling log. -- **Human steering comments** — plain-prose comments from maintainers, treated by the agent as directives. +- The **status comment** (the earliest bot comment, edited in place each iteration) -- a dashboard of current state. +- A **per-iteration comment** for every iteration (accepted, rejected, or error) -- the rolling log. +- **Human steering comments** -- plain-prose comments from maintainers, treated by the agent as directives. ### Auto-Creation for File-Based Migrations @@ -549,7 +549,7 @@ If `selected_issue` is `null` in `/tmp/gh-aw/crane.json`, the migration is file- Record the new issue number in the state file's `Issue` field. On subsequent runs, the pre-step discovers the existing migration issue automatically. -For issue-based migrations, no creation is needed — the source issue is already the migration issue. +For issue-based migrations, no creation is needed -- the source issue is already the migration issue. ### Status Comment @@ -559,17 +559,17 @@ On the **first iteration**, post a comment on the migration issue. On **every su ```markdown -🤖 **Crane Status** +[bot] **Crane Status** | | | |---|---| -| **Status** | 🟢 Active / ⏸️ Paused / ⚠️ Error / ✅ Completed | -| **Migration** | {source-language} → {target-languages} | +| **Status** | [+] Active / [||] Paused / [!] Error / [+] Completed | +| **Migration** | {source-language} -> {target-languages} | | **Strategy** | {in-place / greenfield} | | **Best Score** | {best_metric} | -| **Progress** | {progress fraction or "—"} | +| **Progress** | {progress fraction or "--"} | | **Milestones** | {done}/{total} done, {in_progress} in-progress, {blocked} blocked | -| **Target Metric** | {target_metric or "— (open-ended)"} | +| **Target Metric** | {target_metric or "-- (open-ended)"} | | **Iterations** | {iteration_count} | | **Last Run** | [{YYYY-MM-DD HH:MM UTC}]({run_url}) | | **Branch** | [`crane/{migration-name}`](https://github.com/{owner}/{repo}/tree/crane/{migration-name}) | @@ -591,7 +591,7 @@ On the **first iteration**, post a comment on the migration issue. On **every su After **every iteration** (accepted, rejected, or error), post a **new comment**: ```markdown -🤖 **Iteration {N}** — [{status_emoji} {status}]({run_url}) +[bot] **Iteration {N}** -- [{status_emoji} {status}]({run_url}) - **Milestone**: {milestone name, or "Planning" for iteration 0} - **Change**: {one-line description of what was done} @@ -608,15 +608,15 @@ After **every iteration** (accepted, rejected, or error), post a **new comment** ### Migration Issue Rules -- For issue-based migrations, the source issue body IS the migration definition — do not modify it (the user owns it). +- For issue-based migrations, the source issue body IS the migration definition -- do not modify it (the user owns it). - For file-based migrations, the migration issue body is informational and may be lightly updated, but the migration file (`migration.md`) remains the source of truth. - The `crane-migration` label must remain on the issue for the migration to be discovered. When a migration completes, the label is removed and replaced with `crane-completed`. -- Closing the migration issue stops the migration from being discovered. Do NOT close the migration issue when the PR is merged — the branch continues to accumulate future iterations until the target metric is reached. +- Closing the migration issue stops the migration from being discovered. Do NOT close the migration issue when the PR is merged -- the branch continues to accumulate future iterations until the target metric is reached. - Migration issues are labeled `[crane-migration, automation, crane]`. ## Halting Condition -Migrations are usually **goal-oriented** — you want to finish. Set `target-metric: 1.0` in the frontmatter and Crane stops the migration when the health score reaches 1.0 (which, with the recommended `correctness × progress` convention, means "fully migrated and verified"). +Migrations are usually **goal-oriented** -- you want to finish. Set `target-metric: 1.0` in the frontmatter and Crane stops the migration when the health score reaches 1.0 (which, with the recommended `correctness x progress` convention, means "fully migrated and verified"). ### How It Works @@ -628,8 +628,8 @@ Migrations are usually **goal-oriented** — you want to finish. Set `target-met - Set `Completed: true` in the Machine State table. - Set `Completed Reason` to a human-readable message (e.g., `target metric 1.0 reached with value 1.0`). - **For issue-based migrations**: remove the `crane-migration` label, add the `crane-completed` label. - - Update the status comment to ✅ Completed. - - Post a celebratory per-iteration comment: `🎉 **Migration complete!** {source} → {target} finished after {N} iterations.` + - Update the status comment to [+] Completed. + - Post a celebratory per-iteration comment: `[+] **Migration complete!** {source} -> {target} finished after {N} iterations.` - The migration will not be selected for future runs. ### Open-Ended Migrations @@ -657,35 +657,35 @@ When creating or updating a migration's state file, use this structure: ```markdown # Crane: {migration-name} -🤖 *This file is maintained by the Crane agent. Maintainers may freely edit any section.* +[bot] *This file is maintained by the Crane agent. Maintainers may freely edit any section.* --- -## ⚙️ Machine State +## [*] Machine State -> 🤖 *Updated automatically after each iteration. The pre-step scheduler reads this table — keep it accurate.* +> [bot] *Updated automatically after each iteration. The pre-step scheduler reads this table -- keep it accurate.* | Field | Value | |-------|-------| -| Last Run | — | +| Last Run | -- | | Iteration Count | 0 | -| Best Metric | — | -| Target Metric | — | +| Best Metric | -- | +| Target Metric | -- | | Metric Direction | higher | | Strategy | auto | | Branch | `crane/{migration-name}` | -| PR | — | -| Issue | — | +| PR | -- | +| Issue | -- | | Paused | false | -| Pause Reason | — | +| Pause Reason | -- | | Completed | false | -| Completed Reason | — | +| Completed Reason | -- | | Consecutive Errors | 0 | -| Recent Statuses | — | +| Recent Statuses | -- | --- -## 📋 Migration Info +## [list] Migration Info **Source**: {source-language} ({version}) **Target**: {target-languages joined} @@ -696,7 +696,7 @@ When creating or updating a migration's state file, use this structure: --- -## 🗺️ Inventory +## [map] Inventory > Modules in scope, their dependencies and consumers, and notes on test coverage and risk. Generated on iteration 0, refined as the migration progresses. @@ -704,7 +704,7 @@ When creating or updating a migration's state file, use this structure: --- -## 🧭 Strategy & Rationale +## [compass] Strategy & Rationale > Why `in-place` or `greenfield` was chosen. Refer to this whenever a milestone is unclear about whether to bridge or to fork. @@ -712,7 +712,7 @@ When creating or updating a migration's state file, use this structure: --- -## 🪜 Milestones +## [ladder] Milestones > Ordered list of units to migrate. Each milestone has a name, scope, acceptance criterion, and status (`todo` / `in-progress` / `done` / `blocked`). Reorder freely as priorities shift. @@ -722,15 +722,15 @@ When creating or updating a migration's state file, use this structure: --- -## 🎯 Current Focus +## [target] Current Focus The milestone the next iteration will work on, plus any human steering for it. -*(populated on first iteration — defaults to the first `todo` milestone)* +*(populated on first iteration -- defaults to the first `todo` milestone)* --- -## 📚 Lessons Learned +## [docs] Lessons Learned Key findings accumulated over iterations. @@ -738,7 +738,7 @@ Key findings accumulated over iterations. --- -## 🚧 Blockers & Foreclosed Approaches +## [wip] Blockers & Foreclosed Approaches Approaches that have been tried and definitively ruled out, plus active blockers that have to be resolved before the relevant milestone can advance. @@ -746,7 +746,7 @@ Approaches that have been tried and definitively ruled out, plus active blockers --- -## 🔭 Future Work +## [scope] Future Work Promising ideas surfaced but not yet promoted to milestones. Both the agent and maintainers contribute here. @@ -754,7 +754,7 @@ Promising ideas surfaced but not yet promoted to milestones. Both the agent and --- -## 📊 Iteration History +## [chart] Iteration History All iterations in reverse chronological order (newest first). @@ -768,27 +768,27 @@ All iterations in reverse chronological order (newest first). | Last Run | ISO timestamp | UTC timestamp of the last iteration | | Iteration Count | integer | Total iterations completed | | Best Metric | number | Best `migration_score` achieved so far | -| Target Metric | number or `—` | Target score from frontmatter (halting condition). Typically `1.0` | +| Target Metric | number or `--` | Target score from frontmatter (halting condition). Typically `1.0` | | Metric Direction | `higher` or `lower` | Whether larger or smaller values count as improvement. Defaults to `higher` | | Strategy | `in-place` / `greenfield` / `auto` | The chosen strategy. After iteration 0 should never be `auto` | | Branch | branch name | Long-running branch: `crane/{migration-name}` | -| PR | `#number` or `—` | Draft PR number | -| Issue | `#number` or `—` | Single migration issue | +| PR | `#number` or `--` | Draft PR number | +| Issue | `#number` or `--` | Single migration issue | | Paused | `true` or `false` | Whether the migration is paused | -| Pause Reason | text or `—` | `manual`, `consecutive errors`, `ci-fix-exhausted: `, `stuck in CI fix loop: `, `ci-timeout` | +| Pause Reason | text or `--` | `manual`, `consecutive errors`, `ci-fix-exhausted: `, `stuck in CI fix loop: `, `ci-timeout` | | Completed | `true` or `false` | Whether the target metric has been reached | -| Completed Reason | text or `—` | e.g., `target metric 1.0 reached with value 1.0` | +| Completed Reason | text or `--` | e.g., `target metric 1.0 reached with value 1.0` | | Consecutive Errors | integer | Count of consecutive verification failures | | Recent Statuses | comma-separated | Last 10 outcomes: `accepted`, `rejected`, `error`, or `ci-fix-exhausted` | ### Iteration History Entry Format -After each iteration, prepend an entry to **📊 Iteration History**. Use `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}` for the run URL. +After each iteration, prepend an entry to **[chart] Iteration History**. Use `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}` for the run URL. ```markdown -### Iteration {N} — {YYYY-MM-DD HH:MM UTC} — [Run](https://github.com/{owner}/{repo}/actions/runs/{run_id}) +### Iteration {N} -- {YYYY-MM-DD HH:MM UTC} -- [Run](https://github.com/{owner}/{repo}/actions/runs/{run_id}) -- **Status**: ✅ Accepted / ❌ Rejected / ⚠️ Error +- **Status**: [+] Accepted / [x] Rejected / [!] Error - **Milestone**: {milestone name, or "Planning" for iteration 0} - **Change**: {one-line description} - **Score**: {value} (previous best: {previous_best}, delta: {signed-delta}) @@ -803,17 +803,17 @@ After each iteration, prepend an entry to **📊 Iteration History**. Use `${{ g - **Always** read the state file before proposing a change. It contains the plan you're executing. - **Always** update the state file after each iteration. -- **Update the Machine State table first** — the scheduling pre-step depends on it. +- **Update the Machine State table first** -- the scheduling pre-step depends on it. - **Update Milestones** after every accepted iteration: mark `done`, promote sub-milestones, demote blocked ones. - **Prepend** iteration history entries (newest first). -- **Accumulate** Lessons Learned — add new insights, don't overwrite existing ones. -- **Add to Blockers / Foreclosed Approaches** only when an approach is conclusively ruled out (not just rejected once) — and explain *why*. -- **Respect Current Focus** — if a maintainer has set or edited it, follow it in your next proposal. +- **Accumulate** Lessons Learned -- add new insights, don't overwrite existing ones. +- **Add to Blockers / Foreclosed Approaches** only when an approach is conclusively ruled out (not just rejected once) -- and explain *why*. +- **Respect Current Focus** -- if a maintainer has set or edited it, follow it in your next proposal. - **Write the state file** to the repo-memory folder. Changes are automatically committed and pushed. -- **Keep the state file compact.** Must stay under `max-file-size` (default 40 KB — see `state_file_max_bytes` in `/tmp/gh-aw/crane.json`). When prepending a new iteration entry, collapse older iteration entries (beyond the most recent 10) into compressed summary lines: +- **Keep the state file compact.** Must stay under `max-file-size` (default 40 KB -- see `state_file_max_bytes` in `/tmp/gh-aw/crane.json`). When prepending a new iteration entry, collapse older iteration entries (beyond the most recent 10) into compressed summary lines: ```markdown - ### Iters 30–60 — ✅ (score 0.40→0.72, +12 milestones done): brief summary of what was ported across this range + ### Iters 30-60 -- [+] (score 0.40->0.72, +12 milestones done): brief summary of what was ported across this range ``` Also prune Lessons Learned to the most relevant entries, and consolidate similar Blockers entries. If `state_file_size_bytes` is already > 80% of `state_file_max_bytes`, **compact aggressively** this iteration: collapse to the most recent 5 detailed entries, merge older compressed ranges into broader bands, and trim verbose milestone notes. @@ -821,34 +821,34 @@ After each iteration, prepend an entry to **📊 Iteration History**. Use `${{ g ## Guidelines - **One milestone per iteration** when possible. Split big milestones into sub-milestones rather than landing huge commits. -- **Keep the build green every iteration.** For `in-place` migrations, the system must keep working — no half-ported modules left lying around between iterations. +- **Keep the build green every iteration.** For `in-place` migrations, the system must keep working -- no half-ported modules left lying around between iterations. - **The evaluator is sacred.** Never modify the verification script after the migration's first iteration. Updating fixtures or adding new parity cases is fine; rewriting the scoring is not. -- **Repo-memory state file is the single source of truth.** Plan, milestones, history, lessons — all live there. Keep it up to date. +- **Repo-memory state file is the single source of truth.** Plan, milestones, history, lessons -- all live there. Keep it up to date. - **Read the state file before every proposal.** Foreclosed Approaches and Lessons Learned exist to prevent repeating failures. -- **Respect human input.** Current Focus and any human comments on the migration issue are directives — follow them. +- **Respect human input.** Current Focus and any human comments on the migration issue are directives -- follow them. - **Diminishing returns.** If the last 5 consecutive iterations were rejected, post a comment suggesting the user review the milestone list or change the strategy. -- **Transparency.** Every PR and comment includes AI disclosure with 🤖. +- **Transparency.** Every PR and comment includes AI disclosure with [bot]. - **Safety.** Never modify files outside the migration's declared source/target paths. Never modify the verification script after iteration 1. Never modify the migration definition (except via `/crane` command mode). - **Read AGENTS.md first**: before starting work, read the repository's `AGENTS.md` file (if present) for project-specific conventions. ## Common Mistakes to Avoid -> ❌ **Do NOT create a new branch with a suffix for each iteration.** +> [x] **Do NOT create a new branch with a suffix for each iteration.** > Correct: `crane/stats_py_to_ts` > Wrong: `crane/stats_py_to_ts-abc123`, `crane/stats_py_to_ts-iter42` > Use the `head_branch` field from `/tmp/gh-aw/crane.json` verbatim. -> ❌ **Do NOT create a new PR if one already exists for `crane/{migration-name}`.** +> [x] **Do NOT create a new PR if one already exists for `crane/{migration-name}`.** > The pre-step provides `existing_pr` in `/tmp/gh-aw/crane.json`. If not null, **always** use `push-to-pull-request-branch`. -> ❌ **Do NOT leave both source and target implementations in an `in-place` migration "for safety".** +> [x] **Do NOT leave both source and target implementations in an `in-place` migration "for safety".** > A milestone is only `done` when callers go through the new implementation and the old one is gone. Dual-implementation accumulates dead code and defeats the strangler-fig pattern. -> ❌ **Do NOT modify the verification script after the first iteration.** +> [x] **Do NOT modify the verification script after the first iteration.** > The evaluator is the migration's scoreboard. Changing it mid-flight invalidates all prior iterations and breaks the ratchet. -> ❌ **Do NOT skip the planning iteration (Step 0).** -> Crane's first job is to plan, not to port. The Step 0 commit is the migration's foundation — every later iteration reads from it. Trying to "just start porting" produces incoherent migrations. +> [x] **Do NOT skip the planning iteration (Step 0).** +> Crane's first job is to plan, not to port. The Step 0 commit is the migration's foundation -- every later iteration reads from it. Trying to "just start porting" produces incoherent migrations. -> ❌ **Do NOT modify files outside the migration's declared source/target paths.** -> The Source and Target sections are the allowlist. Touching anything else — including the migration definition itself — is forbidden outside command mode. +> [x] **Do NOT modify files outside the migration's declared source/target paths.** +> The Source and Target sections are the allowlist. Touching anything else -- including the migration definition itself -- is forbidden outside command mode. diff --git a/.github/workflows/scripts/crane_scheduler.py b/.github/workflows/scripts/crane_scheduler.py index ff83514f..88ff8d5a 100644 --- a/.github/workflows/scripts/crane_scheduler.py +++ b/.github/workflows/scripts/crane_scheduler.py @@ -69,9 +69,9 @@ def parse_machine_state(content): - """Parse the ⚙️ Machine State table from a state file. Returns a dict.""" + """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) + m = re.search(r"## [*] Machine State.*?\n(.*?)(?=\n## |\Z)", content, re.DOTALL) if not m: return state section = m.group(0) @@ -81,7 +81,7 @@ def parse_machine_state(content): if raw_key.lower() in ("field", "---", ":---", ":---:", "---:"): continue key = raw_key.lower().replace(" ", "_") - val = None if raw_val in ("—", "-", "") else raw_val + val = None if raw_val in ("--", "-", "") else raw_val state[key] = val # Coerce types for int_field in ("iteration_count", "consecutive_errors"): @@ -257,7 +257,7 @@ def _bootstrap_template_if_missing(): if os.path.isdir(MIGRATIONS_DIR): return os.makedirs(MIGRATIONS_DIR, exist_ok=True) - bt = chr(96) # backtick — keep gh-aw compiler happy if this ever gets inlined + bt = chr(96) # backtick -- keep gh-aw compiler happy if this ever gets inlined template = "\n".join([ "", "", @@ -309,7 +309,7 @@ def _bootstrap_template_if_missing(): ]) with open(TEMPLATE_FILE, "w") as f: f.write(template) - # Leave the template unstaged — the agent will create a draft PR with it + # Leave the template unstaged -- the agent will create a draft PR with it print("BOOTSTRAPPED: created {} locally (agent will create a draft PR)".format(TEMPLATE_FILE)) diff --git a/.github/workflows/shared/reporting.md b/.github/workflows/shared/reporting.md index f1a4ddba..5a120025 100644 --- a/.github/workflows/shared/reporting.md +++ b/.github/workflows/shared/reporting.md @@ -13,13 +13,13 @@ When analyzing workflow run logs or reporting information from GitHub Actions ru **Format:** `````markdown -[§12345](https://github.com/owner/repo/actions/runs/12345) +[#12345](https://github.com/owner/repo/actions/runs/12345) ````` **Example:** `````markdown -Analysis based on [§456789](https://github.com/github/gh-aw/actions/runs/456789) +Analysis based on [#456789](https://github.com/github/gh-aw/actions/runs/456789) ````` ### 2. Document References for Workflow Runs @@ -32,9 +32,9 @@ When your analysis is based on information mined from one or more workflow runs, --- **References:** -- [§12345](https://github.com/owner/repo/actions/runs/12345) -- [§12346](https://github.com/owner/repo/actions/runs/12346) -- [§12347](https://github.com/owner/repo/actions/runs/12347) +- [#12345](https://github.com/owner/repo/actions/runs/12345) +- [#12346](https://github.com/owner/repo/actions/runs/12346) +- [#12347](https://github.com/owner/repo/actions/runs/12347) ````` **Guidelines:** From 0161ecdf32d389fa3c8f24c4c729acf77391a767 Mon Sep 17 00:00:00 2001 From: mrjf Date: Thu, 21 May 2026 07:48:17 -0700 Subject: [PATCH 3/3] Exclude auto-generated lock files from CodeQL actions analysis The crane.lock.yml is generated by 'gh aw compile' and contains patterns (issue_comment trigger + checkout) that CodeQL flags as 'checkout of untrusted code in trusted context'. The lock file includes branch validation but CodeQL cannot see through the generated structure. Exclude *.lock.yml from scanning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/codeql/codeql-config.yml | 5 +++++ .github/workflows/codeql.yml | 1 + 2 files changed, 6 insertions(+) create mode 100644 .github/codeql/codeql-config.yml diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..ad4791bb --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,5 @@ +name: "CodeQL configuration" + +paths-ignore: + # Auto-generated lock files from gh-aw compile + - ".github/workflows/*.lock.yml" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f8717eee..83e1c38a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -30,6 +30,7 @@ jobs: uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} + config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4