Skip to content

fix(chart): premise-first cascades and unambiguous initial-map aliase… #1143

fix(chart): premise-first cascades and unambiguous initial-map aliase…

fix(chart): premise-first cascades and unambiguous initial-map aliase… #1143

name: flow-next
on:
push:
branches: [main]
paths:
- "plugins/flow-next/**"
- ".github/workflows/test-flow-next.yml"
# Dogfood copies guarded against template drift (test_dogfood_template_parity).
- ".flow/usage.md"
- ".flow/templates/spec.md"
# Cursor + Codex local-installers live at repo-root scripts/ — outside
# plugins/flow-next/**, so trigger explicitly to run the install smokes.
- "scripts/install-cursor.sh"
- "scripts/install-cursor.ps1"
- "scripts/install-codex.sh"
- "scripts/ci/verify_cursor_install.py"
# Root Cursor team-marketplace manifest (fn-123) — outside plugins/**
- ".cursor-plugin/**"
# fn-119 parallel unit-suite entrypoint
- "scripts/run_tests_parallel.py"
# Ruff gate: config, plus every path `ruff check .` actually lints.
# These must stay in step - a filter narrower than the lint scope
# lets a violation land without the gate ever running (#244 review).
- "ruff.toml"
- "scripts/**.py"
- "agent_docs/**.py"
pull_request:
branches: [main]
paths:
- "plugins/flow-next/**"
- ".github/workflows/test-flow-next.yml"
- ".flow/usage.md"
- ".flow/templates/spec.md"
- "scripts/install-cursor.sh"
- "scripts/install-cursor.ps1"
- "scripts/install-codex.sh"
- "scripts/ci/verify_cursor_install.py"
- ".cursor-plugin/**"
- "scripts/run_tests_parallel.py"
# Ruff gate: config, plus every path `ruff check .` actually lints.
# These must stay in step - a filter narrower than the lint scope
# lets a violation land without the gate ever running (#244 review).
- "ruff.toml"
- "scripts/**.py"
- "agent_docs/**.py"
workflow_dispatch:
inputs:
shuffle:
description: "Ordering canary - run unit suite with --shuffle (manual/weekly; file-level only)"
required: false
type: boolean
default: false
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
jobs:
test:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python-version: "3.11"
- os: macos-latest
python-version: "3.11"
- os: windows-latest
python-version: "3.11"
# Latest stable full gate. `3.x` advances with setup-python while the
# explicit 3.11 rows preserve the minimum-runtime contract.
- os: ubuntu-latest
python-version: "3.x"
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.inputs.shuffle is the ordering canary (manual /
# weekly; do not schedule a new workflow).
# 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 }}
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
SHUFFLE_ARGS=()
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ "${{ github.event.inputs.shuffle }}" = "true" ]; then
SHUFFLE_ARGS=(--shuffle --seed "$GITHUB_RUN_ID")
echo "Ordering canary: --shuffle --seed $GITHUB_RUN_ID"
fi
EXCLUDES=()
if [ "$RUNNER_OS" = "Windows" ]; then
# Windows-only exclusions (fn-119): latent incompatibilities in files
# NEVER exercised on windows CI before the parallel entrypoint
# (pre-fn-119 windows unit coverage was 29/87 files; these keep it
# at 81/87 - strictly wider than before). Causes on the fn-119 PR:
# backend_spec hangs; flow_gitignore cp1252 read-back; gate_receipt
# POSIX sh shim + literal-backslash filename; normalize_section /
# reveval / task_create_files CLI e2e failures on 8.3 short paths.
# Windows-compat sweep tracked as a follow-up spec - remove entries
# there, never silently here.
EXCLUDES+=(--exclude test_backend_spec.py)
EXCLUDES+=(--exclude test_flow_gitignore.py)
EXCLUDES+=(--exclude test_gate_receipt.py)
EXCLUDES+=(--exclude test_normalize_section_content.py)
EXCLUDES+=(--exclude test_reveval_parse_guard.py)
EXCLUDES+=(--exclude test_task_create_files.py)
fi
python scripts/run_tests_parallel.py "${JOBS_ARGS[@]}" "${SHUFFLE_ARGS[@]}" "${EXCLUDES[@]}"
# ──────────── 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 above; 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:
name: Python ${{ matrix.python-version }} compatibility smoke
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
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, usage, and setup-mode contracts
run: |
cd plugins/flow-next/tests
python -m unittest \
test_startup_bootstrap \
test_init_stamp_launchers \
test_bin_launcher_parity \
test_cmd_usage \
test_setup_mode_stamp \
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:
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"
- name: Git Bash — launcher rejects Python below 3.11 before source load
shell: bash
run: |
set -euo pipefail
export PATH="$PYOLD_DIR:/usr/bin:/bin"
FLOWCTL="$GITHUB_WORKSPACE/plugins/flow-next/scripts/flowctl"
set +e
OUT="$("$FLOWCTL" --help 2>&1)"; RC=$?
set -e
[ "$RC" -ne 0 ]
echo "$OUT" | grep -q "Python 3.11 or newer is required"
! echo "$OUT" | grep -q '^usage:'
- name: PowerShell — batch launcher rejects Python below 3.11 before source load
shell: pwsh
run: |
$PSNativeCommandUseErrorActionPreference = $false
$env:PATH = $env:PYOLD_DIR
$flowctl = Join-Path $env:GITHUB_WORKSPACE "plugins/flow-next/scripts/flowctl.cmd"
# PowerShell's direct .cmd invocation does not reliably preserve the
# batch exit code on every runner. Assert it at the native cmd.exe
# boundary; the preceding step already proves direct pwsh invocation.
$command = "call `"$flowctl`" --help"
$outputLines = & $env:ComSpec /d /c $command 2>&1
$exitCode = $LASTEXITCODE
$out = $outputLines | Out-String
Write-Host $out
if ($exitCode -eq 0) { Write-Error "old Python probe unexpectedly succeeded"; exit 1 }
if ($out -notmatch "Python 3.11 or newer is required") { Write-Error "missing minimum-version error: $out"; exit 1 }
if ($out -match '^usage:') { Write-Error "flowctl.py loaded before minimum-version rejection"; exit 1 }
# The tested native failure is expected; do not let the Actions pwsh
# wrapper propagate its retained LASTEXITCODE after our assertions.
exit 0