Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions .github/actions/checkout-eyrie/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ description: Clone Hawk ecosystem repos into hawk/external for hawk go.work

inputs:
ref:
description: Git ref to checkout (branch or tag)
description: Git ref to checkout only when allow_branch_fallback is true
required: false
default: main
allow_branch_fallback:
description: >
When true, clone a branch head if the Gitlink is missing (dev-only escape hatch).
Default false — missing or unreachable pins fail the job so releases never
silently track main.
required: false
default: "false"

runs:
using: composite
Expand All @@ -16,6 +23,7 @@ runs:
# Passed via env (not ${{ }} interpolation into the script) so an
# attacker-controlled ref (e.g. a fork branch name) cannot inject shell.
INPUT_REF: ${{ inputs.ref }}
ALLOW_BRANCH_FALLBACK: ${{ inputs.allow_branch_fallback }}
run: |
set -euo pipefail
mkdir -p "${GITHUB_WORKSPACE}/external"
Expand All @@ -33,24 +41,27 @@ runs:
# (e.g. by a squash-merge). A depth-1 clone can't check
# out older commits and fails with "unable to read tree".
git clone "https://github.com/GrayCodeAI/${repo}.git" "$dest"
if ! (cd "$dest" && git checkout --quiet "$commit" 2>/dev/null); then
echo "Submodule commit $commit not reachable, falling back to ref"
ref="$INPUT_REF"
if [ "$ref" = "main" ]; then
rm -rf "$dest"
git clone --depth=1 --branch main \
"https://github.com/GrayCodeAI/${repo}.git" "$dest"
else
(cd "$dest" && git fetch --depth=1 "origin" "$ref" 2>/dev/null && git checkout --quiet "$ref" 2>/dev/null) || (cd "$dest" && git fetch --depth=1 origin main 2>/dev/null && git checkout --quiet main 2>/dev/null)
fi
if ! (cd "$dest" && git checkout --quiet "$commit"); then
echo "::error::Pinned submodule commit $commit is not reachable in $repo"
echo "Refusing to test an unpinned branch head for a pinned Hawk commit."
exit 1
fi
else
if [ "${ALLOW_BRANCH_FALLBACK}" != "true" ]; then
echo "::error::Missing Gitlink for external/${repo} at HEAD"
echo "Hawk requires a pinned submodule commit for every engine."
echo "Record the pin with: git submodule update --init external/${repo}"
echo "and commit the Gitlink. Branch-head fallback is disabled by default"
echo "(set allow_branch_fallback=true only for local experiments)."
exit 1
fi
ref="$INPUT_REF"
# Fall back to main if the branch doesn't exist on the dependency repo.
# Optional escape hatch: fall back to main if the branch doesn't exist.
if ! git ls-remote --heads "https://github.com/GrayCodeAI/${repo}.git" "$ref" | grep -q .; then
echo "Branch '$ref' not found on $repo, falling back to main"
ref="main"
fi
echo "::warning::Cloning $repo at branch head '$ref' (no Gitlink; allow_branch_fallback=true)"
git clone --depth=1 --branch "$ref" \
"https://github.com/GrayCodeAI/${repo}.git" "$dest"
fi
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ runs:
- name: Create workspace
shell: bash
run: |
printf 'go 1.26.4\n\nuse .\n\nreplace (\n\tgithub.com/GrayCodeAI/hawk-core-contracts => ./external/hawk-core-contracts\n\tgithub.com/GrayCodeAI/eyrie => ./external/eyrie\n\tgithub.com/GrayCodeAI/inspect => ./external/inspect\n\tgithub.com/GrayCodeAI/sight => ./external/sight\n\tgithub.com/GrayCodeAI/tok => ./external/tok\n\tgithub.com/GrayCodeAI/trace => ./external/trace\n\tgithub.com/GrayCodeAI/yaad => ./external/yaad\n)\n' > go.work
printf 'go 1.26.5\n\nuse .\n\nreplace (\n\tgithub.com/GrayCodeAI/hawk-core-contracts => ./external/hawk-core-contracts\n\tgithub.com/GrayCodeAI/eyrie => ./external/eyrie\n\tgithub.com/GrayCodeAI/inspect => ./external/inspect\n\tgithub.com/GrayCodeAI/sight => ./external/sight\n\tgithub.com/GrayCodeAI/tok => ./external/tok\n\tgithub.com/GrayCodeAI/trace => ./external/trace\n\tgithub.com/GrayCodeAI/yaad => ./external/yaad\n)\n' > go.work
49 changes: 39 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ jobs:
run: bash ./scripts/check-shared-types-imports.sh
- name: ecosystem boundary guard
run: bash ./scripts/check-ecosystem-boundaries.sh
- name: internal layer boundary guard
run: bash ./scripts/check-internal-layer-imports.sh
- name: eyrie client boundary guard
run: bash ./scripts/check-eyrie-client-imports.sh

