diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c911083d..8e908b3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -275,9 +275,86 @@ jobs: $bashScript = $bashScript -replace "`r", "" wsl -d "$distro" -- bash -lc "$bashScript" + installer-powershell: + name: PowerShell installer (windows-x64) + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: npm ci + + - name: Build TypeScript + embed assets + run: npm run build + + # Same build as release.yml's windows-x64 leg, so the acceptance runs the + # real self-contained binary the release ships. + - name: Cross-compile windows-x64 standalone binary + shell: bash + run: | + set -euo pipefail + bun build --compile --target=bun-windows-x64 ./src/cli.ts --outfile jaiph-windows-x64.exe + ls -la jaiph-windows-x64.exe + + - name: Run PowerShell installer acceptance (docs/install.ps1) + shell: pwsh + run: | + $env:JAIPH_TEST_WINDOWS_EXE = Join-Path $env:GITHUB_WORKSPACE "jaiph-windows-x64.exe" + ./e2e/tests/installer_powershell.ps1 + + windows-native-smoke: + name: Native Windows smoke (windows-latest, no WSL) + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: npm ci + + - name: Build TypeScript + embed assets + run: npm run build + + # Same build as release.yml's windows-x64 leg / the installer-powershell + # job, so the smoke test runs the real self-contained binary we ship. + - name: Cross-compile windows-x64 standalone binary + shell: bash + run: | + set -euo pipefail + bun build --compile --target=bun-windows-x64 ./src/cli.ts --outfile jaiph-windows-x64.exe + ls -la jaiph-windows-x64.exe + + # Native run only — no `wsl`. Git for Windows' sh.exe (preinstalled on the + # runner) is the POSIX shell for inline lines; the harness shadows `wsl` so + # any accidental invocation fails the job. + - name: Run native Windows smoke (e2e/tests/windows_native_smoke.ps1) + shell: pwsh + run: | + $env:JAIPH_TEST_WINDOWS_EXE = Join-Path $env:GITHUB_WORKSPACE "jaiph-windows-x64.exe" + ./e2e/tests/windows_native_smoke.ps1 + docker-publish: name: Publish Docker runtime image - needs: [test, e2e, docs-local, e2e-wsl] + needs: [test, e2e, docs-local, e2e-wsl, installer-powershell, windows-native-smoke] if: github.ref == 'refs/heads/nightly' || startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab8d24d6..af3850a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,15 +57,24 @@ jobs: - target: bun-darwin-arm64 os: darwin arch: arm64 + ext: "" - target: bun-darwin-x64 os: darwin arch: x64 + ext: "" - target: bun-linux-x64 os: linux arch: x64 + ext: "" - target: bun-linux-arm64 os: linux arch: arm64 + ext: "" + # Bun has no bun-windows-arm64 target; windows ships x64 only. + - target: bun-windows-x64 + os: windows + arch: x64 + ext: ".exe" steps: - name: Checkout uses: actions/checkout@v4 @@ -88,20 +97,61 @@ jobs: - name: Cross-compile standalone binary for ${{ matrix.target }} run: | set -euo pipefail - bun build --compile --target=${{ matrix.target }} ./src/cli.ts --outfile "jaiph-${{ matrix.os }}-${{ matrix.arch }}" - ls -la "jaiph-${{ matrix.os }}-${{ matrix.arch }}" + out="jaiph-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.ext }}" + bun build --compile --target=${{ matrix.target }} ./src/cli.ts --outfile "${out}" + ls -la "${out}" - name: Upload binary artifact uses: actions/upload-artifact@v4 with: - name: jaiph-${{ matrix.os }}-${{ matrix.arch }} - path: jaiph-${{ matrix.os }}-${{ matrix.arch }} + name: jaiph-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.ext }} + path: jaiph-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.ext }} if-no-files-found: error retention-days: 7 + sanity-windows: + name: Sanity gate (windows-x64 --version) + needs: build + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve tag and channel + id: meta + shell: bash + run: | + set -euo pipefail + case "${GITHUB_REF}" in + refs/tags/v*) + tag="${GITHUB_REF_NAME}"; channel="stable" ;; + refs/heads/nightly|refs/tags/nightly) + tag="nightly"; channel="nightly" ;; + *) + echo "Unsupported ref for release: ${GITHUB_REF}" >&2; exit 1 ;; + esac + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "channel=${channel}" >> "${GITHUB_OUTPUT}" + + - name: Download windows binary artifact + uses: actions/download-artifact@v4 + with: + name: jaiph-windows-x64.exe + path: release-assets + + - name: Sanity gate (windows-x64 --version) + working-directory: release-assets + shell: bash + run: | + set -euo pipefail + got="$(./jaiph-windows-x64.exe --version)" + echo "got: ${got}" + bash ../scripts/release-version-check.sh \ + "${{ steps.meta.outputs.channel }}" "${{ steps.meta.outputs.tag }}" "${got}" + release: name: Publish release assets - needs: build + needs: [build, sanity-windows] runs-on: ubuntu-latest permissions: contents: write @@ -138,7 +188,7 @@ jobs: set -euo pipefail ls -la rm -f SHA256SUMS - sha256sum jaiph-darwin-arm64 jaiph-darwin-x64 jaiph-linux-x64 jaiph-linux-arm64 > SHA256SUMS + sha256sum jaiph-darwin-arm64 jaiph-darwin-x64 jaiph-linux-x64 jaiph-linux-arm64 jaiph-windows-x64.exe > SHA256SUMS cat SHA256SUMS - name: Sanity gate (linux-x64 --version) @@ -148,19 +198,8 @@ jobs: chmod +x jaiph-linux-x64 got="$(./jaiph-linux-x64 --version)" echo "got: ${got}" - if [ "${{ steps.meta.outputs.channel }}" = "stable" ]; then - tag="${{ steps.meta.outputs.tag }}" - expected="jaiph ${tag#v}" - if [ "${got}" != "${expected}" ]; then - echo "Version sanity check failed: expected '${expected}', got '${got}'" >&2 - exit 1 - fi - else - if ! printf '%s\n' "${got}" | grep -Eq '^jaiph [0-9]+\.[0-9]+\.[0-9]+'; then - echo "Version sanity check failed: '${got}' does not look like a jaiph version" >&2 - exit 1 - fi - fi + bash ../scripts/release-version-check.sh \ + "${{ steps.meta.outputs.channel }}" "${{ steps.meta.outputs.tag }}" "${got}" - name: Publish stable release ${{ steps.meta.outputs.tag }} if: steps.meta.outputs.channel == 'stable' @@ -172,13 +211,15 @@ jobs: gh release upload "${tag}" --clobber \ jaiph-darwin-arm64 jaiph-darwin-x64 \ jaiph-linux-x64 jaiph-linux-arm64 \ + jaiph-windows-x64.exe \ SHA256SUMS else gh release create "${tag}" \ --title "${tag}" \ - --notes "Jaiph ${tag} — standalone binaries (darwin/linux × arm64/x64) plus SHA256SUMS." \ + --notes "Jaiph ${tag} — standalone binaries (darwin/linux × arm64/x64, windows x64) plus SHA256SUMS." \ jaiph-darwin-arm64 jaiph-darwin-x64 \ jaiph-linux-x64 jaiph-linux-arm64 \ + jaiph-windows-x64.exe \ SHA256SUMS fi @@ -191,6 +232,7 @@ jobs: gh release upload nightly --clobber \ jaiph-darwin-arm64 jaiph-darwin-x64 \ jaiph-linux-x64 jaiph-linux-arm64 \ + jaiph-windows-x64.exe \ SHA256SUMS else gh release create nightly \ @@ -200,5 +242,6 @@ jobs: --target "${GITHUB_SHA}" \ jaiph-darwin-arm64 jaiph-darwin-x64 \ jaiph-linux-x64 jaiph-linux-arm64 \ + jaiph-windows-x64.exe \ SHA256SUMS fi diff --git a/.jaiph/prepare_release.jh b/.jaiph/prepare_release.jh index a110429c..a57c7089 100755 --- a/.jaiph/prepare_release.jh +++ b/.jaiph/prepare_release.jh @@ -58,18 +58,20 @@ script npm_version_no_tag = `npm version "$1" --no-git-tag-version --allow-same- script update_install_release_ref = ```python3 import sys old, new = sys.argv[1], sys.argv[2] -path = "docs/install" -with open(path, "r", encoding="utf-8") as f: - src = f.read() needle = f"v{old}" -count = src.count(needle) -if count == 0: - sys.stderr.write(f"docs/install: hardcoded ref v{old} not found\n") - sys.exit(1) -new_src = src.replace(needle, f"v{new}") -with open(path, "w", encoding="utf-8") as f: - f.write(new_src) -print(count) +total = 0 +# Both installers pin the same hardcoded ref; keep them in lockstep. +for path in ("docs/install", "docs/install.ps1"): + with open(path, "r", encoding="utf-8") as f: + src = f.read() + count = src.count(needle) + if count == 0: + sys.stderr.write(f"{path}: hardcoded ref v{old} not found\n") + sys.exit(1) + with open(path, "w", encoding="utf-8") as f: + f.write(src.replace(needle, f"v{new}")) + total += count +print(total) ``` script run_npm_build = `npm run build >&2` @@ -124,7 +126,7 @@ workflow default(arg) { log """ prepare_release: staged release v${version} - package.json + package-lock.json (npm version ${version}) - - docs/install (release ref v${old_version} -> v${version}) + - docs/install + docs/install.ps1 (release ref v${old_version} -> v${version}) - docs/registry (regenerated) - dist/ (rebuilt; jaiph --version == jaiph ${version}) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c09f9e5..f06d178c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Unreleased +## All changes + +- **Feature — Distro: native Windows smoke job in CI:** CI's only Windows coverage was `e2e-wsl`, which runs the suite inside WSL against the *Linux* binary. A new **`windows-native-smoke`** job (`.github/workflows/ci.yml`, `windows-latest`) proves the *native* `jaiph.exe` runs. It cross-compiles the standalone binary from the checkout (`bun build --compile --target=bun-windows-x64`, the same build as the release / `installer-powershell` legs) and runs **`e2e/tests/windows_native_smoke.ps1`** against it. The harness (a PowerShell acceptance test, same convention as `installer_powershell.ps1`, pointed at the binary via `JAIPH_TEST_WINDOWS_EXE`) covers three contracts, each failing when violated: (1) a **sample workflow** runs host-only (`JAIPH_UNSAFE=true`) exercising an inline shell line (through Git for Windows' `sh.exe`), a `script` step with a non-bash lang tag (` ```node `), `${…}` string interpolation, and `log` output — with assertions against the real `jaiph.exe` **stdout** (exit code `0` plus the interpolated `log` line and the `PASS` footer); (2) a **mid-run cancellation** records the workflow leader's descendant PID tree (`Win32_Process` parent/child links), delivers a real **Ctrl-C** isolated to the leader's own console (`AttachConsole` + `GenerateConsoleCtrlEvent`, so the CI runner shell is untouched), and fails if **any** descendant survives — exercising the win32 `taskkill /T /F` teardown path in `killProcessTree`; (3) a **`prompt`-step credential pre-flight** (`agent.backend = "codex"`, `OPENAI_API_KEY` unset, and `JAIPH_UNSAFE` dropped so the pre-flight runs — win32 forces host-only regardless) fails fast with the documented `E_AGENT_CREDENTIALS` error, bounded by a 30s `WaitForExit` so a hang is a failure, rather than calling any backend. The job never touches WSL — it shadows `wsl` so an accidental call throws — and now gates merge alongside `test`/`e2e`/`e2e-wsl` (added to `docker-publish`'s `needs`); `e2e-wsl` is left in place. Host-portable guards in `integration/windows-native-smoke.test.ts` pin the CI job shape and the harness contract (build command, stdout assertions, cancellation orphan check, pre-flight error, no-WSL, gate membership) so a regression fails `npm test` on any platform. +- **Feature — Distro: main-page install tabs — Windows variant with platform auto-detect:** The landing page (`docs/index.html`) hero install card now offers a Windows PowerShell path alongside the POSIX `curl … | bash` one-liners, and defaults to the right one for the visitor's platform. A new **`.os-switch`** sub-toggle (two buttons, `data-os="posix"` "macOS / Linux" and `data-os="windows"` "Windows", `role="group"`) sits above the existing run-sample / init-project / just-install tabs, and every tab panel now wraps its content in two **`.os-variant`** blocks (`data-os="posix"` / `data-os="windows"`). On load, **`attachOsSwitch()`** (`docs/assets/js/main.js`, scoped to `section.try-it-out .card`) auto-selects the Windows variant for Windows visitors via **`isWindowsPlatform()`** — which prefers `navigator.userAgentData?.platform` and falls back to `navigator.platform`, matching `/win/i` — while macOS/Linux visitors keep exactly today's POSIX default with no layout shift; **`setOsVariant()`** flips the `is-active` class on both the switch buttons and the variants across all three panels, so a manual platform choice (or a tab switch) is remembered card-wide. The **"Just install"** Windows variant is the copy-able `irm https://jaiph.org/install.ps1 | iex` line (via the existing copy button) plus the `npm install -g jaiph` alternative. The **"Run sample"** and **"Init project"** tabs have no PowerShell equivalent for their `curl … | bash -s` / `curl … | bash` pipes, so instead of showing a bash-only command as the Windows default they show the install one-liner followed by a short "then run:" `jaiph run say_hello.jh Adam` / `jaiph init` step (installing to `%LOCALAPPDATA%\jaiph\bin`). The static-render constraint holds: the POSIX variant carries `is-active` in the shipped markup and CSS keeps `.os-variant { display: none }` / `.os-variant.is-active { display: block }` (`docs/assets/css/style.css`), so with JS disabled all three bash one-liners render, no panel is blank, and the Windows variant stays reachable in the tab markup — JS only reveals it on demand (and by default for Windows). New Playwright cases in the docs suite (`e2e/playwright/landing-page.spec.ts`, alongside the "Try it out" test) emulate the platform before page scripts run and assert: a Windows visitor defaults to the `irm … | iex` command, a macOS/Linux visitor's default is unchanged from today, manual platform + tab switching works in both directions, the copy button on the Windows variant copies the exact `irm … | iex` line (asserted through a clipboard stub), and with JavaScript disabled all bash commands render with no blank panel. No `docs/*.md` or `README.md` change is needed — the Windows install path is already documented in `docs/setup.md` (`/how-to/install`) and the README install section; this task is landing-page presentation only, with no runtime, CLI, or installer behavior change. +- **Feature — Distro: PowerShell installer at `jaiph.org/install.ps1`:** Windows now has a native install path to match the POSIX `curl … | bash` one. A new **`docs/install.ps1`** (served as `https://jaiph.org/install.ps1`, run with `irm https://jaiph.org/install.ps1 | iex`) is the counterpart to `docs/install`: it downloads **`jaiph-windows-x64.exe`** and `SHA256SUMS` from the pinned release ref (default the current stable tag `v0.10.0`, overridable via `JAIPH_REPO_REF` or the first argument — mirroring the bash installer — plus `JAIPH_RELEASE_BASE_URL` for a local/`file://` release dir used by the tests), verifies the SHA-256 with **`Get-FileHash`** against `SHA256SUMS` and aborts installing nothing on mismatch, then installs to **`%LOCALAPPDATA%\jaiph\bin\jaiph.exe`** (overridable via `JAIPH_BIN_DIR`), adds that directory to the user `PATH` if absent, and prints the same `jaiph --version` / `jaiph --help` try-it hints as the bash installer. Non-x64 / ARM Windows exits non-zero with a documented unsupported-platform message pointing at the from-source instructions (Bun has no Windows arm64 target, so Windows ships x64 only). The bash installer (`docs/install`) now rejects Windows-like `uname` values (`MINGW*`/`MSYS*`/`CYGWIN*`/`Windows_NT`) and points at the PowerShell one-liner instead of the generic build-from-source message; the release-prep workflow (`.jaiph/prepare_release.jh`) rewrites the pinned `vX.Y.Z` ref in **both** `docs/install` and `docs/install.ps1` in lockstep so they can never drift. CI gains an **`installer-powershell`** job on `windows-latest` that cross-compiles the real `jaiph-windows-x64.exe` (same build as the release leg) and runs `e2e/tests/installer_powershell.ps1` against `docs/install.ps1` — checksum mismatch (non-zero, no binary left), unsupported arch (documented message), and a happy-path install where `jaiph --version` works with no Node/npm/Bun on `PATH`; the Docker publish job now `needs` it. Host-portable guards live in `integration/installer-powershell.test.ts` (installer contract, bash↔PowerShell lockstep ref, docs parity). Docs updated (`docs/setup.md`, `docs/index.html`, `docs/architecture.md`, `docs/contributing.md`). +- **Feature — Distro: build and release `jaiph-windows-x64.exe`:** The release workflow (`.github/workflows/release.yml`) now cross-compiles and publishes a Windows binary alongside the existing darwin/linux × arm64/x64 set. A new matrix entry `bun-windows-x64` (`os: windows`, `arch: x64`) produces the asset **`jaiph-windows-x64.exe`**; Bun has no `bun-windows-arm64` target, so Windows ships x64 only. The matrix gained an `ext` field (`""` for POSIX, `".exe"` for Windows) so the compiled `--outfile` and the uploaded artifact carry the extension. `SHA256SUMS` generation now covers all **five** binaries including the `.exe`, and both the stable (`v*`) and rolling `nightly` `gh release` upload lists ship the `.exe` — so every release publishes **six** assets (five binaries + `SHA256SUMS`). A new **`sanity-windows`** job runs on `windows-latest`, downloads the `.exe` artifact, and runs `jaiph-windows-x64.exe --version` through the version gate; the publish job now `needs: [build, sanity-windows]`, so a Windows version mismatch fails the whole release. The linux-x64 gate's inline version comparison was extracted into a shared, unit-testable **`scripts/release-version-check.sh`** (` `: stable output must equal `jaiph ` exactly; any other channel only has to look like a `jaiph ` banner), and both the linux-x64 and windows-x64 gates delegate to it so the comparison lives in one place. The bash installer (`docs/install`) and its e2e test (`e2e/tests/07_installer_binary.sh`) are unchanged — they resolve darwin/linux names from `{os}×{arch}` and cannot install a Windows `.exe` via `curl … | bash`; Windows users download the `.exe` from the Release directly. New acceptance tests (`integration/release-workflow.test.ts`) assert the five-binary matrix (and the absence of a windows-arm64 target), that `SHA256SUMS` and both upload lists include the `.exe`, that a `.exe` checksum entry round-trips through the installer's own `awk` lookup, that the shared gate fails on a stable tag/version mismatch and passes on a match, that the `windows-latest` job runs `--version` through the shared script and blocks publish, and that the naming contract ↔ release matrix ↔ installer asset names stay in parity. Docs updated (`docs/architecture.md`, `docs/contributing.md`). +- **Fix — Portability: home-dir fallback, Docker gating on Windows, and a single ANSI policy:** Three remaining POSIX assumptions that broke a host-only `win32` runtime are closed. (1) **Home directory** — `prepareClaudeEnv` (`src/runtime/kernel/prompt.ts`) resolved the `.claude` config dir from `execEnv.HOME || process.env.HOME`, which is empty in a `USERPROFILE`-only Windows environment. It now falls back to `os.homedir()` as a final source (`execEnv.HOME || process.env.HOME || homedir()`); an explicit `HOME` in `execEnv` still wins. (2) **Docker gating** — the Docker sandbox hardcodes POSIX socket paths and Linux/macOS workspace-presentation branches, so it is out of scope on Windows. `resolveDockerConfig` (`src/runtime/docker.ts`) now forces **host-only mode** on `win32` (same UX as an explicit `JAIPH_UNSAFE=true`) with a one-line notice emitted once per process, so the CLI never probes `docker` and never hard-fails on a missing daemon; `JAIPH_DOCKER_ENABLED=true` cannot override this. (3) **ANSI colors** — a new **`canUseAnsi(stream?)`** helper in the portability module (`src/runtime/kernel/portability.ts`) returns `isTTY && NO_COLOR`-unset, and every color/erase emission site (`src/cli/commands/run.ts`, `src/cli/run/progress.ts`, `src/cli/shared/errors.ts`) routes its gate through it so the policy lives in one place; no rendering change is needed because Node enables console VT processing on Windows 10+ automatically, making `isTTY` a sufficient ANSI proxy. New unit tests: `prepareClaudeEnv` resolves from a stubbed `os.homedir()` when `HOME` is unset and prefers `execEnv.HOME` when set (`src/runtime/kernel/prompt.test.ts`); `resolveDockerConfig` under a stubbed `win32` returns host-only, emits the notice once, and performs zero `docker` invocations (`src/runtime/docker.test.ts`); `canUseAnsi()` is false when `isTTY` is false or `NO_COLOR` is set, plus a `src/`-wide lint test asserting no production file outside `portability.ts` gates directly on `isTTY && NO_COLOR` (`src/runtime/kernel/portability.test.ts`). Docs updated (`docs/architecture.md`, `docs/sandboxing.md`, `docs/configuration.md`, `docs/env-vars.md`). +- **Fix — Portability: single `resolveShell()` seam for inline shell lines and hooks:** Inline workflow shell lines (`executeShLine` in `src/runtime/kernel/node-workflow-runtime.ts`) and hook commands (`src/cli/run/hooks.ts`) used to run via a hardcoded `spawn("sh", ["-c", …])`. That works on POSIX but fails on `win32`, where there is no `sh` on the default `PATH`. A new **`resolveShell(): string`** in the portability module (`src/runtime/kernel/portability.ts`) is now the single seam both call sites go through. On POSIX it returns bare `sh`. On `win32` it locates Git for Windows' bundled `sh.exe` — first on `PATH`, then in the standard install layouts (`/bin/sh.exe`, `/usr/bin/sh.exe`) under each known root (`%ProgramFiles%`, `%ProgramFiles(x86)%`, `%ProgramW6432%`, plus the default `C:\Program Files` / `C:\Program Files (x86)`) — and throws a diagnosable Jaiph error with the stable code **`E_NO_POSIX_SHELL`** naming Git for Windows (https://git-scm.com/download/win) as the fix if none is found. Resolution is memoized for the lifetime of the process. Crucially, inline lines are **never** translated to `cmd`/PowerShell — Jaiph's language semantics require POSIX `sh` on every platform, or workflows stop being portable — so the seam only ever resolves *which* `sh` to run, never rewrites the command. POSIX inline-shell semantics are unchanged (existing e2e passes). New unit tests (`src/runtime/kernel/portability.test.ts`) stub `process.platform` and the `PATH` / existence lookup: POSIX returns `sh`, `win32` returns a discovered `sh.exe` path (both the on-`PATH` and Git-for-Windows-fallback branches), `win32` with no shell available throws `E_NO_POSIX_SHELL` with a message naming Git for Windows, and the result is memoized (the existence probe is not consulted on a second call); a `src/`-wide lint test asserts no production file outside `portability.ts` calls `spawn("sh"` / `spawn('sh'` directly. Docs updated (`docs/architecture.md`). +- **Fix — Portability: execute script steps via an explicit interpreter, not shebang + exec bit:** Script steps used to run by spawning the emitted file directly (`executeScript` in `src/runtime/kernel/node-workflow-runtime.ts`), relying on the `#!/usr/bin/env ` shebang line and the `0o755` bit set by `src/transpile/build.ts`. Windows honors neither, and `noexec` mounts on Linux strip the exec bit — both break script execution. The runtime already knows the interpreter (it writes the shebang itself), so `executeScript` now reads the emitted script's shebang and resolves the interpreter through the new **`resolveInterpreterFromShebang`** (`src/parse/script-bash.ts`) — `#!/usr/bin/env ` → spawn ``, an absolute-path shebang (`#!/bin/bash`) → spawn that path, an `env -S` split-flag form → the real interpreter after `-S`, a missing/blank shebang → default `bash` — then spawns ` ` explicitly. The shebang line is **still** written into every emitted script (they stay directly executable by hand on POSIX) and the `0o755` bit is still set, but the runtime no longer depends on either being honored by the OS. A spawn `ENOENT` from an interpreter missing on `PATH` now surfaces as a diagnosable Jaiph error naming the interpreter (`script interpreter "" not found — install it or fix the script shebang`) instead of a raw `spawn ENOENT`. New unit tests assert the resolved interpreter + argv on the spawn call itself through an injectable `_scriptSpawn` seam (`src/runtime/kernel/node-workflow-runtime.script-exec.test.ts`), prove that a script with the exec bit stripped (`0o644`) still runs on POSIX, and prove that a missing-interpreter shebang produces the diagnosable error; `resolveInterpreterFromShebang` gets its own unit coverage (`src/parse/script-bash.test.ts`). Docs updated (`docs/architecture.md`, `docs/setup.md`). +- **Fix — Portability: cross-platform process-tree termination:** `jaiph run` spawns the workflow leader with `detached: true` (`src/runtime/kernel/workflow-launch.ts`) and previously stopped it via `process.kill(-pid, signal)` — a negative-PID group kill, which signals the leader's whole process group. That primitive is POSIX-only: on `win32` it throws, the previous `child.kill()` fallback terminated only the leader, and the agent backends / script children it spawned were **orphaned**. A new portability module `src/runtime/kernel/portability.ts` exports **`killProcessTree(pid, signal)`** and is now the single sanctioned home for group kills. On POSIX it preserves the prior behavior — `process.kill(-pid, signal)` with a per-process fallback when the group no longer exists (`ESRCH`). On `win32` it force-kills the entire tree with `taskkill /pid /T /F` (spawned via an injectable `_portability.spawn` seam, not shelled), degrading to a per-process `process.kill(pid, signal)` if `taskkill` cannot be launched (reported asynchronously via the child's `error` event). Because `taskkill /F` is already a forceful kill with no graceful phase, a follow-up `SIGKILL` escalation after a `SIGTERM`/`SIGINT` is a **documented no-op** on `win32` — the tree is already gone. Every subprocess-termination call site is repointed at the helper: run teardown `terminateRunProcessGroup` (`src/cli/run/lifecycle.ts`, previously the negative-PID group kill) and the two SIGTERM→SIGKILL escalation paths that previously assumed POSIX signal semantics via `child.kill()` — the prompt watchdog's `killChild` (`src/runtime/kernel/prompt.ts`) and the Docker run-timeout kill (`src/runtime/docker.ts`) — so those escalations now terminate the whole child tree instead of just the leader. POSIX Ctrl-C behavior of `jaiph run` is unchanged (leader and children terminate; exit code and cleanup semantics identical), covered by the existing signal-lifecycle e2e. New unit tests (`src/runtime/kernel/portability.test.ts`) cover both platform branches by stubbing `process.platform` (precedent: `src/runtime/docker.test.ts`) and assert the exact mechanism invoked — negative-PID kill on POSIX (plus the ESRCH per-process fallback and SIGKILL escalation), `taskkill /pid /T /F` argv on `win32` (SIGTERM, SIGINT/Ctrl-C, and the degrade-to-per-process path), a proof that the win32 branch **never** calls `process.kill` with a negative PID, and a `src/`-wide lint test asserting no production file outside `portability.ts` matches `process.kill(-`. The watchdog unit tests in `src/runtime/kernel/prompt.test.ts` are updated to spy on `process.kill` (the watchdog now terminates through `killProcessTree`). Docs updated (`docs/architecture.md`, `docs/configuration.md`). + # 0.10.0 ## Summary diff --git a/QUEUE.md b/QUEUE.md index 64f890c3..72c078fc 100644 --- a/QUEUE.md +++ b/QUEUE.md @@ -13,3 +13,4 @@ Process rules: 7. **Acceptance criteria are non-negotiable.** A task is not done until every acceptance bullet is verified by a test that fails when the contract is violated. "It works on my machine" or "the existing tests pass" is not acceptance. *** + diff --git a/README.md b/README.md index 82fb3d97..bd6dbbe6 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ Requires `node` and `curl`. The script installs Jaiph automatically if needed. curl -fsSL https://jaiph.org/install | bash ``` +On Windows, install with PowerShell instead (installs `jaiph-windows-x64.exe` to `%LOCALAPPDATA%\jaiph\bin`): + +```powershell +irm https://jaiph.org/install.ps1 | iex +``` + Or install from npm: ```bash diff --git a/docs/architecture.md b/docs/architecture.md index 6dc87d33..e9bd19e9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -72,6 +72,8 @@ All orchestration — local `jaiph run`, `jaiph test`, and **Docker `jaiph run`* - **Node Workflow Runtime (`src/runtime/kernel/node-workflow-runtime.ts`)** - `NodeWorkflowRuntime` interprets the AST directly: walks workflow steps, manages scope/variables, delegates prompt and script execution to kernel helpers, handles channels/inbox/dispatch, owns the frame stack and heartbeat, and writes run artifacts. + - **Script steps execute via an explicit interpreter, not the shebang + exec bit.** `executeScript` reads the emitted script's shebang line, resolves the interpreter through **`resolveInterpreterFromShebang`** (`src/parse/script-bash.ts`) — `#!/usr/bin/env ` → spawn ``, an absolute-path shebang → spawn that path, a missing shebang → default `bash` — and spawns ` `. This is portable: it does not depend on the OS honoring the shebang (Windows honors neither shebang nor exec bit) or on the file's `0o755` bit (`noexec` mounts strip it). The shebang line is **still** written into every emitted script (they stay directly executable by hand on POSIX), but the runtime never relies on it being honored. A spawn `ENOENT` from a missing interpreter surfaces as a diagnosable Jaiph error naming the interpreter rather than a raw `ENOENT`. + - **Inline shell lines resolve their shell through one portable seam.** A single-line shell step (`executeShLine`) and CLI hook commands (`src/cli/run/hooks.ts`) both run under POSIX `sh -c`, but the shell itself is resolved through **`resolveShell()`** (`src/runtime/kernel/portability.ts`) rather than a hardcoded `spawn("sh", …)`. On POSIX this is bare `sh`; on **`win32`**, where there is no `sh` on the default `PATH`, it discovers Git for Windows' bundled `sh.exe` — first on `PATH`, then in the standard install layouts (`/bin/sh.exe`, `/usr/bin/sh.exe`) under each known root — memoizes the result for the process, and throws a diagnosable **`E_NO_POSIX_SHELL`** error naming Git for Windows if none is found. Inline lines are **never** translated to `cmd`/PowerShell: Jaiph's shell semantics are POSIX `sh` on every platform, so the seam only ever chooses *which* `sh` to invoke, never rewrites the command — otherwise workflows would stop being portable. `resolveShell()` is the single call site for the POSIX shell; no other `spawn("sh", …)` remains in `src/`. - One private `evaluateExpr(scope, expr, …)` dispatcher handles every value position — `const` / `return` / `send` / `say` step handlers and the body of every `exec` step delegate to it. It switches on `Expr.kind` to run the managed call (`call` / `ensure_call` / `inline_script`) or `prompt`, walks a `match` expression, or interpolates a `literal` value through `interpolateWithCaptures`. There is no fan-out across "managed sidecar vs literal value" because that branch is gone from the AST. - **Prompt transport-failure retry.** `runPromptStep` wraps each `executePrompt` invocation in a retry loop driven by the schedule resolved through `src/runtime/kernel/prompt-retry.ts` (default `15s → 1m → 10m → 30m → 2h`, six total attempts; configurable via `JAIPH_PROMPT_RETRY` / `JAIPH_PROMPT_RETRY_DELAYS`). Only the transport path (non-zero exit from the backend) is retried; invalid JSON and schema-validation failures return `{ ok: false }` on the first attempt. Each attempt emits its own `PROMPT_START` / `PROMPT_END` and `STEP_START` / `STEP_END`; each failure (and the final termination) logs a `LOGERR` through `RuntimeEventEmitter.emitLog`. The backoff sleep is injectable (`sleep` constructor option) and interruptible via `runtime.abort()` / an internal `AbortController` so SIGINT and in-process aborts halt the loop without further backend calls. Retry composes **below** `recover` / `catch` — backoff is exhausted before the failure reaches the recover loop. See [Configuration — Prompt retry on transport failure](configuration.md#prompt-retry-on-transport-failure). - Three sibling modules under `src/runtime/kernel/` carry concerns that used to live inline in the runtime file. Dependency direction is one-way (orchestrator → helpers/emitter/mock); no circular imports back. @@ -90,7 +92,7 @@ All orchestration — local `jaiph run`, `jaiph test`, and **Docker `jaiph run`* - `jaiph format` rewrites `.jh` / `.test.jh` files into canonical style. `emitModule(ast, trivia, opts?)` reads the semantic AST together with the parallel **`Trivia`** store ([Trivia (CST layer)](#trivia-cst-layer)) to round-trip leading comments, top-level order, `config` body sequence, `"""..."""` and `bareSource` forms, the original quotedness of top-level `const` values (`EnvDeclDef.wasQuoted` — `true` for `"…"` / `"""…"""` sources, `undefined` for bare tokens — so a quoted value is never silently rewritten as bare based on whether it contains a space), and prompt / script body discriminators. Step emission switches on `WorkflowStepDef.type` (8 variants) and an `emitExpr` helper switches on `Expr.kind` (8 kinds) — there are no dual code paths for "managed sidecar vs literal value" because that branch was removed from the AST. Call arguments render straight off the typed `Arg[]` — `var` → bare name, `literal` → raw — so the formatter no longer re-parses any args string or consults a `bareIdentifierArgs` shadow field. Pure data→text emitter; no side-effects beyond file writes. Round-trip is bit-for-bit on every fixture under `examples/` and `test-fixtures/golden-ast/fixtures/` — pinned by `src/format/roundtrip.test.ts`, which asserts `parse → format → parse → format` converges in one step on every fixture. - **Docker runtime helper (`src/runtime/docker.ts`)** - - Parses mount specs, resolves Docker config (image, network, timeout), and builds the `docker run` invocation when the CLI enables **Docker sandboxing** for `jaiph run` (environment-driven; there is no `jaiph run --docker` flag — see [Sandboxing](sandboxing.md)). The container runs the same **`jaiph run --raw`** / **`__workflow-runner`** entry as local execution. The default image is the official `ghcr.io/jaiphlang/jaiph-runtime` GHCR image; every selected image must already contain `jaiph` (no auto-install or derived-image build at runtime). Image preparation (`prepareImage`) runs before the CLI banner: it checks whether the image is local, pulls with `--quiet` if needed (short status lines on stderr instead of Docker’s default pull UI), and verifies that `jaiph` exists in the image. `spawnDockerProcess` does not pull or verify — it receives a pre-resolved image. The spawn call uses `stdio: ["ignore", "pipe", "pipe"]` — stdin is ignored so the Docker CLI does not block on stdin EOF, which would stall event streaming and hang the host CLI after the container exits. + - Parses mount specs, resolves Docker config (image, network, timeout), and builds the `docker run` invocation when the CLI enables **Docker sandboxing** for `jaiph run` (environment-driven; there is no `jaiph run --docker` flag — see [Sandboxing](sandboxing.md)). On **`win32`** the Docker sandbox is out of scope: **`resolveDockerConfig`** forces host-only mode (same UX as an explicit **`JAIPH_UNSAFE=true`**) with a one-line notice, so the CLI never probes `docker` and never hard-fails on a missing daemon (`JAIPH_DOCKER_ENABLED=true` cannot override this). The container runs the same **`jaiph run --raw`** / **`__workflow-runner`** entry as local execution. The default image is the official `ghcr.io/jaiphlang/jaiph-runtime` GHCR image; every selected image must already contain `jaiph` (no auto-install or derived-image build at runtime). Image preparation (`prepareImage`) runs before the CLI banner: it checks whether the image is local, pulls with `--quiet` if needed (short status lines on stderr instead of Docker’s default pull UI), and verifies that `jaiph` exists in the image. `spawnDockerProcess` does not pull or verify — it receives a pre-resolved image. The spawn call uses `stdio: ["ignore", "pipe", "pipe"]` — stdin is ignored so the Docker CLI does not block on stdin EOF, which would stall event streaming and hang the host CLI after the container exits. - **Workspace immutability:** By default Docker runs cannot modify the host workspace. The host checkout is mounted read-only (overlay) or as a disposable clone (copy); `/jaiph/workspace` is sandbox-local and discarded on exit. The only host-writable path is `/jaiph/run` (run artifacts). Workflows that need to capture workspace changes should write files (for example a `git diff` into a temp path) and publish them with `artifacts.save()`. The explicit opt-in **inplace** mode (truthy **`JAIPH_INPLACE`** — `1` or `true`, or `jaiph run --inplace`) breaks this contract on purpose — the host workspace itself is bind-mounted read-write so the run's edits persist live on the host, with the rest of the sandbox (caps, env allowlist, mount set) unchanged. See [Sandboxing](sandboxing.md) for the full contract and [Save artifacts](artifacts.md). ## Local module graph @@ -123,7 +125,7 @@ User-visible contracts (banner, hooks, run artifacts, `run_summary.jsonl`, `retu ### CLI responsibilities - Parse, validate, and launch workflows/tests. -- Own **process spawn** for `jaiph run` (detached workflow runner process group for signal propagation). +- Own **process spawn** for `jaiph run` (detached workflow runner process group for signal propagation). Terminating a run means terminating the whole tree — the detached leader plus the agent backends and script children it spawned — routed through **`killProcessTree(pid, signal)`** (`src/runtime/kernel/portability.ts`), the single sanctioned home for group kills. On POSIX it signals the leader's process group with **`process.kill(-pid, signal)`**, falling back to a per-process kill if the group no longer exists (`ESRCH`). On **`win32`** a negative-PID group kill throws and a per-process kill would orphan the children, so it force-kills the tree with **`taskkill /pid /T /F`** (spawned, not shelled), degrading to a per-process kill if `taskkill` cannot be launched. Because `taskkill /F` is already forceful, a follow-up `SIGKILL` escalation after a `SIGTERM`/`SIGINT` is a **documented no-op** on `win32`. All group-kill call sites route through this helper: run teardown (`src/cli/run/lifecycle.ts`), the prompt watchdog (`src/runtime/kernel/prompt.ts`), and the Docker run-timeout kill (`src/runtime/docker.ts`). - Parse live runtime events; render terminal progress; trigger hooks — skipped in **`jaiph run --raw`** (child stdio inherited; see [CLI](cli.md#jaiph-run)). ## Contracts @@ -168,13 +170,13 @@ Authoring rules, fixtures, and mock syntax for `*.test.jh` are documented in [Te ## CLI progress reporting pipeline -The progress UI combines a **static** step tree derived from the workflow AST (`src/cli/run/progress.ts`) with **live** updates from the runtime event stream. Event wiring: `src/cli/run/events.ts` and `src/cli/run/stderr-handler.ts` parse `__JAIPH_EVENT__` lines; `src/cli/run/emitter.ts` bridges into the renderer. Line-oriented formatting (`formatStartLine`, `formatHeartbeatLine`, `formatCompletedLine`) lives primarily in `src/cli/run/display.ts`, which shares some display helpers with `progress.ts`. Async branch numbering (subscript ₁₂₃… prefixes) is driven by `async_indices` on step and log events — the runtime propagates a chain of 1-based branch indices through `AsyncLocalStorage`, and the stderr handler renders them at the appropriate indent level. `const` steps whose `Expr` value is `kind: "match"` are walked for nested `run` / `ensure` arms; matched targets appear as child items in the step tree (for example `▸ workflow my_flow` or `▸ rule my_rule` under the `const` row). This pipeline does not apply to **`jaiph run --raw`**. +The progress UI combines a **static** step tree derived from the workflow AST (`src/cli/run/progress.ts`) with **live** updates from the runtime event stream. Event wiring: `src/cli/run/events.ts` and `src/cli/run/stderr-handler.ts` parse `__JAIPH_EVENT__` lines; `src/cli/run/emitter.ts` bridges into the renderer. Line-oriented formatting (`formatStartLine`, `formatHeartbeatLine`, `formatCompletedLine`) lives primarily in `src/cli/run/display.ts`, which shares some display helpers with `progress.ts`. Async branch numbering (subscript ₁₂₃… prefixes) is driven by `async_indices` on step and log events — the runtime propagates a chain of 1-based branch indices through `AsyncLocalStorage`, and the stderr handler renders them at the appropriate indent level. Whether ANSI SGR colors are emitted is a single policy — **`canUseAnsi()`** (`src/runtime/kernel/portability.ts`) returns `isTTY && NO_COLOR` unset — and every color emission site (`src/cli/commands/run.ts`, `src/cli/run/progress.ts`, `src/cli/shared/errors.ts`) routes its gate through it rather than re-deriving `isTTY && NO_COLOR` locally. On Windows 10+ Node enables console VT processing automatically, so `isTTY` is a sufficient ANSI proxy with no extra win32 branch. `const` steps whose `Expr` value is `kind: "match"` are walked for nested `run` / `ensure` arms; matched targets appear as child items in the step tree (for example `▸ workflow my_flow` or `▸ rule my_rule` under the `const` row). This pipeline does not apply to **`jaiph run --raw`**. ## Distribution: Node vs Bun standalone - **Development / npm:** `npm run build` runs `npm run embed-assets` (regenerates **`src/runtime/embedded-assets.ts`** from `runtime/overlay-run.sh` and `docs/jaiph-skill.md`, and **`src/version.ts`** from `package.json`'s `version` field), then `tsc`, copies **`src/runtime/`** to **`dist/src/runtime/`** (kernel, `docker.ts`, etc.), and copies **`runtime/overlay-run.sh`** from the repo root into **`dist/src/runtime/overlay-run.sh`**. The published `jaiph` bin is **`node dist/src/cli.js`**. - **Standalone:** `npm run build:standalone` runs the same build, copies **`dist/src/runtime`** to **`dist/runtime`** beside the binary, then `bun build --compile ./src/cli.ts --outfile dist/jaiph`. Workflow launch self-spawns via **`process.execPath`** using the internal **`__workflow-runner`** argv marker (`src/runtime/kernel/workflow-launch.ts` + `src/cli/index.ts`): the node build invokes `node dist/src/cli.js __workflow-runner …`; the bun-compiled binary invokes itself, `jaiph __workflow-runner …`. The reserved marker is excluded from `--help`/usage and the file-shorthand path. `overlay-run.sh` and `docs/jaiph-skill.md` are also embedded base64 inside the executable via **`src/runtime/embedded-assets.ts`**, so the standalone artifact is **fully self-contained** — no sibling `runtime/` or `docs/` files required. The displayed `jaiph --version` string is sourced from the generated **`src/version.ts`** (codegen'd from `package.json` by `embed-assets`), so the literal is statically baked into both the `tsc` and the `bun build --compile` outputs without a runtime read of `package.json`. **Bash** (or whatever shebang your `script` steps use) is still required on the host for script subprocesses. Ship **`dist/jaiph`** alone, or with **`dist/runtime`** alongside it for parity with the npm layout (table in [Contributing](contributing.md)). -- **Release artifacts:** `.github/workflows/release.yml` cross-compiles the standalone binary for **darwin/linux × arm64/x64** on **`v*`** tag pushes and on pushes to the **`nightly`** branch, generates a `SHA256SUMS` covering the four binaries, runs a `--version` sanity gate on the linux-x64 output, and uploads the five assets to the matching GitHub Release (stable tag or rolling **`nightly`** prerelease). Asset filenames are fixed by the installer contract — see [Contributing — Release asset naming contract](contributing.md#release-asset-naming-contract). +- **Release artifacts:** `.github/workflows/release.yml` cross-compiles the standalone binary for **darwin/linux × arm64/x64** plus **windows x64** (`jaiph-windows-x64.exe`; Bun has no windows arm64 target) on **`v*`** tag pushes and on pushes to the **`nightly`** branch, generates a `SHA256SUMS` covering the five binaries, runs `--version` sanity gates on the linux-x64 and windows-x64 outputs, and uploads the six assets to the matching GitHub Release (stable tag or rolling **`nightly`** prerelease). Asset filenames are fixed by the installer contract — see [Contributing — Release asset naming contract](contributing.md#release-asset-naming-contract). Two installers consume these assets: the POSIX **`docs/install`** (`curl … | bash`, darwin/linux; rejects Windows and points at the PowerShell one) and **`docs/install.ps1`** (`irm https://jaiph.org/install.ps1 | iex`, Windows x64), which downloads `jaiph-windows-x64.exe`, verifies it against `SHA256SUMS` with `Get-FileHash`, and installs to `%LOCALAPPDATA%\jaiph\bin`. ## Mermaid architecture diagram diff --git a/docs/assets/css/style.css b/docs/assets/css/style.css index 2b3cc45b..2543e3e0 100644 --- a/docs/assets/css/style.css +++ b/docs/assets/css/style.css @@ -704,6 +704,41 @@ pre code .code-line::before { display: block; } +.os-switch { + display: flex; + gap: 0.4rem; + margin-bottom: 0.75rem; +} + +.os-switch-button { + border: 1px solid var(--tab-border); + border-radius: 8px; + background: var(--tab-bg); + color: var(--muted); + font-size: 0.8rem; + font-weight: 600; + font-family: "Fira Code", monospace; + padding: 0.3rem 0.7rem; + cursor: pointer; +} + +.os-switch-button.is-active { + background: var(--tab-active-bg); + border-color: var(--tab-active-border); + color: var(--tab-active-color); +} + +/* Platform variants: static markup shows the POSIX (bash) variant; JS reveals + the Windows variant on demand (and by default for Windows visitors). Without + JS the POSIX variant stays visible and the Windows one remains in the markup. */ +.os-variant { + display: none; +} + +.os-variant.is-active { + display: block; +} + .primitive-list { margin: 0.5rem 0 0; } diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js index 6c9e91ef..8521742e 100644 --- a/docs/assets/js/main.js +++ b/docs/assets/js/main.js @@ -732,6 +732,51 @@ }); } + /** + * True when the visitor is on Windows. Prefers the modern + * navigator.userAgentData.platform, falling back to navigator.platform. + */ + function isWindowsPlatform() { + var platform = (navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform || ""; + return /win/i.test(platform); + } + + /** + * Toggles the visible platform variant (posix / windows) across every + * panel in the install card, and reflects the choice on the switch buttons. + */ + function setOsVariant(root, os) { + root.querySelectorAll(".os-switch-button").forEach(function (button) { + button.classList.toggle("is-active", button.getAttribute("data-os") === os); + }); + root.querySelectorAll(".os-variant").forEach(function (variant) { + variant.classList.toggle("is-active", variant.getAttribute("data-os") === os); + }); + } + + /** + * Wires the platform sub-toggle in the install card and auto-selects the + * Windows variant for Windows visitors. Manual switching stays available; + * non-Windows visitors keep the static (POSIX) default with no layout shift. + */ + function attachOsSwitch() { + var root = document.querySelector("section.try-it-out .card"); + if (!root || root.querySelectorAll(".os-variant").length === 0) { + return; + } + root.querySelectorAll(".os-switch-button").forEach(function (button) { + button.addEventListener("click", function () { + var os = button.getAttribute("data-os"); + if (os) { + setOsVariant(root, os); + } + }); + }); + if (isWindowsPlatform()) { + setOsVariant(root, "windows"); + } + } + function attachCodeTabs() { const buttons = document.querySelectorAll(".code-tab-button"); buttons.forEach(function (button) { @@ -979,6 +1024,7 @@ highlightAll(); attachCopyButtons(); attachCodeTabs(); + attachOsSwitch(); attachDocsNavToggle(); attachThemeToggle(); }); @@ -989,6 +1035,7 @@ highlightAll(); attachCopyButtons(); attachCodeTabs(); + attachOsSwitch(); attachDocsNavToggle(); attachThemeToggle(); } diff --git a/docs/configuration.md b/docs/configuration.md index d1ba8de1..d7dc7492 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,14 +83,17 @@ In-file `runtime.docker_enabled` is not supported (`E_PARSE`); use the env-only ## Docker enablement +Checks are applied top to bottom; the first match wins. + | Check | Result | |---|---| +| Platform is Windows (`win32`) | Docker off (host-only mode, with a one-line notice). Overrides everything below, including `JAIPH_DOCKER_ENABLED=true`. | | `JAIPH_DOCKER_ENABLED` is set to exact `true` | Docker on. | | `JAIPH_DOCKER_ENABLED` is set to any other value | Docker off. | | `JAIPH_DOCKER_ENABLED` is unset and `JAIPH_UNSAFE=true` | Docker off. | | Default (no env) | Docker on. | -`CI=true` does not change this default. Host `jaiph run --raw` never consults this branch — the workflow runner is local in that path. See [Sandboxing](sandboxing.md) for the full model. +`CI=true` does not change this default. Host `jaiph run --raw` never consults this branch — the workflow runner is local in that path. On Windows the Docker sandbox is out of scope, so `jaiph run` resolves to host-only mode automatically without probing `docker` or failing on a missing daemon — see [Sandboxing — Windows runs host-only](sandboxing.md#windows-runs-host-only) for the full model. ## Precedence {: #precedence} @@ -236,7 +239,7 @@ The retry backoff above handles a backend that *fails*. A separate set of watchd Set any variable to `0` to disable that layer. The idle timer resets on every chunk of backend output, so a slow-but-active run is bounded only by the absolute cap. -The completion-grace layer specifically addresses the known `claude -p` failure mode where the CLI streams its final answer (and the terminal `result` event) but the process never exits — often because a descendant it spawned is still holding the output pipe open. When a watchdog fires it sends `SIGTERM`, escalating to `SIGKILL` after 5s, and tears down the runtime's handles on the child's stdio so a lingering descendant cannot keep the run alive. Under Docker, `runtime.docker_timeout_seconds` remains the outer backstop for the whole container. +The completion-grace layer specifically addresses the known `claude -p` failure mode where the CLI streams its final answer (and the terminal `result` event) but the process never exits — often because a descendant it spawned is still holding the output pipe open. When a watchdog fires it terminates the backend's whole process tree (via `killProcessTree`; see [Architecture](architecture.md)) with `SIGTERM`, escalating to `SIGKILL` after 5s, and tears down the runtime's handles on the child's stdio so a lingering descendant cannot keep the run alive. On Windows the tree is force-killed with `taskkill /T` on the first signal, so the `SIGKILL` escalation is a no-op. Under Docker, `runtime.docker_timeout_seconds` remains the outer backstop for the whole container. ## Custom agent commands diff --git a/docs/contributing.md b/docs/contributing.md index 05119b24..cc911d3a 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -171,6 +171,9 @@ Tests that span multiple modules, require subprocess/PTY harnesses, exercise pro | `integration/docs-reference-task5.test.ts` | Integration | Reference quadrant — permalinks, nav placement, `env-vars.md` source parity against `src/`, anti-tutorial shape guards | | `integration/docs-tutorials-task6.test.ts` | Integration | Tutorial quadrant — permalinks, `/getting-started` redirect absorption, runnable `first-workflow` snippet with documented output | | `integration/docs-nav-structure-task7.test.ts` | Integration | Nav spine — five Diátaxis section headings in documented order; every published page under its quadrant exactly once | +| `integration/release-workflow.test.ts` | Integration | Release matrix / asset-naming contract — five-binary matrix (no windows-arm64), `SHA256SUMS` + upload lists include `jaiph-windows-x64.exe`, shared version-gate script, naming contract ↔ matrix ↔ installer parity | +| `integration/installer-powershell.test.ts` | Integration | Windows PowerShell installer (`docs/install.ps1`) contract — download/verify/install steps, bash↔PowerShell lockstep release ref, and `docs/setup.md` / main-page one-liner parity | +| `integration/windows-native-smoke.test.ts` | Integration | Host-portable guards for the `windows-native-smoke` CI job and its `e2e/tests/windows_native_smoke.ps1` harness — job shape (windows-latest, `bun --compile` build, gate membership alongside `test`/`e2e`/`e2e-wsl`), stdout/exit-code assertions, cancellation orphan check, `prompt` pre-flight error, and no-WSL enforcement | | `integration/sample-build/build.test.ts` | Integration | Build/transpile behavior — `buildScripts`, script extraction | | `integration/sample-build/cli-tree.test.ts` | Integration | CLI tree output rendering for sample workflows | | `integration/sample-build/run-core.test.ts` | Integration | Core runtime execution — workflow runs, step sequencing, artifacts | @@ -187,7 +190,7 @@ The `integration/sample-build/` directory also has a shared `helpers.ts` module ## CI pipeline -The project uses GitHub Actions (`.github/workflows/ci.yml`). The workflow defines **six** jobs; on a typical feature-branch push, **five** of them run. The sixth — **Publish Docker runtime image** — runs only on pushes to **`nightly`** and on **`v*`** version tags, after the test, E2E, docs, and WSL jobs succeed (ShellCheck is not a publish gate). It builds and pushes `ghcr.io/jaiphlang/jaiph-runtime` (the default `runtime.docker_image` / `JAIPH_DOCKER_IMAGE` when Docker sandboxing is on; see **Docker runtime helper** in [Architecture](architecture.md#core-components)). +The project uses GitHub Actions (`.github/workflows/ci.yml`). The workflow defines **eight** jobs; on a typical feature-branch push, **seven** of them run. The eighth — **Publish Docker runtime image** — runs only on pushes to **`nightly`** and on **`v*`** version tags, after the test, E2E, docs, WSL, PowerShell-installer, and native-Windows-smoke jobs succeed (ShellCheck is not a publish gate). It builds and pushes `ghcr.io/jaiphlang/jaiph-runtime` (the default `runtime.docker_image` / `JAIPH_DOCKER_IMAGE` when Docker sandboxing is on; see **Docker runtime helper** in [Architecture](architecture.md#core-components)). | Job | Runner | Purpose | |-----|--------|---------| @@ -196,6 +199,8 @@ The project uses GitHub Actions (`.github/workflows/ci.yml`). The workflow defin | **E2E** | Matrix: **`ubuntu-latest` twice** + **`macos-latest`** | Job id `e2e`; in the Actions UI each leg appears as **`E2E (,