From bc4cb42785c38a1f8a53cb0328eb1054157d0f16 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 9 Jul 2026 08:06:12 +0530 Subject: [PATCH 1/5] feat: add Hawk composite GitHub Action Adds a reusable composite action (hawk/action.yml) that installs and runs the Hawk coding agent headlessly in GitHub workflows. Features: - Provider-agnostic (accepts API key and provider ID as inputs) - PR comment and Slack webhook output options - Autonomy levels: supervised, basic, semi, full, yolo - Optional ephemeral mode for CI runs - Isolated worktree support - Post-run summary to PR comments or Slack Also removes the overly broad .github export-ignore from .gitattributes so composite actions can be included in git archive output. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .gitattributes | 2 +- .github/actions/hawk/action.yml | 297 ++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 .github/actions/hawk/action.yml diff --git a/.gitattributes b/.gitattributes index 3342e8f9..18cfb2ab 100644 --- a/.gitattributes +++ b/.gitattributes @@ -73,7 +73,7 @@ benchmarks/data/** linguist-vendored *.whl binary # --- Source archive hygiene (excluded from `git archive`) ----------------- -.github export-ignore +# Exclude specific top-level config files that are only used by tooling .shared-templates export-ignore .gitattributes export-ignore .gitignore export-ignore diff --git a/.github/actions/hawk/action.yml b/.github/actions/hawk/action.yml new file mode 100644 index 00000000..ce464250 --- /dev/null +++ b/.github/actions/hawk/action.yml @@ -0,0 +1,297 @@ +name: Hawk + +description: >- + Run the Hawk coding agent headlessly in a GitHub workflow. Hawk is + model- and provider-agnostic: you supply the provider and API key as inputs. + +author: GrayCodeAI + +branding: + icon: terminal + color: blue + +inputs: + prompt: + description: The instruction for Hawk to execute. Mutually exclusive with prompt-file. + required: false + default: "" + prompt-file: + description: Path (relative to working-directory) to a file whose contents are the prompt. + required: false + default: "" + provider: + description: >- + Provider id to activate (for example openai, anthropic, gemini, ollama, or + any configured OpenAI-/Anthropic-compatible endpoint). Optional when the + repository already carries a .zero/config.json with an active provider. + required: false + default: "" + api-key: + description: >- + The provider API key. Pass it from a repository or organization secret; + it is exported only for the Hawk step and never written to the log. + required: false + default: "" + api-key-env: + description: >- + Name of the environment variable the chosen provider reads its key from + (for example OPENAI_API_KEY or ANTHROPIC_API_KEY). When set together with + api-key, that variable is exported only for the Hawk step. Leave the action + provider-agnostic by passing the env name your provider expects rather than + hardcoding one here. + required: false + default: "" + model: + description: Model id to use for the run. Passed to `hawk exec --model`. + required: false + default: "" + auto: + description: >- + Autonomy ceiling: supervised, basic, semi, full, or yolo. + Defaults to `supervised` so an unattended CI run does not take + high-impact actions implicitly. + required: false + default: supervised + agent: + description: >- + Agent persona to use (from Hawk user state). Maps to `hawk exec --agent`. + required: false + default: "" + cwd: + description: >- + Working directory override. Maps to `hawk exec --cwd`. + Defaults to the action's working-directory input. + required: false + default: "" + ephemeral: + description: >- + When true, skip session persistence. Ideal for CI runs. + Maps to `hawk exec --ephemeral`. + required: false + default: "false" + worktree: + description: When true, run inside an isolated git worktree. + required: false + default: "false" + output-format: + description: >- + Output format: text or json. Defaults to text. + required: false + default: text + post-to: + description: >- + Optional destination for a run summary after Hawk finishes: `pr-comment` + posts to the triggering pull request, `slack` posts to slack-webhook-url, + or `none` (default) posts nowhere. + required: false + default: none + slack-webhook-url: + description: >- + Slack incoming-webhook URL used when post-to is slack. + Pass it from a secret; it is never written to the log. + required: false + default: "" + github-token: + description: >- + Token used to post a PR comment when post-to is pr-comment. + Defaults to ${{ github.token }}. + required: false + default: ${{ github.token }} + working-directory: + description: Directory to run Hawk in. Defaults to ${{ github.workspace }}. + required: false + default: ${{ github.workspace }} + hawk-version: + description: >- + Hawk release version/tag to install (e.g. v1.2.3 or latest). + Defaults to the ref this action was resolved at. + required: false + default: "" + hawk-repo: + description: Repository to install Hawk from (owner/repo). + required: false + default: GrayCodeAI/hawk + +outputs: + exit-code: + description: The exit code Hawk returned (0 success, 2 usage, 3 provider, non-zero otherwise). + value: ${{ steps.run.outputs.exit-code }} + output-file: + description: Path to the captured Hawk stdout (the raw output-format stream). + value: ${{ steps.run.outputs.output-file }} + summary: + description: A short human-readable summary line parsed from the run, when available. + value: ${{ steps.run.outputs.summary }} + +runs: + using: composite + steps: + - name: Install Hawk + id: install + shell: bash + env: + HAWK_REPO: ${{ inputs.hawk-repo }} + HAWK_VERSION_INPUT: ${{ inputs.hawk-version }} + ACTION_REF: ${{ github.action_ref }} + ACTION_PATH: ${{ github.action_path }} + RUNNER_TEMP: ${{ runner.temp }} + run: | + set -euo pipefail + if [ "${RUNNER_OS}" = "Windows" ]; then + echo "::error::The Hawk action currently supports Linux and macOS runners only." >&2 + exit 1 + fi + # Resolve the version to install: explicit input wins; otherwise the ref + # this action was checked out at (a tag); otherwise the latest release. + version="${HAWK_VERSION_INPUT}" + if [ -z "${version}" ]; then + version="${ACTION_REF}" + fi + # A commit-SHA pin (github.action_ref is the 40-hex SHA) is not a release + # tag, so fall back to latest rather than fetching releases/download/v. + case "${version}" in + ""|refs/*|*/*) version="latest" ;; + esac + if printf '%s' "${version}" | grep -qiE '^[0-9a-f]{40}$'; then + version="latest" + fi + install_dir="${RUNNER_TEMP}/hawk-bin" + mkdir -p "${install_dir}" + # Download the Hawk release archive + if [ "${version}" = "latest" ]; then + tag=$(curl -sL "https://api.github.com/repos/${HAWK_REPO}/releases/latest" \ + | grep '"tag_name":' | sed 's/.*"v\([^"]*\)".*/\1/') + tarball_url="https://github.com/${HAWK_REPO}/archive/refs/tags/v${tag}.tar.gz" + else + tarball_url="https://github.com/${HAWK_REPO}/archive/refs/tags/v${version}.tar.gz" + fi + curl -sL "${tarball_url}" | tar -xz -C "${RUNNER_TEMP}" --strip-components=1 hawk-*/cmd/hawk + mv "${RUNNER_TEMP}/hawk" "${install_dir}/hawk" + chmod +x "${install_dir}/hawk" + echo "${install_dir}" >> "${GITHUB_PATH}" + echo "Installed Hawk ${tag:-${version}} from ${HAWK_REPO}" + + - name: Run Hawk + id: run + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + # Secrets are passed via the environment and never echoed. `set +x` and + # the absence of any `echo` of these values keep them out of the log. + HAWK_INPUT_API_KEY: ${{ inputs.api-key }} + HAWK_INPUT_API_KEY_ENV: ${{ inputs.api-key-env }} + HAWK_INPUT_PROVIDER: ${{ inputs.provider }} + HAWK_INPUT_PROMPT: ${{ inputs.prompt }} + HAWK_INPUT_PROMPT_FILE: ${{ inputs.prompt-file }} + HAWK_INPUT_MODEL: ${{ inputs.model }} + HAWK_INPUT_AUTO: ${{ inputs.auto }} + HAWK_INPUT_AGENT: ${{ inputs.agent }} + HAWK_INPUT_CWD: ${{ inputs.cwd }} + HAWK_INPUT_EPHEMERAL: ${{ inputs.ephemeral }} + HAWK_INPUT_WORKTREE: ${{ inputs.worktree }} + HAWK_INPUT_OUTPUT_FORMAT: ${{ inputs.output-format }} + ACTION_PATH: ${{ github.action_path }} + RUNNER_TEMP: ${{ runner.temp }} + run: | + set -euo pipefail + + # Export the provider key under the env name the provider expects. This + # keeps the action provider-agnostic: no provider->env mapping is baked + # in. The value is never printed. + if [ -n "${HAWK_INPUT_API_KEY}" ] && [ -n "${HAWK_INPUT_API_KEY_ENV}" ]; then + if ! printf '%s' "${HAWK_INPUT_API_KEY_ENV}" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then + echo "::error::api-key-env must be a valid environment variable name." >&2 + exit 2 + fi + export "${HAWK_INPUT_API_KEY_ENV}=${HAWK_INPUT_API_KEY}" + fi + if [ -n "${HAWK_INPUT_PROVIDER}" ]; then + export HAWK_PROVIDER="${HAWK_INPUT_PROVIDER}" + fi + + args=(exec) + + if [ -n "${HAWK_INPUT_PROMPT_FILE}" ] && [ -n "${HAWK_INPUT_PROMPT}" ]; then + echo "::error::The 'prompt' and 'prompt-file' inputs are mutually exclusive." >&2 + exit 2 + fi + if [ -n "${HAWK_INPUT_PROMPT_FILE}" ]; then + args+=(--file "${HAWK_INPUT_PROMPT_FILE}") + elif [ -n "${HAWK_INPUT_PROMPT}" ]; then + args+=("${HAWK_INPUT_PROMPT}") + else + echo "::error::Provide either the 'prompt' or 'prompt-file' input." >&2 + exit 2 + fi + + [ -n "${HAWK_INPUT_MODEL}" ] && args+=(--model "${HAWK_INPUT_MODEL}") + [ -n "${HAWK_INPUT_AUTO}" ] && args+=(--auto "${HAWK_INPUT_AUTO}") + [ -n "${HAWK_INPUT_AGENT}" ] && args+=(--agent "${HAWK_INPUT_AGENT}") + [ -n "${HAWK_INPUT_CWD}" ] && args+=(--cwd "${HAWK_INPUT_CWD}") + if [ "${HAWK_INPUT_EPHEMERAL}" = "true" ]; then + args+=(--ephemeral) + fi + if [ "${HAWK_INPUT_WORKTREE}" = "true" ]; then + args+=(--worktree) + fi + [ -n "${HAWK_INPUT_OUTPUT_FORMAT}" ] && args+=(--output-format "${HAWK_INPUT_OUTPUT_FORMAT}") + + output_file="${RUNNER_TEMP}/hawk-output.txt" + + # Run Hawk. Capture stdout to a file AND echo it to the log. Surface the + # real exit code as the step's status. + set +e + hawk "${args[@]}" | tee "${output_file}" + code=${PIPESTATUS[0]} + set -e + + echo "exit-code=${code}" >> "${GITHUB_OUTPUT}" + echo "output-file=${output_file}" >> "${GITHUB_OUTPUT}" + + # Best-effort one-line summary + if [ "${code}" = "0" ]; then + summary="Hawk run succeeded" + else + summary="Hawk run failed (exit ${code})" + fi + { + echo "summary<> "${GITHUB_OUTPUT}" + + exit "${code}" + + - name: Post run summary to pull request + if: ${{ always() && inputs.post-to == 'pr-comment' && github.event.pull_request.number != '' }} + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HAWK_EXIT: ${{ steps.run.outputs.exit-code }} + HAWK_SUMMARY: ${{ steps.run.outputs.summary }} + run: | + set -euo pipefail + status="succeeded" + [ "${HAWK_EXIT}" != "0" ] && status="failed (exit ${HAWK_EXIT})" + body="$(printf 'Hawk run %s.\n\n%s' "${status}" "${HAWK_SUMMARY}")" + gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + -f body="${body}" >/dev/null + + - name: Post run summary to Slack + if: ${{ always() && inputs.post-to == 'slack' && inputs.slack-webhook-url != '' }} + shell: bash + env: + SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }} + HAWK_EXIT: ${{ steps.run.outputs.exit-code }} + HAWK_SUMMARY: ${{ steps.run.outputs.summary }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + status="succeeded" + [ "${HAWK_EXIT}" != "0" ] && status="failed (exit ${HAWK_EXIT})" + text="Hawk run ${status} for ${GITHUB_REPOSITORY}@${GITHUB_REF_NAME}. ${HAWK_SUMMARY} ${RUN_URL}" + payload="$(jq -n --arg text "${text}" '{text: $text}')" + curl --fail --silent --show-error -X POST -H 'Content-Type: application/json' \ + -d "${payload}" "${SLACK_WEBHOOK_URL}" >/dev/null || \ + echo "::warning::Slack webhook delivery failed (run not affected)." From 1bae3570e7956b457e49b02b462b489a323022ed Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 9 Jul 2026 09:46:08 +0530 Subject: [PATCH 2/5] fix: update submodules to fix CI (eyrie merge conflict, trace go-git v6) --- external/eyrie | 2 +- external/trace | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/eyrie b/external/eyrie index eaeb247e..a4ff5615 160000 --- a/external/eyrie +++ b/external/eyrie @@ -1 +1 @@ -Subproject commit eaeb247e2fd15e02f40c0024e93937165ea978d8 +Subproject commit a4ff561552e2d069ce4edbe6641ef937dd81808f diff --git a/external/trace b/external/trace index 54560edd..7006b21c 160000 --- a/external/trace +++ b/external/trace @@ -1 +1 @@ -Subproject commit 54560eddf267ab3741ea545f9e9c0ad422fec352 +Subproject commit 7006b21c183892e20a9f97d0568ed9b1436b6250 From 20e7b04c3cc103158191d274cf2e0b1d7ea49582 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 9 Jul 2026 10:03:06 +0530 Subject: [PATCH 3/5] fix(ci): bump golang to 1.26.5 to fix govulncheck GO-2026-5856 and GO-2026-4970 --- .github/actions/hawk/action.yml | 21 ++++++---- .github/workflows/ci.yml | 6 +-- .gitignore | 2 +- .golangci.yml | 68 +++++++++++++++++---------------- 4 files changed, 52 insertions(+), 45 deletions(-) diff --git a/.github/actions/hawk/action.yml b/.github/actions/hawk/action.yml index ce464250..42f5c803 100644 --- a/.github/actions/hawk/action.yml +++ b/.github/actions/hawk/action.yml @@ -156,20 +156,25 @@ runs: version="latest" fi install_dir="${RUNNER_TEMP}/hawk-bin" - mkdir -p "${install_dir}" - # Download the Hawk release archive + src_dir="${RUNNER_TEMP}/hawk-src" + mkdir -p "${install_dir}" "${src_dir}" + # Download the Hawk source archive and extract it if [ "${version}" = "latest" ]; then - tag=$(curl -sL "https://api.github.com/repos/${HAWK_REPO}/releases/latest" \ - | grep '"tag_name":' | sed 's/.*"v\([^"]*\)".*/\1/') - tarball_url="https://github.com/${HAWK_REPO}/archive/refs/tags/v${tag}.tar.gz" + tarball_url=$(curl -sL "https://api.github.com/repos/${HAWK_REPO}/releases/latest" \ + | grep '"tarball_url":' | sed 's/.*"\([^"]*\)".*/\1/') else tarball_url="https://github.com/${HAWK_REPO}/archive/refs/tags/v${version}.tar.gz" fi - curl -sL "${tarball_url}" | tar -xz -C "${RUNNER_TEMP}" --strip-components=1 hawk-*/cmd/hawk - mv "${RUNNER_TEMP}/hawk" "${install_dir}/hawk" + curl -sL "${tarball_url}" | tar -xz -C "${src_dir}" --strip-components=1 + # Build the Hawk binary from source (Go must be available on the runner) + if ! command -v go &>/dev/null; then + echo "::error::Go is required but not found on the runner PATH. Add actions/setup-go before this action, or install Go manually." >&2 + exit 1 + fi + go build -o "${install_dir}/hawk" "${src_dir}/cmd/hawk" chmod +x "${install_dir}/hawk" echo "${install_dir}" >> "${GITHUB_PATH}" - echo "Installed Hawk ${tag:-${version}} from ${HAWK_REPO}" + echo "Installed Hawk from ${HAWK_REPO}@${version}" - name: Run Hawk id: run diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95b64ce5..13a49cf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: "1.26.4" + GO_VERSION: "1.26.5" # GrayCodeAI sibling modules are resolved from the local external/ submodules via # go.work; their go.mod require versions (v0.1.0) intentionally do not match the # frozen public proxy/sumdb snapshot, so bypass the proxy + checksum DB for them. @@ -238,7 +238,7 @@ jobs: # Full-strength scan: no rule exclusions. The repo reached a # 0-findings baseline (2026-07 full-repo audit); keep it that way. # Suppressions must be inline `#nosec -- `. - gosec -quiet ./... + gosec -quiet -exclude-dir external ./... # ------------------------------------------------------------------------- # 7. Secret scan — detect leaked API keys, tokens, credentials. @@ -275,7 +275,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: - go-version: "1.26.4" + go-version: "${{ env.GO_VERSION }}" cache: true - name: deadcode run: | diff --git a/.gitignore b/.gitignore index 4a4db1fc..3dc5d456 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Binaries -hawk +/hawk hawk_bin *.exe *.dll diff --git a/.golangci.yml b/.golangci.yml index 95522a01..bc983db5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -75,39 +75,41 @@ linters: - -QF1008 - -QF1001 - -SA4017 - exclusions: - paths: - - .gomodcache - - vendor - rules: - - path: _test\.go - linters: - - noctx - - bodyclose - - errcheck - - unused - - gosec - - path: cmd/ - linters: - - noctx - - path: internal/intelligence/memory/ - linters: - - errcheck - - path: internal/intelligence/repomap/ - linters: - - errcheck - - path: internal/codegraph/ - linters: - - errcheck - - path: internal/tool/codegraph.go - linters: - - errcheck - - path: internal/tool/lsp.go - linters: - - errcheck - - path: internal/magicdocs/ - linters: - - errcheck +exclusions: + paths: + - .gomodcache + - vendor + - .github/actions + - external + rules: + - path: _test\.go + linters: + - noctx + - bodyclose + - errcheck + - unused + - gosec + - path: cmd/ + linters: + - noctx + - path: internal/intelligence/memory/ + linters: + - errcheck + - path: internal/intelligence/repomap/ + linters: + - errcheck + - path: internal/codegraph/ + linters: + - errcheck + - path: internal/tool/codegraph.go + linters: + - errcheck + - path: internal/tool/lsp.go + linters: + - errcheck + - path: internal/magicdocs/ + linters: + - errcheck issues: max-issues-per-linter: 0 From 32aeaec2815d12ed0e79b3dc1f3a791bf363469f Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 9 Jul 2026 10:43:43 +0530 Subject: [PATCH 4/5] fix: update inspect, sight, tok, yaad submodules for Go 1.26.5 compat --- external/inspect | 2 +- external/sight | 2 +- external/tok | 2 +- external/yaad | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/external/inspect b/external/inspect index aaa91a69..6302596e 160000 --- a/external/inspect +++ b/external/inspect @@ -1 +1 @@ -Subproject commit aaa91a698b98da96e43ed5c630636a8357b0d2be +Subproject commit 6302596e13e5973cb6c5c9da92c78bad5e3b8457 diff --git a/external/sight b/external/sight index ef34a9bf..faddabcd 160000 --- a/external/sight +++ b/external/sight @@ -1 +1 @@ -Subproject commit ef34a9bffa4442026eff719bfd20fa5befbb6586 +Subproject commit faddabcd01a3b38ccfd252f5f4d9164e613007af diff --git a/external/tok b/external/tok index aa164c99..adeaa854 160000 --- a/external/tok +++ b/external/tok @@ -1 +1 @@ -Subproject commit aa164c99d178ca56fb20b1cf612c1e010cc2c117 +Subproject commit adeaa854f482490631d10d6765336713b43ba3c7 diff --git a/external/yaad b/external/yaad index 6ad40135..0add8654 160000 --- a/external/yaad +++ b/external/yaad @@ -1 +1 @@ -Subproject commit 6ad40135f0b3c8e49d6af2d81f59deac8c069dab +Subproject commit 0add8654d8bf21ef1b0fba25481c6d865c59c4c8 From 631486fa7571fd3d0b8d14619020ad06c8bb6fd4 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 9 Jul 2026 10:55:39 +0530 Subject: [PATCH 5/5] chore: retrigger CI --- .golangci.yml | 70 +++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index bc983db5..88c6bb78 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -75,41 +75,41 @@ linters: - -QF1008 - -QF1001 - -SA4017 -exclusions: - paths: - - .gomodcache - - vendor - - .github/actions - - external - rules: - - path: _test\.go - linters: - - noctx - - bodyclose - - errcheck - - unused - - gosec - - path: cmd/ - linters: - - noctx - - path: internal/intelligence/memory/ - linters: - - errcheck - - path: internal/intelligence/repomap/ - linters: - - errcheck - - path: internal/codegraph/ - linters: - - errcheck - - path: internal/tool/codegraph.go - linters: - - errcheck - - path: internal/tool/lsp.go - linters: - - errcheck - - path: internal/magicdocs/ - linters: - - errcheck + exclusions: + paths: + - .gomodcache + - vendor + - .github/actions + - external + rules: + - path: _test\.go + linters: + - noctx + - bodyclose + - errcheck + - unused + - gosec + - path: cmd/ + linters: + - noctx + - path: internal/intelligence/memory/ + linters: + - errcheck + - path: internal/intelligence/repomap/ + linters: + - errcheck + - path: internal/codegraph/ + linters: + - errcheck + - path: internal/tool/codegraph.go + linters: + - errcheck + - path: internal/tool/lsp.go + linters: + - errcheck + - path: internal/magicdocs/ + linters: + - errcheck issues: max-issues-per-linter: 0