Expand Down Expand Up @@ -103,6 +105,35 @@ jobs:
fi
done

public-modules:
name: public module graph
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Verify public dependency graph
env:
GOWORK: "off"
run: |
go mod download
go mod verify
go build -mod=readonly ./cmd/hawk
go test ./... -count=1 -timeout=300s

submodule-release-parity:
name: submodule and module parity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- run: bash ./scripts/check-submodule-release-parity.sh

# -------------------------------------------------------------------------
# 3. Vet + static analysis — compiler-level correctness.
# -------------------------------------------------------------------------
Expand Down Expand Up @@ -330,18 +361,16 @@ jobs:
- name: Binary size check (linux/amd64 only)
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: |
size=$(go build -trimpath -o /tmp/hawk-bin ./cmd/hawk && wc -c < /tmp/hawk-bin)
# Use -ldflags="-s -w" to match release build size (Makefile LDFLAGS).
# This catches binary bloat regressions against the actual shipped artifact size.
size=$(go build -trimpath -ldflags="-s -w" -o /tmp/hawk-bin ./cmd/hawk && wc -c < /tmp/hawk-bin)
size_mb=$((size / 1024 / 1024))
echo "Binary size: ${size_mb}MB"
# Threshold bumped from 100MB → 110MB. The current dev binary
# with full instrumentation is ~103MB; the release build (with
# -ldflags="-s -w") sits at ~76MB. This job builds the dev binary
# (no -ldflags), so the 100MB threshold was firing on every CI run
# as a warning. Bump to 110MB to give ourselves headroom while we
# decide whether to add more size-reduction work. BOTH this and
# Makefile size-check must move together.
if [ "$size_mb" -gt 110 ]; then
echo "::warning::Binary size ${size_mb}MB exceeds 110MB threshold"
# Threshold lowered from 110MB → 80MB. With -ldflags="-s -w" the
# release binary is ~76MB. A 80MB threshold gives 4MB headroom for
# regression detection while still being realistic.
if [ "$size_mb" -gt 80 ]; then
echo "::warning::Binary size ${size_mb}MB exceeds 80MB threshold"
fi
rm -f /tmp/hawk-bin

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/compatibility-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
go-version: "1.26.5"
cache: true
- name: Structural validation (schema + version pins)
run: make compat-check
Expand Down
28 changes: 27 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,38 @@ jobs:
with:
fetch-depth: 0 # goreleaser needs full history for changelog

# Releases must use Gitlink pins only — never a branch head fallback.
- uses: ./.github/actions/checkout-eyrie
with:
allow_branch_fallback: "false"

- name: Verify Gitlink pins present
run: |
set -euo pipefail
missing=0
for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do
commit=$(git ls-tree HEAD "external/${repo}" | awk '{print $3}' || true)
if [ -z "$commit" ]; then
echo "::error::Release requires Gitlink for external/${repo}"
missing=1
else
head=$(git -C "external/${repo}" rev-parse HEAD)
if [ "$head" != "$commit" ]; then
echo "::error::external/${repo} checked out $head, Gitlink wants $commit"
missing=1
else
echo "OK external/${repo} @ ${commit:0:12}"
fi
fi
done
if [ "$missing" -ne 0 ]; then
exit 1
fi

- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
go-version: "1.26.5"
cache: true

- name: Run GoReleaser
Expand Down
2 changes: 1 addition & 1 deletion .shared-templates/workflows/compatibility-test.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
go-version: "1.26.5"
cache: true
- name: Structural validation (schema + version pins)
run: make compat-check
Expand Down
2 changes: 1 addition & 1 deletion .shared-templates/workflows/go-ci.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: "1.26.4"
GO_VERSION: "1.26.5"
GOPROXY: "https://proxy.golang.org,direct"
GOPRIVATE: "github.com/GrayCodeAI/*"
GONOSUMDB: "github.com/GrayCodeAI/*"
Expand Down
2 changes: 1 addition & 1 deletion .shared-templates/workflows/go-release.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.26.4"
go-version: "1.26.5"
cache: true

