Skip to content

Worker bridged implementer path (#431 follow-up) #1815

Worker bridged implementer path (#431 follow-up)

Worker bridged implementer path (#431 follow-up) #1815

Workflow file for this run

name: flow-next
on:
push:
branches: [main]
pull_request:
branches: [main]
# Weekly drift backstop: full ubuntu / python 3.x run (units + smokes).
# The 3.x row was demoted from the per-PR matrix (it duplicated ubuntu-3.11
# almost entirely); day-to-day 3.x coverage rides python-intermediate-smoke,
# and this cron catches anything a new stable Python breaks in the full gate.
schedule:
- cron: "0 3 * * 1" # Mondays 03:00 UTC
workflow_dispatch:
inputs:
suite_mode:
description: "Unit-suite mode for this exact ref"
required: true
type: choice
options:
- parallel
- serial
- shuffle
default: parallel
pattern:
description: "Optional single-file glob relative to plugins/flow-next/tests (for example test_gate_receipt.py)"
required: false
type: string
default: ""
verbose:
description: "Print per-test names and failing-file output"
required: false
type: boolean
default: false
file_timeout:
description: "Per-file timeout in seconds (1-900)"
required: false
type: string
default: "900"
legacy_baseline:
description: "Measurement lever (fn-155) - run the unit suite at the pre-CI-detection job count, computed per runner"
required: false
type: boolean
default: false
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
units_matrix: ${{ steps.classify.outputs.units_matrix }}
smokes_matrix: ${{ steps.classify.outputs.smokes_matrix }}
run_smokes: ${{ steps.classify.outputs.run_smokes }}
run_windows_stub: ${{ steps.classify.outputs.run_windows_stub }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Classify change and emit job matrices
id: classify
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: python scripts/ci/classify_changes.py
# ──────────── Units (static checks + parallel unit suite) ────────────
# Split from the smokes so the two ~6-9m halves run side by side per OS
# instead of back to back. Neither job depends on the other.
units:
timeout-minutes: 35
needs: changes
strategy:
fail-fast: false
matrix:
# Explicit 3.11 rows preserve the minimum-runtime contract; the
# latest-stable `3.x` full gate runs on the weekly schedule and in
# python-intermediate-smoke, not per-PR.
include: ${{ fromJSON(needs.changes.outputs.units_matrix) }}
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash # Git Bash on Windows; bash on Linux/macOS — unifies the matrix.
steps:
- name: Disable git autocrlf so checkout preserves LF on Windows
run: git config --global core.autocrlf false
# Default Windows-runner config converts LF→CRLF on checkout, which
# changes heredoc bytes that smokes compare byte-identically against
# flowctl output. Force LF on all OSes for consistent fixtures.
# Reached-path B0/B1 validation reads immutable prompt sources from their
# recorded commits. A shallow checkout makes valid frozen evidence look
# corrupt because those commits are absent.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Configure git identity (smoke tests create commits)
run: |
git config --global user.email "ci@flow-next.test"
git config --global user.name "flow-next CI"
git config --global init.defaultBranch main
# ──────────── Static checks (cheap, fail fast) ────────────
- name: Syntax check — flowctl.py
run: python -m py_compile plugins/flow-next/scripts/flowctl.py
# Correctness-only ruleset; see ruff.toml for what is deliberately out.
# PIN THE VERSION: ruff 0.16 moved its default set from 59 rules to 413,
# which turns an unpinned `ruff` into an unannounced CI break.
# One row only - lint results do not vary by OS or Python version.
- name: Lint — ruff
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
run: |
python -m pip install --disable-pip-version-check ruff==0.16.0
ruff check .
- name: Syntax check — ralph.sh
run: bash -n plugins/flow-next/skills/flow-next-ralph-init/templates/ralph.sh
# ──────────── Python unit tests (file-level parallel, fn-119) ────────────
# Canonical full-suite entrypoint: scripts/run_tests_parallel.py
# Shards by FILE only (never within a file). Default jobs = the runner's
# full core count on CI and cores-2 locally (fn-155; the job-count policy
# lives in the runner, not here). --serial is the equivalence fallback.
# workflow_dispatch suite_mode supports focused, serial, and shuffled
# exact-ref diagnostics without changing ordinary PR/push behavior.
# workflow_dispatch.inputs.legacy_baseline is the fn-155 measurement
# lever: it reproduces the pre-change job count from this same commit,
# computed per runner because nothing establishes that all three OS legs
# report the same core count. Inert on push and pull_request.
- name: Python unit tests - parallel suite (fn-119)
env:
LEGACY_BASELINE: ${{ github.event.inputs.legacy_baseline }}
SUITE_MODE: ${{ github.event.inputs.suite_mode }}
TEST_PATTERN: ${{ github.event.inputs.pattern }}
TEST_VERBOSE: ${{ github.event.inputs.verbose }}
FILE_TIMEOUT: ${{ github.event.inputs.file_timeout }}
run: |
JOBS_ARGS=()
if [ "$LEGACY_BASELINE" = "true" ]; then
JOBS_ARGS=(--jobs "$(python -c 'import os; print(max(1, (os.cpu_count() or 2) - 2))')")
echo "Legacy baseline (fn-155): ${JOBS_ARGS[*]}"
fi
MODE_ARGS=()
PATTERN_ARGS=()
VERBOSE_ARGS=()
TIMEOUT_ARGS=()
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "$SUITE_MODE" in
parallel) ;;
serial) MODE_ARGS=(--serial) ;;
shuffle) MODE_ARGS=(--shuffle --seed "$GITHUB_RUN_ID") ;;
*) echo "Invalid suite_mode: $SUITE_MODE (expected parallel, serial, or shuffle)" >&2; exit 2 ;;
esac
if [ -n "$TEST_PATTERN" ]; then
case "$TEST_PATTERN" in
*/*|*\\*) echo "pattern must be a filename glob relative to plugins/flow-next/tests" >&2; exit 2 ;;
esac
PATTERN_ARGS=(--pattern "$TEST_PATTERN")
fi
if [ "$TEST_VERBOSE" = "true" ]; then
VERBOSE_ARGS=(--verbose)
fi
case "$FILE_TIMEOUT" in
''|*[!0-9]*) echo "file_timeout must be an integer from 1 to 900" >&2; exit 2 ;;
esac
if [ "$FILE_TIMEOUT" -lt 1 ] || [ "$FILE_TIMEOUT" -gt 900 ]; then
echo "file_timeout must be an integer from 1 to 900" >&2
exit 2
fi
TIMEOUT_ARGS=(--file-timeout "$FILE_TIMEOUT")
echo "Diagnostic mode: $SUITE_MODE; pattern=${TEST_PATTERN:-test_*.py}; timeout=${FILE_TIMEOUT}s"
fi
# No Windows-only exclusions (fn-120, R8): every OS leg runs the SAME
# discovered corpus. fn-119 shipped a six-file Windows EXCLUDES list
# for latent incompatibilities in files that had never run on Windows
# CI; fn-120.1/.2 fixed the five deterministic ones and fn-120.3
# retired the last (test_backend_spec.py - no hang reproduced on
# windows-2025, and the runner now isolates + kills each shard's
# process tree). Never re-add a per-OS filter here: an exclusion is
# invisible debt. Bound a suspect file with --file-timeout instead.
python scripts/run_tests_parallel.py "${JOBS_ARGS[@]}" "${MODE_ARGS[@]}" "${PATTERN_ARGS[@]}" "${VERBOSE_ARGS[@]}" "${TIMEOUT_ARGS[@]}"
# ──────────── Smokes (cursor install + functional bash smokes) ────────────
# Runs in parallel with the units job on each OS; skipped entirely on
# docs-only PR and main push ranges.
smokes:
timeout-minutes: 30
needs: changes
if: needs.changes.outputs.run_smokes == 'true'
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.changes.outputs.smokes_matrix) }}
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash # Git Bash on Windows; bash on Linux/macOS — unifies the matrix.
steps:
- name: Disable git autocrlf so checkout preserves LF on Windows
run: git config --global core.autocrlf false
# Default Windows-runner config converts LF→CRLF on checkout, which
# changes heredoc bytes that smokes compare byte-identically against
# flowctl output. Force LF on all OSes for consistent fixtures.
# Reached-path B0/B1 validation reads immutable prompt sources from their
# recorded commits. A shallow checkout makes valid frozen evidence look
# corrupt because those commits are absent.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Configure git identity (smoke tests create commits)
run: |
git config --global user.email "ci@flow-next.test"
git config --global user.name "flow-next CI"
git config --global init.defaultBranch main
# ──────────── Cursor local-install (1.9.0) ────────────
# The bash + PowerShell Cursor installers (scripts/install-cursor.{sh,ps1})
# must not drift, and must actually produce a complete + clean install on
# each OS. Installer-parity unit tests ride the parallel suite in the
# units job; the real-install smoke runs the platform's OWN installer
# (rsync on Unix, robocopy on Windows) into ~/.cursor/plugins/local and
# verifies against the source tree (component trees match 1:1; codex/
# tests/ *.pyc excluded).
- name: install-cursor.sh real-install smoke (Unix)
if: always() && runner.os != 'Windows'
run: |
bash "$GITHUB_WORKSPACE/scripts/install-cursor.sh"
python "$GITHUB_WORKSPACE/scripts/ci/verify_cursor_install.py"
- name: install-cursor.ps1 real-install smoke (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
./scripts/install-cursor.ps1
python ./scripts/ci/verify_cursor_install.py
# ──────────── Functional smokes (ordered cheap-first) ────────────
# All smokes refuse to run from the main plugin repo, so cd into the
# runner's temp dir before invoking. $RUNNER_TEMP is set on every
# GitHub-hosted runner (ubuntu/macos/windows).
- name: pick_python_test.sh (Windows 9009-stub probe regression, POSIX/Git-Bash, fn-77.5)
if: always() && runner.os != 'Windows'
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/pick_python_test.sh"
# POSIX/mac-linux unit layer for the interpreter probe: a fake 9009
# `python3` stub first on PATH must be rejected by the shared resolver +
# the bash launcher, PYTHON_BIN override is itself probed, py -3 preferred,
# python3 still first on mac/linux (R8). The real windows-latest end-to-end
# of flowctl.cmd + the bash launcher lives in the windows-python3-stub job.
- name: ci_test.sh (flowctl integration, 58 cases)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/ci_test.sh"
- name: resolve-pr_smoke_test.sh (58 cases, ~1s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/resolve-pr_smoke_test.sh"
- name: strategy_smoke_test.sh (62 cases, ~5s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/strategy_smoke_test.sh"
- name: audit_smoke_test.sh (41 cases, ~6s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/audit_smoke_test.sh"
- name: glossary_smoke_test.sh (80 cases, ~7s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/glossary_smoke_test.sh"
- name: prospect_smoke_test.sh (94 cases, ~60s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/prospect_smoke_test.sh"
- name: make-pr_smoke_test.sh (67 cases, ~7s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/make-pr_smoke_test.sh"
- name: map_smoke_test.sh (75 cases, fn-50.1 + fn-50.6 directory-level ignore + config-state --json guards)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/map_smoke_test.sh"
- name: impl-review_smoke_test.sh (74 cases, ~70s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/impl-review_smoke_test.sh"
- name: smoke_test.sh (130 cases, ~100s)
if: always()
run: |
cd "$RUNNER_TEMP"
bash "$GITHUB_WORKSPACE/plugins/flow-next/scripts/smoke_test.sh"
# Smokes intentionally skipped (require external CLIs not on GitHub runners):
# - ralph_smoke_test.sh / ralph_smoke_rp.sh — need claude / codex / rp-cli
# - plan_review_prompt_smoke.sh — needs rp-cli
python-intermediate-smoke:
timeout-minutes: 10
name: Python ${{ matrix.python-version }} compatibility smoke
needs: changes
if: needs.changes.outputs.run_smokes == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# `3.x` (latest stable) rides this cheap contract leg per-PR now that
# the full ubuntu-3.x matrix row is demoted to the weekly schedule.
python-version: ["3.12", "3.13", "3.x"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Compile flowctl source and bootstrap
run: |
python -m py_compile \
plugins/flow-next/scripts/flowctl.py \
plugins/flow-next/scripts/flowctl_bootstrap.py
- name: Runtime, launcher, and usage contracts
run: |
cd plugins/flow-next/tests
python -m unittest \
test_startup_bootstrap \
test_bin_launcher_parity \
test_cmd_usage \
test_precheck_mode_contract \
-q
# ─────────────────────────────────────────────────────────────────────────────
# fn-77.5 (R10): real windows-latest end-to-end. A DETERMINISTIC fake `python3`
# stub (exit 9009 — the Microsoft Store App Execution Alias failure mode) is
# forced FIRST on PATH for BOTH a Git Bash step AND a PowerShell/cmd step (NOT
# reliant on the runner's own Store alias, which may be absent). A real
# python.org Python (+ the py launcher, already on the hosted image) is present
# via setup-python; then the bash `flowctl` launcher and the `flowctl.cmd` batch
# shim must each probe past the stub, resolve a working interpreter, and run
# `flowctl init` — proving the fix works in the exact shells where Claude
# Desktop / native Codex / Cursor invoke flowctl on Windows.
# ─────────────────────────────────────────────────────────────────────────────
windows-python3-stub:
needs: changes
if: needs.changes.outputs.run_windows_stub == 'true'
timeout-minutes: 10
runs-on: windows-latest
steps:
- name: Disable git autocrlf so the LF-pinned bash launcher stays LF
shell: bash
run: git config --global core.autocrlf false
# The bash `flowctl` launcher is pinned eol=lf (.gitattributes); CRLF would
# break `bash` parsing. Default Windows-runner autocrlf would rewrite it.
- uses: actions/checkout@v4
- name: Setup Python (real python.org interpreter + py launcher)
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Configure git identity (flowctl init touches git)
shell: bash
run: |
git config --global user.email "ci@flow-next.test"
git config --global user.name "flow-next CI"
git config --global init.defaultBranch main
- name: Build a deterministic 9009 python3 stub for both shells
shell: bash
run: |
set -euo pipefail
STUB="$RUNNER_TEMP/pystub"
mkdir -p "$STUB"
# Extensionless stub for Git Bash: MSYS resolves bare `python3` to this
# shebang script (it does not append .cmd), so `python3` runs the stub.
{
printf '#!/bin/bash\n'
printf 'echo "Python was not found; run without arguments to install from the Microsoft Store, or disable this shortcut from Settings > Apps > Advanced app settings > App execution aliases." >&2\n'
printf 'exit 9009\n'
} > "$STUB/python3"
chmod +x "$STUB/python3"
# .cmd stub for cmd.exe / PowerShell: PATHEXT resolves `python3` here.
{
printf '@ECHO OFF\n'
printf 'ECHO Python was not found; disable the Microsoft Store App execution alias. 1>&2\n'
printf 'EXIT /b 9009\n'
} > "$STUB/python3.cmd"
# Prove the stub really is non-functional before wiring it onto PATH.
if "$STUB/python3" -c "import sys" >/dev/null 2>&1; then
echo "FAIL: stub python3 should exit non-zero" >&2; exit 1
fi
printf 'PYSTUB_DIR=%s\n' "$STUB" >> "$GITHUB_ENV"
- name: Build deterministic working-but-too-old probes for both shells
shell: bash
run: |
set -euo pipefail
OLD="$RUNNER_TEMP/pyold"
mkdir -p "$OLD"
for name in py python3 python; do
{
printf '#!/bin/bash\n'
printf 'exit 3\n'
} > "$OLD/$name"
chmod +x "$OLD/$name"
{
printf '@ECHO OFF\r\n'
printf 'EXIT /b 3\r\n'
} > "$OLD/$name.cmd"
done
printf 'PYOLD_DIR=%s\n' "$OLD" >> "$GITHUB_ENV"
- name: Git Bash — bash `flowctl` bypasses the 9009 stub
shell: bash
run: |
set -euo pipefail
export PATH="$PYSTUB_DIR:$PATH"
echo "python3 resolves to: $(command -v python3)"
# The stub is genuinely first on PATH and non-functional.
if python3 -c "import sys" >/dev/null 2>&1; then
echo "FAIL: python3 stub should exit non-zero under Git Bash" >&2; exit 1
fi
FLOWCTL="$GITHUB_WORKSPACE/plugins/flow-next/scripts/flowctl"
REPO="$RUNNER_TEMP/bash-stub-repo"; mkdir -p "$REPO"; cd "$REPO"; git init -q
OUT="$("$FLOWCTL" init --json 2>&1)" || { echo "FAIL: launcher exited non-zero"; echo "$OUT"; exit 1; }
echo "$OUT"
echo "$OUT" | grep -q '"success": true' || { echo "FAIL: flowctl init did not succeed" >&2; exit 1; }
if echo "$OUT" | grep -q "Python was not found"; then
echo "FAIL: stub message leaked — launcher did not bypass the stub" >&2; exit 1
fi
echo "PASS: bash flowctl bypassed the 9009 python3 stub"
- name: PowerShell — `flowctl.cmd` bypasses the 9009 stub
shell: pwsh
run: |
# GitHub pwsh sets $ErrorActionPreference='Stop'; with the 7.3+ default
# $PSNativeCommandUseErrorActionPreference=$true a non-zero native exit
# THROWS. We invoke a stub that is MEANT to fail, so drive control flow
# off $LASTEXITCODE explicitly instead.
$PSNativeCommandUseErrorActionPreference = $false
$env:PATH = "$env:PYSTUB_DIR;$env:PATH"
Write-Host "python3 resolves to: $((Get-Command python3 -ErrorAction SilentlyContinue).Source)"
# The stub is genuinely first on PATH and non-functional (native 9009).
& python3 -c "import sys" 2>$null
if ($LASTEXITCODE -eq 0) { Write-Error "python3 stub should exit non-zero in pwsh"; exit 1 }
$flowctl = Join-Path $env:GITHUB_WORKSPACE "plugins/flow-next/scripts/flowctl.cmd"
$repo = Join-Path $env:RUNNER_TEMP "cmd-stub-repo"
New-Item -ItemType Directory -Force -Path $repo | Out-Null
Set-Location $repo
git init -q
$outputLines = & $flowctl init --json 2>&1
$exitCode = $LASTEXITCODE
$out = $outputLines | Out-String
Write-Host $out
if ($exitCode -ne 0) { Write-Error "flowctl.cmd exited $exitCode"; exit 1 }
if ($out -notmatch '"success": true') { Write-Error "flowctl.cmd init did not succeed"; exit 1 }
if ($out -match "Python was not found") { Write-Error "stub message leaked — flowctl.cmd did not bypass the stub"; exit 1 }
Write-Host "PASS: flowctl.cmd bypassed the 9009 python3 stub"
ci:
name: CI
if: always()
needs: [changes, units, smokes, python-intermediate-smoke, windows-python3-stub]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every selected job
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
python3 - <<'PYTHON'
import json, os
needs = json.loads(os.environ['NEEDS_JSON'])
required = {'changes', 'units'}
outputs = needs['changes'].get('outputs', {})
if outputs.get('run_smokes') == 'true':
required.update(('smokes', 'python-intermediate-smoke'))
if outputs.get('run_windows_stub') == 'true':
required.add('windows-python3-stub')
for name, job in needs.items():
allowed = {'success'} if name in required else {'success', 'skipped'}
if job['result'] not in allowed:
raise SystemExit(f"{name}: {job['result']}")
PYTHON