Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
960c3a8
v1.60.2.0 fix: free-suite drift on dev machines (eval-list cwd, gemin…
garrytan Aug 7, 2026
94993f7
v1.61.0.0 fix wave: guards failing open / silent failures (9 fixes, 4…
garrytan Aug 8, 2026
d078622
v1.62.0.0 feat: plan-mode auto-select at the review scope gate (#2533)
garrytan Aug 12, 2026
1d41ee3
v1.63.0.0 feat: GStack 2 fork port wave — egress receipts, context-bi…
garrytan Aug 14, 2026
008dd65
v1.64.0.0 fix wave: full tracker audit — 90 fixes, 52 issues closed, …
garrytan Aug 15, 2026
c118e24
v1.64.1.0 v1.64.1.0: the code-smell fix wave — every pipeline guard n…
garrytan Aug 15, 2026
0a92631
restore local work onto v1.64.1.0: /autobuilder-loop + /plan-delivera…
sneakygriff Aug 15, 2026
475b9bc
fix: cross-model forum findings on the restoration (Codex gpt-5.6-sol…
sneakygriff Aug 15, 2026
46f8510
fix(test): exempt the live repo tree from hermetic-wiring's operator-…
sneakygriff Aug 15, 2026
8a37d90
fix(test): repair three browse test files stranded by v1.63-v1.64.1 r…
sneakygriff Aug 15, 2026
beee8b4
feat: port the #48 preamble carve onto v1.64.1.0 — shared lazy preamb…
sneakygriff Aug 15, 2026
7dcc285
fix(egress): wire bin/gstack-artifacts-preamble's daily git fetch thr…
sneakygriff Aug 16, 2026
06f34c2
fix(test): snapshot.test.ts — forward the BrowserManager through the …
sneakygriff Aug 16, 2026
cff1299
fix(autobuilder-loop): worktree preflight gate — dirty start no longe…
sneakygriff Aug 16, 2026
f5eea04
fix(security): sanitize agent-consumed status fields in artifacts-pre…
sneakygriff Aug 16, 2026
28b940c
test: KEEP-IN-SYNC tripwire — artifacts-preamble twin must match brai…
sneakygriff Aug 16, 2026
268ce79
test: shared preamble/sections/ floors + index-reference existence gu…
sneakygriff Aug 16, 2026
f06268d
fix(config): normalize off/false/0 and on/true/1 aliases for codex_re…
sneakygriff Aug 16, 2026
09b116b
fix(brain-cache): recent-decisions digests name their source (ledger …
sneakygriff Aug 16, 2026
2e745a8
test: recipe-invariant pins for autobuilder-loop + plan-deliverables,…
sneakygriff Aug 16, 2026
07936e1
fix(carve): section index emits a relative-path fallback for vendored…
sneakygriff Aug 16, 2026
32eea11
fix(ship): Step 5 gate — repo-local binaries before bunx (with explic…
sneakygriff Aug 16, 2026
81a3b0b
regen: all host outputs + re-blessed ship goldens for the P2-ledger s…
sneakygriff Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 15 additions & 4 deletions .github/docker/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ RUN printf 'Acquire::Retries "5";\nAcquire::http::Timeout "30";\nAcquire::https:

# System deps (retry apt-get update + install as a unit — even Hetzner can blip).
# Includes xz-utils so the Node.js .tar.xz download below can decompress.
# python3: bin/gstack-jsonl-merge, gstack-brain-sync, gstack-detach, and other
# bash bins shell out to it (macOS ships python3; the base image doesn't).
# file: skill-validation's no-compiled-binaries-in-git check runs `file --mime-type`.
# poppler-utils: make-pdf's e2e gates hard-require pdftotext/pdffonts/pdfinfo in CI.
RUN for i in 1 2 3; do \
apt-get update && apt-get install -y --no-install-recommends \
git curl unzip xz-utils ca-certificates jq bc gpg && break || \
git curl unzip xz-utils ca-certificates jq bc gpg python3 file poppler-utils && break || \
(echo "apt retry $i/3 after failure"; sleep 10); \
done \
&& rm -rf /var/lib/apt/lists/*
Expand Down Expand Up @@ -61,10 +65,14 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL "https://nodejs.org
&& node --version \
&& npm --version

# Bun (install to /usr/local so non-root users can access it)
# Bun (install to /usr/local so non-root users can access it).
# The version MUST be passed as a positional arg — bun.sh/install ignores a
# BUN_VERSION env var, so the old `| BUN_VERSION=x.y.z bash` form silently
# installed latest on every image rebuild (observed: 1.3.13/1.3.14 drift vs
# the 1.3.10 devs run locally).
ENV BUN_INSTALL="/usr/local"
RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://bun.sh/install \
| BUN_VERSION=1.3.10 bash
| bash -s "bun-v1.3.10"

# Claude CLI
RUN npm i -g @anthropic-ai/claude-code
Expand All @@ -82,8 +90,10 @@ RUN npx playwright install-deps chromium
# (headed-xvfb, headed-orphan-cleanup) can exercise the Linux container
# auto-spawn path on every CI run. Without Xvfb in the image, the most
# common production --headed path goes untested.
# fonts-noto-color-emoji: the make-pdf emoji render gate needs a color-emoji
# fallback font (mirrors make-pdf-gate.yml's Ubuntu setup step).
RUN for i in 1 2 3; do \
apt-get update && apt-get install -y --no-install-recommends fonts-liberation fontconfig xvfb x11-utils && break || \
apt-get update && apt-get install -y --no-install-recommends fonts-liberation fonts-noto-color-emoji fontconfig xvfb x11-utils && break || \
(echo "fonts-liberation install retry $i/3"; sleep 10); \
done \
&& fc-cache -f \
Expand All @@ -105,6 +115,7 @@ RUN npx playwright install chromium \

# Verify everything works
RUN bun --version && node --version && claude --version && jq --version && gh --version \
&& python3 --version && command -v file && command -v pdftotext && command -v pdffonts && command -v pdfinfo \
&& npx playwright --version \
&& fc-match "Liberation Sans" | grep -qi "Liberation" \
|| (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1)
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/actionlint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
name: Workflow Lint
on: [push, pull_request]

# Cancel superseded runs for the same branch (matches evals.yml,
# windows-free-tests.yml, etc.). head_ref is set on pull_request; ref_name is
# the fallback for push so a rapid push series doesn't pile up stale lint runs.
concurrency:
group: actionlint-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true

jobs:
actionlint:
runs-on: ubicloud-standard-8
Expand Down
58 changes: 41 additions & 17 deletions .github/workflows/evals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,30 @@ jobs:
- if: steps.check.outputs.exists == 'false'
run: cp package.json bun.lock .github/docker/

# A fork PR's GITHUB_TOKEN only has `packages: read`, so pushing fails.
# Still BUILD (validates Dockerfile.ci changes), just don't publish. This
# job intentionally keeps no `if:` so fork PRs still get one real, honest
# green check here instead of a run where every job is grey.
- if: steps.check.outputs.exists == 'false'
uses: docker/build-push-action@v6
with:
context: .github/docker
file: .github/docker/Dockerfile.ci
push: true
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
tags: |
${{ steps.meta.outputs.tag }}
${{ env.IMAGE }}:latest

# Fork PRs never receive repository secrets (ANTHROPIC_API_KEY et al), so every
# API-calling eval fails at SDK auth before a model runs. Skip deterministically
# rather than leaving the outcome to Docker-cache luck: a warm cache let these
# run and fail, a cold one made build-image fail its push and the shards skip.
# Same-repo PRs, pushes, and workflow_dispatch keep full coverage. Fork work
# gets real coverage via a trusted base-repo branch.
evals:
runs-on: ${{ matrix.suite.runner || 'ubicloud-standard-8' }}
needs: build-image
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
container:
image: ${{ needs.build-image.outputs.image-tag }}
credentials:
Expand Down Expand Up @@ -169,19 +180,28 @@ jobs:
console.log("seeded", p);
'

# PTY smokes drive the interactive `claude` TUI and send /office-hours and
# /plan-ceo-review. Claude Code discovers user-scoped skills from
# $HOME/.claude/skills/<name>/SKILL.md, but .claude/skills is gitignored, so
# a fresh CI checkout has NO registry — claude prints "Unknown command:
# /plan-ceo-review". Mirror setup's --no-prefix registry minimally: a gstack
# root symlink (resolves the preamble's absolute ~/.claude/skills/gstack/bin/*
# and ~/.claude/skills/gstack/<skill>/sections/* paths) plus a per-skill
# top-level dir holding SKILL.md (+ sections) symlinks for the two skills
# PTY smokes drive the interactive `claude` TUI and send /office-hours,
# /plan-ceo-review, /plan-eng-review, and /plan-design-review. Claude Code
# discovers user-scoped skills from $HOME/.claude/skills/<name>/SKILL.md,
# but .claude/skills is gitignored, so a fresh CI checkout has NO registry
# — claude prints "Unknown command: /plan-ceo-review". Mirror setup's
# --no-prefix registry minimally: a gstack root symlink (resolves the
# preamble's absolute ~/.claude/skills/gstack/bin/* and
# ~/.claude/skills/gstack/<skill>/sections/* paths) plus a per-skill
# top-level dir holding SKILL.md (+ sections) symlinks for the four skills
# these tests invoke. No ./setup (it builds binaries, launches Chromium,
# installs fonts, reads a /dev/tty prompt) and no binary build (SKILL.md +
# bin/ + sections/ are committed). $HOME is /github/home here; the spawned
# claude inherits it (this runner adds no HOME/CLAUDE_CONFIG_DIR override,
# no hermetic mode) and the Seed step already proved claude reads $HOME.
#
# KEEP THIS STEP even though seedSkills/hermeticSkillsConfigDir() now
# registers skills for hermetic PTY children: that registry is SYMLINKS
# into the repo checkout, and this container's cross-mount symlinks
# defeat the TUI skill scanner (see the note inside the step below) —
# the real-file copies here are what the TUI actually reads. HOME is
# also not hermeticized, so the absolute ~/.claude/skills/gstack/...
# preamble paths resolve through the gstack root symlink this step makes.
- name: Register gstack skills for PTY smoke
if: matrix.suite.name == 'e2e-pty-plan-smoke'
run: |
Expand All @@ -201,7 +221,7 @@ jobs:
# registry recognized it, isolating the failure to the container's
# cross-mount symlink). Copy SKILL.md + sections as real files so the TUI
# reads them directly.
for s in office-hours plan-ceo-review; do
for s in office-hours plan-ceo-review plan-eng-review plan-design-review; do
rm -rf "${SKILLS_DIR:?}/$s"
mkdir -p "$SKILLS_DIR/$s"
cp "$REPO/$s/SKILL.md" "$SKILLS_DIR/$s/SKILL.md"
Expand All @@ -216,7 +236,7 @@ jobs:
# ~/.claude/skills/gstack symlink above.
PROJ_SKILLS="$REPO/.claude/skills"
mkdir -p "$PROJ_SKILLS"
for s in office-hours plan-ceo-review; do
for s in office-hours plan-ceo-review plan-eng-review plan-design-review; do
rm -rf "${PROJ_SKILLS:?}/$s"
mkdir -p "$PROJ_SKILLS/$s"
cp "$REPO/$s/SKILL.md" "$PROJ_SKILLS/$s/SKILL.md"
Expand All @@ -229,18 +249,22 @@ jobs:
for f in \
"$SKILLS_DIR/office-hours/SKILL.md" \
"$SKILLS_DIR/plan-ceo-review/SKILL.md" \
"$SKILLS_DIR/plan-eng-review/SKILL.md" \
"$SKILLS_DIR/plan-design-review/SKILL.md" \
"$SKILLS_DIR/gstack/bin/gstack-update-check" \
"$SKILLS_DIR/gstack/office-hours/sections/design-and-handoff.md" \
"$SKILLS_DIR/gstack/plan-ceo-review/sections/review-sections.md"; do
"$SKILLS_DIR/gstack/plan-ceo-review/sections/review-sections.md" \
"$SKILLS_DIR/gstack/plan-eng-review/sections/review-sections.md" \
"$SKILLS_DIR/gstack/plan-design-review/sections/review-sections.md"; do
if [ ! -e "$f" ]; then
echo "ERROR: skill-registry target missing (symlink dangles): $f" >&2
exit 1
fi
done
grep -m1 '^name: office-hours$' "$SKILLS_DIR/office-hours/SKILL.md" >/dev/null \
|| { echo "ERROR: office-hours SKILL.md missing 'name: office-hours' frontmatter" >&2; exit 1; }
grep -m1 '^name: plan-ceo-review$' "$SKILLS_DIR/plan-ceo-review/SKILL.md" >/dev/null \
|| { echo "ERROR: plan-ceo-review SKILL.md missing 'name: plan-ceo-review' frontmatter" >&2; exit 1; }
for s in office-hours plan-ceo-review plan-eng-review plan-design-review; do
grep -m1 "^name: $s\$" "$SKILLS_DIR/$s/SKILL.md" >/dev/null \
|| { echo "ERROR: $s SKILL.md missing 'name: $s' frontmatter" >&2; exit 1; }
done
echo "skill registry OK"

- name: Run ${{ matrix.suite.name }}
Expand All @@ -263,7 +287,7 @@ jobs:
report:
runs-on: ubicloud-standard-8
needs: evals
if: always() && github.event_name == 'pull_request'
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 5
permissions:
contents: read
Expand Down
184 changes: 184 additions & 0 deletions .github/workflows/free-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: Free Tests
# The full free suite (`bun test`: browse/test/ + test/ + make-pdf/test/ minus
# paid evals) previously ran in NO CI job — only Windows curated shards, paid
# evals, and doc-freshness gates existed. Two test files crashed at module load
# for 48 versions without any signal. This job closes that hole.
on:
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: free-tests-${{ github.head_ref }}
cancel-in-progress: true

env:
IMAGE: ghcr.io/${{ github.repository }}/ci

jobs:
# Same cached pre-baked toolchain image as evals.yml (only rebuilds on
# Dockerfile/lockfile change).
build-image:
runs-on: ubicloud-standard-8
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v4

- id: meta
run: echo "tag=${{ env.IMAGE }}:${{ hashFiles('.github/docker/Dockerfile.ci', 'package.json', 'bun.lock') }}" >> "$GITHUB_OUTPUT"

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check if image exists
id: check
run: |
if docker manifest inspect ${{ steps.meta.outputs.tag }} > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- if: steps.check.outputs.exists == 'false'
run: cp package.json bun.lock .github/docker/

- if: steps.check.outputs.exists == 'false'
uses: docker/build-push-action@v6
with:
context: .github/docker
file: .github/docker/Dockerfile.ci
push: true
tags: |
${{ steps.meta.outputs.tag }}
${{ env.IMAGE }}:latest

free-tests:
runs-on: ubicloud-standard-8
needs: build-image
container:
image: ${{ needs.build-image.outputs.image-tag }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
options: --user runner
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

# Bun creates root-owned temp dirs during Docker build. GH Actions runs as
# runner user with HOME=/github/home. Redirect bun's cache to a writable dir.
- name: Fix bun temp
run: |
mkdir -p /home/runner/.cache/bun
{
echo "BUN_INSTALL_CACHE_DIR=/home/runner/.cache/bun"
echo "BUN_TMPDIR=/home/runner/.cache/bun"
echo "TMPDIR=/home/runner/.cache"
} >> "$GITHUB_ENV"

# Several test files exercise real git operations (gstack-artifacts-init,
# session-update-autostash, team-mode, brain-sync) and bins that read the
# current branch (gstack-decision-search). The container checkout is owned
# by a different uid than `runner`, so git needs safe.directory, and
# commit-making tests need an identity.
- name: Git identity for git-exercising tests
run: |
git config --global user.email "ci@gstack.invalid"
git config --global user.name "gstack CI"
git config --global --add safe.directory '*'

# Same restore rationale as evals.yml: recursive copy beats symlink
# (realpath escapes workspace) and hardlink (cross-device overlay-fs).
- name: Restore deps
run: |
if [ -d /opt/node_modules_cache ] && diff -q /opt/node_modules_cache/.package.json package.json >/dev/null 2>&1; then
cp -r /opt/node_modules_cache node_modules
else
bun install
fi

- run: bun run build

# Fail fast if the container can't launch Chromium — the browse
# integration tests need it.
- name: Verify Chromium
run: |
echo "whoami=$(whoami) HOME=$HOME TMPDIR=${TMPDIR:-unset}"
bun -e "import {chromium} from 'playwright';const b=await chromium.launch({args:['--no-sandbox']});console.log('Chromium OK');await b.close()"

# ONE BUN PROCESS PER FILE, on purpose. A single multi-file `bun test`
# run of this suite is structurally unreliable here — observed twice
# while building this job:
# 1. Silent truncation: server-lifecycle tests stub process.exit, and
# shutdown's async timers can hit the REAL exit after restore,
# killing the whole bun process mid-suite with exit 0 and NO
# summary (died at file 47, then file 51, of 358).
# 2. Co-run state bleed: files green in isolation failed under
# multi-file module sharing.
# Per-file spawning makes truncation impossible by construction (the
# census drives the loop; a killed child is a recorded failure, not a
# vanished suite) and also covers the old exit-0-on-module-load-error
# Bun behavior. Same isolation model as scripts/test-paid-shards.ts.
- name: Run free suite (per-file isolation)
shell: bash
run: |
set -o pipefail
# Container-incompatible files, each with a reason (same curated-
# exclusion pattern as the Windows shards in test-free-shards.ts).
# Anything NOT on this list that fails still fails the job. Trimming
# this list is tracked follow-up work.
declare -A SKIP=(
[browse/test/compare-board.test.ts]="pre-existing env failure (also fails on dev machines; needs a display-shaped env)"
[browse/test/handoff.test.ts]="needs the headed Chrome-for-Testing build (headless-only container)"
[browse/test/snapshot.test.ts]="pre-existing env failure (viewport/tab timing under container load)"
[browse/test/extension-sender-auth.test.ts]="extension identity checks need a real chrome-extension origin"
[browse/test/security-sidepanel-dom.test.ts]="sidepanel DOM harness needs the extension loaded headed"
[browse/test/terminal-agent-integration.test.ts]="real PTY round-trip; container TTY semantics differ"
[browse/test/xvfb.test.ts]="tests xvfb management; container has no X server to manage"
[browse/test/security-audit-r2.test.ts]="one behavioral tmpdir-allowlist test breaks under this job's TMPDIR override (bun temp-dir workaround above)"
[design/test/variants-retry-after.test.ts]="known timing flake, tracked in TODOS.md (HTTP-date Retry-After rounding)"
)
FILES=$(bun run scripts/test-free-shards.ts --list | grep -E '^ (browse/|test/|make-pdf/|design/)' | sed 's/^ //')
TOTAL=$(echo "$FILES" | wc -l | tr -d ' ')
echo "Enumerated $TOTAL free test files"
FAILED=""
N=0
SKIPPED=0
for f in $FILES; do
N=$((N+1))
if [ -n "${SKIP[$f]:-}" ]; then
echo "SKIP [$N/$TOTAL] $f — ${SKIP[$f]}"
SKIPPED=$((SKIPPED+1))
continue
fi
if ! bun test "$f" > /tmp/one.log 2>&1; then
echo "FAIL [$N/$TOTAL] $f"
tail -30 /tmp/one.log
FAILED="$FAILED $f"
fi
done
echo "Skipped $SKIPPED container-incompatible files (reasons above)."
# Tree-mutation tripwire: a test that rewrites tracked files poisons
# every later file in the loop with confusing failures (observed:
# gstack-config's skill_prefix auto-relink patched 52 SKILL.md names,
# failing five unrelated suites downstream). Name the real culprit.
MUTATED=$(git status --porcelain --untracked-files=no)
if [ -n "$MUTATED" ]; then
echo ""
echo "A test mutated tracked files in the working tree — later failures may be collateral:"
echo "$MUTATED"
FAILED="$FAILED [tree-mutation]"
fi
if [ -n "$FAILED" ]; then
echo ""
echo "Failed files:$FAILED"
exit 1
fi
echo "All $((TOTAL-SKIPPED)) runnable files green."
Loading
Loading