- name: Run GoReleaser
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ COPY . .
# this is the only correct source — `git describe` would always return empty
# because `.dockerignore` excludes `.git/` from the build context.
RUN rm -f go.work go.work.sum && \
{ echo "go 1.26.4"; echo; echo "use ."; echo; echo "replace ("; \
{ echo "go 1.26.5"; echo; echo "use ."; echo; echo "replace ("; \
for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do \
echo " github.com/GrayCodeAI/${repo} => ./external/${repo}"; \
done; echo ")"; } > go.work && \
Expand Down
16 changes: 11 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ GORELEASER := $(GOBIN_DIR)/goreleaser
# ---------------------------------------------------------------------------
# Phony declarations (alphabetical).
# ---------------------------------------------------------------------------
.PHONY: all bench boundaries build ci clean contracts-guard ecosystem-guard eyrie-client-guard peer-guard cover cover-new fmt help install lint lint-fix \
.PHONY: all bench boundaries build ci clean contracts-guard ecosystem-guard eyrie-client-guard peer-guard internal-layers-guard submodule-release-parity cover cover-new fmt help install lint lint-fix \
release security setup smoke path sync-external test test-10x test-live test-new test-race tidy version vet

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -110,7 +110,13 @@ eyrie-client-guard: ## Fail on new direct eyrie/client imports outside Hawk tran
peer-guard: ## Fail if support engines import each other instead of depending only on Hawk contracts.
bash ./scripts/check-support-repo-coupling.sh

boundaries: contracts-guard ecosystem-guard eyrie-client-guard peer-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos).
internal-layers-guard: ## Enforce one-way dependencies across stable Hawk internal layers.
bash ./scripts/check-internal-layer-imports.sh

boundaries: contracts-guard ecosystem-guard eyrie-client-guard peer-guard internal-layers-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos).

submodule-release-parity: ## Verify every go.mod ecosystem version resolves to its pinned Gitlink.
bash ./scripts/check-submodule-release-parity.sh

lint: ## Run golangci-lint.
@command -v $(GOLANGCI) >/dev/null 2>&1 || (echo "install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" && exit 1)
Expand Down Expand Up @@ -185,7 +191,7 @@ setup: ## Set up local development environment (go.work + external repos).
fi; \
done
@echo "Generating go.work..."
@echo "go 1.26.4" > go.work
@echo "go 1.26.5" > go.work
@echo "" >> go.work
@echo "use ." >> go.work
@echo "" >> go.work
Expand Down Expand Up @@ -265,8 +271,8 @@ build-static: ## Build fully static binaries for Linux (musl-compatible)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o bin/$(NAME)-linux-amd64-static $(MAIN_PKG)
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="$(LDFLAGS)" -o bin/$(NAME)-linux-arm64-static $(MAIN_PKG)

size-check: build ## Report binary size and warn if over threshold (110MB, matching CI).
size-check: build ## Report binary size and warn if over threshold (80MB, matching CI).
@SIZE=$$(stat -f%z bin/$(NAME) 2>/dev/null || stat -c%s bin/$(NAME) 2>/dev/null); \
MB=$$(echo "scale=1; $$SIZE / 1048576" | bc); \
echo "Binary size: $${MB} MB"; \
if [ $$SIZE -gt 115343360 ]; then echo "::warning::Binary size $${MB} MB exceeds 110 MB threshold (CI gate)"; fi
if [ $$SIZE -gt 83886080 ]; then echo "::warning::Binary size $${MB} MB exceeds 80 MB threshold (CI gate)"; fi
12 changes: 6 additions & 6 deletions cmd/agent_grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"sync"
"time"

"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
lipgloss "charm.land/lipgloss/v2"
"github.com/GrayCodeAI/hawk/internal/ui/icons"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

