From 47794bb785555d9bd324d9e33efb19cad9095552 Mon Sep 17 00:00:00 2001 From: tr3lane <32203735+tr3lane@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:03:29 -0500 Subject: [PATCH] Windows: make grok + codex implementer lanes work headless - run_capped: cross-platform hard wall-clock; Windows tree-kills via taskkill //T //F on the winpid; *nix uses validated GNU coreutils timeout (never trusts system32 timeout.exe). Foreground-only to avoid orphaned marker-poll loops. - grok --permission-mode auto (was acceptEdits, which silently drops every write headless on Windows). - codex --sandbox read-only on Windows (no OS sandbox there): runs as a read-only reviewer returning a git-apply-able patch (STATUS: proposal); grok is the Windows writer. macOS/Linux keep workspace-write. *nix behavior unchanged. Tested headless on Windows 11 / Git Bash. --- agents/codex-implementer.md | 88 ++++++++++++++++++++++++++++++------- agents/grok-implementer.md | 71 +++++++++++++++++++++++++----- 2 files changed, 132 insertions(+), 27 deletions(-) diff --git a/agents/codex-implementer.md b/agents/codex-implementer.md index 30963a7..2bffcd5 100644 --- a/agents/codex-implementer.md +++ b/agents/codex-implementer.md @@ -48,48 +48,106 @@ and include its actual output in your final message."] SPEC_EOF ``` -2. Invoke codex non-interactively, sandboxed to the workspace, with reasoning effort pinned high: +2. Invoke codex non-interactively, sandboxed to the workspace, with reasoning effort pinned high — under a **hard wall-clock cap that works on every OS**: ```bash -# Portable timeout: macOS has no `timeout` unless coreutils is installed -T=$(command -v gtimeout || command -v timeout || true) -[ -z "$T" ] && echo "WARN: no timeout binary — codex runs uncapped (brew install coreutils to cap)" - -${T:+$T 600} codex exec \ +# --- Hard wall-clock cap (cross-platform, Windows-safe) --------------------- +# The old `${T:+$T 600}` cap failed two ways on Windows/Git Bash: +# 1. `command -v timeout` can resolve to system32 timeout.exe (an interactive +# countdown, NOT a process capper) — so codex ran UNCAPPED. +# 2. Even GNU timeout / a plain `kill` only reach the DIRECT child. codex is an +# npm shim (sh -> node); killing the shim leaves the node worker running. +# So: on Windows we skip `timeout` entirely and tree-kill the whole Windows +# process tree via taskkill; elsewhere we use validated GNU coreutils timeout. +# The run is ALWAYS bounded — codex can never spin past the deadline. rc 124 = hit cap. +run_capped() { # run_capped (stdin/redirects pass through) + local secs=$1; shift + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) : ;; # Windows -> tree-kill path below + *) # macOS/Linux -> idiomatic GNU timeout + local T; T=$(command -v gtimeout || command -v timeout || true) + if [ -n "$T" ] && "$T" --version 2>/dev/null | grep -qi coreutils; then + "$T" -k 15 "$secs" "$@"; return $? # TERM at , KILL 15s later; 124 on cap + fi ;; # else fall through to the bash-native guard + esac + local flag; flag=$(mktemp) + "$@" <&0 & # <&0 forces stdin (the spec) onto the bg job + local pid=$! + ( i=0 # watcher: bounded by BOTH a deadline AND + while [ "$i" -lt "$secs" ]; do # a kill -0 liveness check, so an abnormal + sleep 1; i=$((i+1)) # codex exit ends the watch — never spins. + kill -0 "$pid" 2>/dev/null || exit 0 # job finished on its own -> stop watching + done + echo 1 > "$flag" # deadline reached -> mark BEFORE killing (no race) + local wp; wp=$(cat "/proc/$pid/winpid" 2>/dev/null) + if [ -n "$wp" ]; then taskkill //T //F //PID "$wp" >/dev/null 2>&1 # kill the whole tree + else kill -TERM "$pid" 2>/dev/null; sleep 15; kill -KILL "$pid" 2>/dev/null; fi + ) >/dev/null 2>&1 & # redirect: watcher never holds the tool's pipe + local killer=$! + wait "$pid"; local rc=$? + wait "$killer" 2>/dev/null # watcher self-exits within 1s; reap, don't kill mid-sleep + [ -s "$flag" ] && rc=124 # normalize: deadline fired -> timeout + rm -f "$flag" + return $rc +} + +# --- Sandbox mode: Windows can't ENFORCE workspace-write -------------------- +# Codex only writes under an OS-enforced sandbox (Seatbelt/Landlock). Windows has +# none, so `--sandbox workspace-write` silently downgrades to read-only and rejects +# every patch. Rather than a blocked-write failure, on Windows run codex READ-ONLY +# as a cross-vendor REVIEWER: it reads the tree and returns its implementation as a +# patch in its final message, which the caller (or the grok lane) applies. Grok is +# the writer on Windows; codex is the second-family check. macOS/Linux write for real. +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + SANDBOX=read-only + printf '\n\nENVIRONMENT NOTE: read-only sandbox (Windows) — you CANNOT write files. Deliver your full implementation in your final message as (a) a single `git apply`-able unified diff AND (b) the complete final contents of every changed file. Do not attempt to write; the caller applies your patch.\n' >> "$SPEC" ;; + *) SANDBOX=workspace-write ;; +esac + +# Run codex in the FOREGROUND under the cap. Cap is 540s (9 min), deliberately +# under the Bash tool's 600000 ms max so the KILL escalation completes before the +# tool would kill bash and re-orphan the child. Set the tool timeout to 600000 ms. +run_capped 540 codex exec \ --model gpt-5.6-sol \ -c model_reasoning_effort=high \ - --sandbox workspace-write \ + --sandbox "$SANDBOX" \ --skip-git-repo-check \ --cd "$(pwd)" \ --output-last-message "$FINAL" \ - < "$SPEC" +rc=$? +[ "$rc" = 124 ] && echo "STATUS: timeout — codex exceeded the 540s wall clock" ``` +**Foreground only — never background codex behind a marker poll.** Run the block above as one foreground Bash call (tool timeout `600000` ms). Do **not** launch codex as a background task: the harness then polls the log for codex's `tokens used` completion marker (`until grep -q "tokens used" …`), and an **abnormal** codex exit under its sandbox bug never writes that marker — so the watcher loop spins forever as an orphaned process. The wall-clock guard already bounds the run; foreground + `run_capped` needs no watcher. If you ever must poll anyway, bound the loop with a deadline **and** a `kill -0 "$pid"` liveness check (as `run_capped`'s own watcher does) so an abnormal exit ends the watch instead of looping. + Flag discipline (non-negotiable): | Flag | Why | |---|---| -| `--sandbox workspace-write` | Codex writes code, scoped to the working tree. Never `danger-full-access`. | -| `-c model_reasoning_effort=high` | Pins GPT-5.6 Sol to high reasoning for complex implementation work. | +| `--sandbox "$SANDBOX"` | `workspace-write` on macOS/Linux (codex writes, scoped to the tree). **`read-only` on Windows** — no OS sandbox exists there, so codex reviews + returns a patch instead of writing (grok is the Windows writer). Never `danger-full-access`. | +| `-c model_reasoning_effort=high` | The lane's whole value is maximum-effort GPT-5.6 Sol. | | `--skip-git-repo-check` + `--cd "$(pwd)"` | Deterministic working root; works outside git repos. | | `- < spec file` | Prompt via stdin. No quoting hazards, no truncated specs. | -| `${T:+$T 600}` | Ten-minute wall clock when `timeout`/`gtimeout` exists (macOS needs `brew install coreutils`); runs uncapped otherwise. On timeout, report `STATUS: timeout` with whatever landed. | +| `run_capped 540` | Hard wall clock (540s, under the Bash tool's 600000 ms max) enforced on **every** OS: Windows tree-kills the process tree via `taskkill //T //F` on the win PID (GNU `timeout`/`kill` only reach the npm shim, not the node worker); macOS/Linux use validated GNU `timeout`/`gtimeout` (`brew install coreutils`). Never trusts Windows `timeout.exe`. On timeout `rc=124` → report `STATUS: timeout` with whatever landed. | -`--model gpt-5.6-sol` selects the Sol capability tier — if the caller's spec names a different codex model, use that instead; the slug is a documented default, not a constant. +`--model gpt-5.6-sol` is the current top GPT tier — if the caller's spec names a different codex model, use that instead; the slug is a documented default, not a constant. -3. **Verify independently.** Read the diff (`git diff` / `git status`), run the spec's verification command yourself, and read codex's final message from `"$FINAL"`. Codex's claim of success is not evidence; your re-run is. +3. **Verify independently.** On macOS/Linux: read the diff (`git diff` / `git status`), run the spec's verification command yourself, and read codex's final message from `"$FINAL"`. Codex's claim of success is not evidence; your re-run is. **On Windows (read-only reviewer):** there is no diff — codex's deliverable IS the patch in `"$FINAL"`. Sanity-check the patch applies cleanly (`git apply --check`) and report it as a `proposal`; do not apply it yourself unless the caller asked (fix/apply decisions belong upstream — the caller routes it to the grok lane or applies it). ## What you return ``` CODEX REPORT -STATUS: complete | partial | timeout | unavailable +STATUS: complete | proposal | partial | timeout | unavailable OBJECTIVE: [restated in one line] -CHANGES: [file — one-line summary, per file, from the actual diff] -VERIFIED: [verification command you re-ran — actual output evidence] +CHANGES: [file — one-line summary, per file, from the actual diff | on Windows: from codex's proposed patch] +VERIFIED: [verification command you re-ran — actual output evidence | on Windows: `git apply --check` result for the proposed patch] CODEX SAID: [one-line summary of codex's final message, note any disagreement with the diff] GAPS: [spec ambiguities, unfinished items, or "none"] ``` +(`STATUS: proposal` = the Windows read-only path — codex returned a reviewed patch for the caller to apply, not a completed write.) ## Rules diff --git a/agents/grok-implementer.md b/agents/grok-implementer.md index e8fa784..b99c008 100644 --- a/agents/grok-implementer.md +++ b/agents/grok-implementer.md @@ -45,36 +45,83 @@ and include its actual output in your final message."] SPEC_EOF ``` -2. Invoke grok headlessly, scoped to the working tree: +2. Invoke grok headlessly, scoped to the working tree — under a **hard wall-clock cap that works on every OS**: ```bash -# Portable timeout: macOS has no `timeout` unless coreutils is installed -T=$(command -v gtimeout || command -v timeout || true) -[ -z "$T" ] && echo "WARN: no timeout binary — grok runs uncapped (brew install coreutils to cap)" - -${T:+$T 600} grok --prompt-file "$SPEC" \ +# --- Hard wall-clock cap (cross-platform, Windows-safe) --------------------- +# The old `${T:+$T 600}` cap failed two ways on Windows/Git Bash: +# 1. `command -v timeout` can resolve to system32 timeout.exe (an interactive +# countdown, NOT a process capper) — so grok ran UNCAPPED. +# 2. Even GNU timeout / a plain `kill` only reach the DIRECT child, leaving any +# grandchild grok spawns (to run commands) alive. +# So: on Windows we skip `timeout` entirely and tree-kill the whole Windows +# process tree via taskkill; elsewhere we use validated GNU coreutils timeout. +# The run is ALWAYS bounded — grok can never spin past the deadline. rc 124 = hit cap. +run_capped() { # run_capped (stdin/redirects pass through) + local secs=$1; shift + case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) : ;; # Windows -> tree-kill path below + *) # macOS/Linux -> idiomatic GNU timeout + local T; T=$(command -v gtimeout || command -v timeout || true) + if [ -n "$T" ] && "$T" --version 2>/dev/null | grep -qi coreutils; then + "$T" -k 15 "$secs" "$@"; return $? # TERM at , KILL 15s later; 124 on cap + fi ;; # else fall through to the bash-native guard + esac + local flag; flag=$(mktemp) + "$@" <&0 & + local pid=$! + ( i=0 # watcher: bounded by BOTH a deadline AND + while [ "$i" -lt "$secs" ]; do # a kill -0 liveness check, so an abnormal + sleep 1; i=$((i+1)) # grok exit ends the watch — never spins. + kill -0 "$pid" 2>/dev/null || exit 0 # job finished on its own -> stop watching + done + echo 1 > "$flag" # deadline reached -> mark BEFORE killing (no race) + local wp; wp=$(cat "/proc/$pid/winpid" 2>/dev/null) + if [ -n "$wp" ]; then taskkill //T //F //PID "$wp" >/dev/null 2>&1 # kill the whole tree + else kill -TERM "$pid" 2>/dev/null; sleep 15; kill -KILL "$pid" 2>/dev/null; fi + ) >/dev/null 2>&1 & # redirect: watcher never holds the tool's pipe + local killer=$! + wait "$pid"; local rc=$? + wait "$killer" 2>/dev/null # watcher self-exits within 1s; reap, don't kill mid-sleep + [ -s "$flag" ] && rc=124 # normalize: deadline fired -> timeout + rm -f "$flag" + return $rc +} + +# Run grok in the FOREGROUND under the cap. Cap is 540s (9 min), deliberately +# under the Bash tool's 600000 ms max so the KILL escalation completes before the +# tool would kill bash and re-orphan the child. Set the tool timeout to 600000 ms. +FINAL=$(mktemp -t grok-final.XXXXXX) +# ⚠ permission-mode: use `auto`, NOT `acceptEdits`. On Windows headless, `acceptEdits` +# (and `dontAsk`/`default`) SILENTLY DROP every file write — grok exits 0, narrates +# success, and no file lands (verified A/B 2026-07-10). Only `auto` and +# `bypassPermissions` actually write headless; `auto` is the least-privilege of the two. +run_capped 540 grok --prompt-file "$SPEC" \ -m grok-4.5 \ - --permission-mode acceptEdits \ + --permission-mode auto \ --output-format plain \ --cwd "$(pwd)" \ - > /tmp/grok-final-$$.txt 2>&1 -FINAL=/tmp/grok-final-$$.txt + > "$FINAL" 2>&1 +rc=$? +[ "$rc" = 124 ] && echo "STATUS: timeout — grok exceeded the 540s wall clock" ``` +**Foreground only — never background grok behind a marker poll.** Run the block above as one foreground Bash call (tool timeout `600000` ms). Do **not** launch grok as a background task: the harness then polls the log for a completion marker (`until grep -q … "$FINAL"`), and an **abnormal** grok exit never writes that marker — so the watcher loop spins forever as an orphaned process. The wall-clock guard already bounds the run; foreground + `run_capped` needs no watcher. If you ever must poll anyway, bound the loop with a deadline **and** a `kill -0 "$pid"` liveness check (as `run_capped`'s own watcher does) so an abnormal exit ends the watch instead of looping. + Flag discipline (non-negotiable): | Flag | Why | |---|---| | `--prompt-file "$SPEC"` | Headless single-task run from a file. No quoting hazards, no truncated specs. | | `-m grok-4.5` | The lane's producer is Grok 4.5, pinned explicitly — never rely on the CLI default. | -| `--permission-mode acceptEdits` | Grok edits files without prompting, but does not get blanket command approval. Never `--always-approve` — you re-run verification yourself. | +| `--permission-mode auto` | Grok applies edits (and runs commands) without prompting — **required for headless writes on Windows**, where `acceptEdits` silently drops every write (exit 0, no file). `auto` is the least-privilege mode that actually writes; you still re-run verification yourself. | | `--cwd "$(pwd)"` | Deterministic working root. | | `--output-format plain` | Final message to stdout, captured for the report. | -| `${T:+$T 600}` | Ten-minute wall clock when `timeout`/`gtimeout` exists. On timeout, report `STATUS: timeout` with whatever landed. | +| `run_capped 540` | Hard wall clock (540s, under the Bash tool's 600000 ms max) enforced on **every** OS: Windows tree-kills the process tree via `taskkill //T //F` on the win PID; macOS/Linux use validated GNU `timeout`/`gtimeout` (`brew install coreutils`). Never trusts Windows `timeout.exe`. On timeout `rc=124` → report `STATUS: timeout` with whatever landed. | `-m grok-4.5` is the current top Grok tier — if the caller's spec names a different grok model, use that instead; the slug is a documented default, not a constant. -3. **Verify independently.** Read the diff (`git diff` / `git status`), run the spec's verification command yourself, and read grok's final message from `"$FINAL"`. Grok's claim of success is not evidence; your re-run is. (`acceptEdits` may have blocked grok from running the verification itself — your re-run covers that by design.) +3. **Verify independently.** Read the diff (`git diff` / `git status`), run the spec's verification command yourself, and read grok's final message from `"$FINAL"`. Grok's claim of success is not evidence; your re-run is — **doubly so on Windows**, where a wrong permission-mode makes grok narrate a write that never happened. Confirm files actually changed on disk, not just that grok *said* so. ## What you return