var (
Expand Down Expand Up @@ -70,7 +70,7 @@ type AgentPane struct {

// NewAgentPane creates a new agent pane for the given task.
func NewAgentPane(id, task string, width, height int) *AgentPane {
vp := viewport.New(width-2, height-3) // account for border + title
vp := viewport.New(viewport.WithWidth(width-2), viewport.WithHeight(height-3)) // account for border + title
return &AgentPane{
ID: id,
Task: task,
Expand Down Expand Up @@ -222,8 +222,8 @@ func (g *AgentGrid) Render() string {
pane := g.panes[idx]
pane.width = paneWidth
pane.height = paneHeight
pane.viewport.Width = paneWidth - 2
pane.viewport.Height = paneHeight - 3
pane.viewport.SetWidth(paneWidth - 2)
pane.viewport.SetHeight(paneHeight - 3)
colStrings = append(colStrings, pane.Render())
}
rowStrings = append(rowStrings, lipgloss.JoinHorizontal(lipgloss.Top, colStrings...))
Expand Down
12 changes: 6 additions & 6 deletions cmd/autonomy_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package cmd
import (
"strings"

"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
lipgloss "charm.land/lipgloss/v2"
"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

// autonomyPickerEntry is one selectable row in the picker.
Expand Down Expand Up @@ -47,7 +47,7 @@ func NewAutonomyPicker(width int) *AutonomyPicker {
ti.Placeholder = "Type to filter…"
ti.Focus()
ti.CharLimit = 40
ti.Width = 40
ti.SetWidth(40)

entries := make([]autonomyPickerEntry, 0, len(allAutonomyTiers))
for _, level := range allAutonomyTiers {
Expand Down Expand Up @@ -108,7 +108,7 @@ func (ap *AutonomyPicker) Update(msg tea.KeyMsg) (*autonomyPickerEntry, bool) {
return nil, false
}

switch msg.Type {
switch key := msg.Key(); key.Code {
case tea.KeyEsc:
ap.Close()
return nil, true
Expand Down Expand Up @@ -173,7 +173,7 @@ func (ap *AutonomyPicker) Render(viewWidth int) string {
b.WriteString(paletteDimStyle.Render(" (↑↓ navigate · Enter select · Esc dismiss)"))
b.WriteString("\n\n")

ap.input.Width = boxWidth - 4
ap.input.SetWidth(boxWidth - 4)
b.WriteString(paletteInputStyle.Width(boxWidth - 2).Render(ap.input.View()))
b.WriteString("\n\n")

Expand Down
10 changes: 5 additions & 5 deletions cmd/autonomy_picker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package cmd
import (
"testing"

tea "charm.land/bubbletea/v2"
"github.com/GrayCodeAI/hawk/internal/engine"
tea "github.com/charmbracelet/bubbletea"
)

func TestAutonomyPicker_HasAllFiveTiers(t *testing.T) {
Expand Down Expand Up @@ -39,15 +39,15 @@ func TestAutonomyPicker_EnterSelectsAndCloses(t *testing.T) {
ap := NewAutonomyPicker(80)
ap.Open(engine.AutonomySupervised)

chosen, handled := ap.Update(tea.KeyMsg{Type: tea.KeyDown})
chosen, handled := ap.Update(tea.KeyPressMsg{Code: tea.KeyDown})
if !handled || chosen != nil {
t.Fatalf("KeyDown should navigate, not select: chosen=%v handled=%v", chosen, handled)
}
if ap.Selected().Level != engine.AutonomyBasic {
t.Fatalf("expected selection to move to AutonomyBasic, got %v", ap.Selected().Level)
}

chosen, handled = ap.Update(tea.KeyMsg{Type: tea.KeyEnter})
chosen, handled = ap.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
if !handled {
t.Fatal("expected Enter to be handled")
}
Expand All @@ -63,7 +63,7 @@ func TestAutonomyPicker_EscClosesWithoutSelecting(t *testing.T) {
ap := NewAutonomyPicker(80)
ap.Open(engine.AutonomyBasic)

chosen, handled := ap.Update(tea.KeyMsg{Type: tea.KeyEsc})
chosen, handled := ap.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
if !handled || chosen != nil {
t.Fatalf("expected Esc to close without a selection, got chosen=%v", chosen)
}
Expand All @@ -77,7 +77,7 @@ func TestAutonomyPicker_FilterByName(t *testing.T) {
ap.Open(engine.AutonomyBasic)

for _, r := range "scout" {
ap.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}})
ap.Update(tea.KeyPressMsg{Code: r, Text: string(r)})
}
if len(ap.filtered) != 1 || ap.filtered[0].Level != engine.AutonomyBasic {
t.Fatalf("expected filter 'scout' to match only Basic tier, got %+v", ap.filtered)
Expand Down
Loading
Loading