diff --git a/.github/workflows/free-durations-record.yml b/.github/workflows/free-durations-record.yml index 8faa4598f..85749eea0 100644 --- a/.github/workflows/free-durations-record.yml +++ b/.github/workflows/free-durations-record.yml @@ -164,7 +164,7 @@ jobs: - Files that failed while recording (durations kept; this PR's own free checks decide): $failed_line - Seed growth ratchet (60 s ceiling, shrink-only allowlist): $ratchet_line - Data only. This run dispatched Free Tests (the required free-tests check), Windows Free Tests and Quality gate on this branch; those are this PR's checks. The pull_request runs wait as "action required" because a token-opened PR never starts them without approval. Stamp and merge per docs/ship-policy.md; the stamp push runs the full PR checks. + Data only. This run dispatched Free Tests (the required free-tests check), Windows Smoke and Quality gate on this branch; those are this PR's checks. The pull_request runs wait as "action required" because a token-opened PR never starts them without approval. Stamp and merge per docs/ship-policy.md; the stamp push runs the full PR checks. EOF if [ -n "$number" ]; then gh pr edit "$number" --body-file pr-body.md @@ -172,5 +172,5 @@ jobs: gh pr create --base main --head "$branch" --title "test(seed): refresh the free-suite duration seeds" --body-file pr-body.md fi gh workflow run free-tests.yml --repo "$GITHUB_REPOSITORY" --ref "$branch" - gh workflow run windows-free-tests.yml --repo "$GITHUB_REPOSITORY" --ref "$branch" + gh workflow run windows-smoke.yml --repo "$GITHUB_REPOSITORY" --ref "$branch" gh workflow run quality-gate.yml --repo "$GITHUB_REPOSITORY" --ref "$branch" diff --git a/.github/workflows/windows-free-tests.yml b/.github/workflows/windows-free-tests.yml index 33e896c42..6146e927d 100644 --- a/.github/workflows/windows-free-tests.yml +++ b/.github/workflows/windows-free-tests.yml @@ -1,9 +1,15 @@ name: Windows Free Tests -# The Windows-safe subset of the free suite on GitHub's free `windows-latest` -# runners. scripts/test-free-shards.ts --windows-only is the single source of -# truth for which files run: POSIX-bound tests (bin/ shebang spawns, raw /tmp -# paths, /bin/sh, mode bitmasks, the Bun-driven browse server) are excluded by +# Nightly, not a PR gate. Windows has minimal support (docs/support-tiers.md): +# the required Windows check is windows-smoke.yml (./setup plus the core +# skills). This workflow runs the curated Windows-safe subset of the free suite +# on main every night and on workflow_dispatch, and never blocks a pull +# request. A red nightly run gets a fix only when it shows an install, startup +# or core-workflow break; other Windows-only failures close as won't fix. +# +# scripts/test-free-shards.ts --windows-only is the single source of truth for +# which files run: POSIX-bound tests (bin/ shebang spawns, raw /tmp paths, +# /bin/sh, mode bitmasks, the Bun-driven browse server) are excluded by # pattern there, with explicit force-include and known-incompatible lists. # Every run prints the curated count and top exclusion reasons in the job # summary so a shrinking subset is visible. @@ -27,13 +33,9 @@ name: Windows Free Tests # dispatch and duration-recording runs never consume. on: - pull_request: - branches: [main] - # Also on main pushes, as free-tests.yml does: trains dispatch only the - # Linux free workflow, so without this no Windows run ever sees a train's - # combined tree or main after a merge. - push: - branches: [main] + # 09:17 UTC, about 2 AM Pacific, on the default branch (main). + schedule: + - cron: '17 9 * * *' workflow_dispatch: inputs: record_durations: @@ -113,8 +115,10 @@ jobs: if-no-files-found: error windows-free-shard: + # gstack-pregate reads a job keyed on a dispatch input as manual, never a + # PR obligation; this lane runs only nightly or by hand. needs: [windows-plan, reuse] - if: ${{ !cancelled() && needs.windows-plan.result == 'success' && (needs.reuse.result != 'success' || needs.reuse.outputs.reused != 'true') }} + if: ${{ !cancelled() && !inputs.record_durations && needs.windows-plan.result == 'success' && (needs.reuse.result != 'success' || needs.reuse.outputs.reused != 'true') }} runs-on: windows-latest timeout-minutes: 15 strategy: @@ -139,14 +143,8 @@ jobs: name: windows-plan path: ${{ runner.temp }} - # bun's install cache lives under ~/.bun/install/cache on every platform. - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: ~/.bun/install/cache - key: windows-bun-${{ hashFiles('bun.lock') }} - # A lockfile bump starts from the previous cache instead of cold. - restore-keys: | - windows-bun- + # No bun install cache: restoring the ~450 MB entry took 33-42 s per + # shard, a cold install 16-17 s (GSTA-203 PR 1 A/B, 3 runs per arm). - name: Configure git identity (required by tests that init temp repos) run: | diff --git a/.github/workflows/windows-setup-e2e.yml b/.github/workflows/windows-setup-e2e.yml index 6b2b928fe..2ca74e7b4 100644 --- a/.github/workflows/windows-setup-e2e.yml +++ b/.github/workflows/windows-setup-e2e.yml @@ -92,16 +92,8 @@ jobs: with: bun-version: 1.4.2 - # Same lockfile-keyed install cache as windows-free-tests.yml (install - # was 45s of a 64s job, all network). - - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: ~/.bun/install/cache - key: windows-bun-${{ hashFiles('bun.lock') }} - # A lockfile bump starts from the previous cache instead of cold - # (restore alone costs ~43s; without this a bump pays it for nothing). - restore-keys: | - windows-bun- + # No bun install cache, as in windows-free-tests.yml: a cold install is + # faster on windows-latest than restoring the ~450 MB cache entry. - name: Configure git identity run: | diff --git a/.github/workflows/windows-smoke.yml b/.github/workflows/windows-smoke.yml new file mode 100644 index 000000000..525f50553 --- /dev/null +++ b/.github/workflows/windows-smoke.yml @@ -0,0 +1,170 @@ +name: Windows Smoke + +# The required Windows check under minimal support (docs/support-tiers.md): +# with a default Git for Windows install, ./setup installs and the core skills +# start. One windows-latest job, under 5 minutes: +# 1. ./setup --host claude into an isolated HOME under Git Bash, with +# gstack's own browser (Playwright Chromium). +# 2. The core skills start: gstack-skill-start for /ship, /review, +# /qa-only, /investigate and /browse answers SKILL_START_PROTO: 1. +# 3. /browse on gstack's own browser: the installed browse runs a piped +# chain flow against a local page from an unrelated directory. +# 4. The fastest Windows-safe free tests for those paths (list below). +# The full curated Windows suite is windows-free-tests.yml: nightly on main +# and on dispatch, never a PR gate. +# +# No bun install cache: a cold install took 16-17 s on windows-latest, a +# restore of the ~450 MB cache 33-42 s (GSTA-203 PR 1 A/B, 3 runs per arm). + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: windows-smoke-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + windows-smoke: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + + - name: Configure git identity (required by tests that init temp repos) + run: | + git config --global user.email "windows-smoke@gstack.test" + git config --global user.name "Windows Smoke" + git config --global init.defaultBranch main + shell: bash + + - name: Install dependencies + run: bun install --frozen-lockfile + shell: bash + + - name: ./setup installs into an isolated HOME + run: | + set -euo pipefail + SMOKE_HOME="$RUNNER_TEMP/smoke-home" + mkdir -p "$SMOKE_HOME" + HOME="$SMOKE_HOME" USERPROFILE="$(cygpath -w "$SMOKE_HOME")" ./setup --host claude + test -x "$SMOKE_HOME/.claude/skills/gstack/bin/gstack-skill-start" || (echo "MISSING: installed gstack-skill-start" && exit 1) + shell: bash + + - name: The core skills start + run: | + set -euo pipefail + SMOKE_HOME="$RUNNER_TEMP/smoke-home" + USERPROFILE="$(cygpath -w "$SMOKE_HOME")" + export HOME="$SMOKE_HOME" USERPROFILE + for skill in ship review qa-only investigate browse; do + set +e + OUT=$(timeout 120 "$SMOKE_HOME/.claude/skills/gstack/bin/gstack-skill-start" --skill "$skill" --model claude 2>&1 < /dev/null) + CODE=$? + set -e + echo "::group::$skill"; echo "$OUT"; echo "::endgroup::" + test "$CODE" -eq 0 || (echo "/$skill: gstack-skill-start exited $CODE" && exit 1) + grep -q '^SKILL_START_PROTO: 1' <<< "$OUT" || (echo "/$skill: no SKILL_START_PROTO: 1 line" && exit 1) + done + shell: bash + + - name: /browse on gstack's own browser runs a piped chain flow from an unrelated directory + run: | + set -euo pipefail + SMOKE_HOME="$RUNNER_TEMP/smoke-home" + USERPROFILE="$(cygpath -w "$SMOKE_HOME")" + export HOME="$SMOKE_HOME" USERPROFILE + BROWSE="$SMOKE_HOME/.claude/skills/gstack/browse/dist/browse.exe" + [ -f "$BROWSE" ] || BROWSE="$SMOKE_HOME/.claude/skills/gstack/browse/dist/browse" + test -f "$BROWSE" || (echo "MISSING: installed browse" && exit 1) + bun -e 'Bun.serve({ hostname: "127.0.0.1", port: 47124, fetch: () => new Response("gstack smoke

gstack-windows-smoke-ok

", { headers: { "content-type": "text/html" } }) }); setInterval(() => {}, 1 << 30);' & + PAGE_PID=$! + trap 'kill "$PAGE_PID" 2>/dev/null || true; "$BROWSE" stop >/dev/null 2>&1 || true' EXIT + mkdir -p "$RUNNER_TEMP/unrelated-dir" && cd "$RUNNER_TEMP/unrelated-dir" + set +e + OUT=$(printf '%s\n' '[["goto","http://127.0.0.1:47124/"],["text"]]' | "$BROWSE" chain 2>&1) + CODE=$? + set -e + echo "$OUT" + if [ "$CODE" -ne 0 ]; then + find "$RUNNER_TEMP/unrelated-dir" "$SMOKE_HOME" -maxdepth 4 \( -name 'browse-daemon.log' -o -name 'browse-startup-error.log' \) -print -exec tail -n 60 {} \; 2>/dev/null || true + echo "browse chain exited $CODE" && exit 1 + fi + grep -q 'gstack-windows-smoke-ok' <<< "$OUT" || (echo "chain output lacks the page text: the flow did not run" && exit 1) + shell: bash + + - name: Fastest Windows-safe free tests for install and the core skills + # Windows-measured seconds from scripts/free-test-durations-windows.json. + # setup-windows-fallback (0.1): setup's _link_or_copy copies, never links, on Windows + # setup-staged-copy (0.6): a copy install is staged and swapped in only when complete + # setup-windows-rerun-refresh (1.7): re-running ./setup refreshes copied skills (#2444) + # setup-skill-copies (0.1): copied SKILL.md hashes and the backup of an edited copy + # setup-bun-cmd-and-pipe-bugs (0.4): setup's bun.cmd and pipe handling + # gstack-launch-probe (0.1): setup's Smart App Control launch probe (#2595) + # state-root-parity (2.1): every skill start resolves one state root in TS and bash + # bun-isolation-launch (1.1): Bun entries ignore .env and bunfig under Git Bash (#3415) + # gstack-lease-git-bash (3.7): the skill-start lease under Git Bash (#3330) + # gstack-wtree-windows-paths (1.3): /review and /ship record a start on C:/ paths (#3101) + # ship-hook-windows-paths (2.3): /ship's pre-push guard in a Windows worktree (#2805) + # qa-only-browser-probe (2.4): /qa-only's browser probe fixture + # find-browse (0.1): /browse resolves its binary (.exe) + # cli-chain-stdin (0.3): /browse chain reads its flow from stdin on Windows (#3039) + # windows-spawn-hide (0.1): /browse's spawns hide their console window (#1835) + env: + GSTACK_FREE_RETRY_FLAKY: '1' + GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} + run: | + set -euo pipefail + cat > "$RUNNER_TEMP/windows-smoke-files.txt" <<'LIST' + test/setup-windows-fallback.test.ts + test/setup-staged-copy.test.ts + test/setup-windows-rerun-refresh.test.ts + test/setup-skill-copies.test.ts + test/setup-bun-cmd-and-pipe-bugs.test.ts + test/gstack-launch-probe.test.ts + test/state-root-parity.test.ts + test/bun-isolation-launch.test.ts + test/gstack-lease-git-bash.test.ts + test/gstack-wtree-windows-paths.test.ts + test/ship-hook-windows-paths.test.ts + test/qa-only-browser-probe.test.ts + browse/test/find-browse.test.ts + browse/test/cli-chain-stdin.test.ts + browse/test/windows-spawn-hide.test.ts + LIST + bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-smoke-files.txt" + shell: bash + + - name: Upload full test logs + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: windows-smoke-logs + path: | + .context/free-test-logs/gstack-free-test-*.log + ${{ runner.temp }}/gstack-free-test-*.log + include-hidden-files: true + if-no-files-found: ignore + + - name: Upload flake ledger + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: flake-ledger-windows-smoke + path: ${{ runner.temp }}/flake-ledger.jsonl + if-no-files-found: ignore + retention-days: 90 diff --git a/.gstack/pregate.json b/.gstack/pregate.json index c424957cc..74a99f1ad 100644 --- a/.gstack/pregate.json +++ b/.gstack/pregate.json @@ -16,8 +16,7 @@ "supabase/**": [] }, "selection": { - "free-tests.yml/free-suite": "bun run scripts/test-free-shards.ts --list", - "windows-free-tests.yml/windows-free-shard": "bun run scripts/test-free-shards.ts --windows-only --list" + "free-tests.yml/free-suite": "bun run scripts/test-free-shards.ts --list" }, "run_profiles": { "ci-reuse/free-tests": { "workflow": "free-tests.yml", "events": ["pull_request", "workflow_dispatch"], "jobs": ["free-plan", "typecheck", "quick-bun-default", "cso-macos-launcher", "macos-named-regressions", "cso-windows-launcher", "cso-docker-integration", "free-tests"], "artifact": "tested-tree", "os": "Linux" }, diff --git a/.gstack/ship-policy.json b/.gstack/ship-policy.json index 2ae73849b..a37b11ca4 100644 --- a/.gstack/ship-policy.json +++ b/.gstack/ship-policy.json @@ -14,6 +14,6 @@ "pr_count": { "default_pr_count": 1, "split_requires_owner": true }, "maintainers": ["garrytan"], "train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", "max_size": 5, "fill_hours": 1, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, - "changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-free-tests", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] }, + "changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-smoke", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] }, "min_gstack_version": "1.91.340.0" } diff --git a/AGENTS.md b/AGENTS.md index febab17af..1b724461b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -285,9 +285,11 @@ Support tiers (full: macOS, Linux; minimal: Windows, Gemini models) are in [docs/support-tiers.md](docs/support-tiers.md). - **macOS** + **Linux**: full test suite supported. -- **Windows**: the curated Windows-safe subset runs in the `windows-free-tests` - CI workflow across six `windows-latest` jobs, packed by Windows-measured - durations. Setup script (`./setup`) requires Git Bash or +- **Windows**: minimal support; [docs/support-tiers.md](docs/support-tiers.md) + says what is promised. Every PR must pass the `windows-smoke` check (`./setup`, + the core skills start, the fastest Windows-safe tests); the curated + Windows-safe subset runs nightly on main in `windows-free-tests` and never + blocks a PR. Setup script (`./setup`) requires Git Bash or MSYS today; native PowerShell support is a future expansion. The `bin/gstack-paths` helper resolves state roots through `CLAUDE_PLUGIN_DATA` / `GSTACK_HOME` so plugin installs work on every platform. diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a2763c2..1a662182b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,155 @@ # Changelog +## [1.92.56.0] - 2026-11-02 + +**`gstack-ship-receipt write` records the session kind on Windows.** Without `--session-kind` or `GSTACK_SESSION_KIND`, it asks the `gstack-session-kind` bash helper. It used to spawn that helper by path, which never starts on Windows, so every /ship receipt there carried no `session_kind`. It now starts the helper through `lib/script-invocation.ts`, which uses Git Bash on Windows. This is the /ship half of #3541. + +### Fixed +- **`bin/gstack-ship-receipt`** starts `gstack-session-kind` with `runScriptSync`. + +### Tests +- `test/ship-receipt-session-kind.test.ts` (windows-free-tests too): a receipt written without a session kind records what `gstack-session-kind` prints. + +## [1.92.55.0] - 2026-11-02 + +- **`/ship` finish no longer stops on a branch whose restamp moved its own stale stamp after a lost slot** (`gstack-ship-release finish`, GST-895). When a branch loses its merge slot, merges main and runs `gstack-restamp --next`, the restamp moves the branch's own `since:` rows from the lost version to the new one. Finish only accepted a moved version at or above the reviewed tree's VERSION, which now came from main, so it read those rows as a behavior change and stopped with "insert 5–11.5 → 12–14 → 16" (gbrain #8134 on 1.92.46.0). Now a line the branch added itself, by the same rule restamp uses (added since the merge base with main, and not added by main), may move any version newer than the branch's fork point, and finish routes it as release metadata. A row main carries, a row main edited after the merge, and a branch row at or before the fork point still stop. Only `stamp_files` paths count, and nothing else on the line may change. + +### Tests +- `test/ship-rereview-triggers.test.ts`: after a lost slot, a merge of main and a restamp, the branch's moved row routes as metadata; moving main's own row, a merged main row that main later edited, a branch row at the fork version, or one older than the fork point each stop. The first case fails on main with `behavior=1`. + +## [1.92.54.0] - 2026-11-02 + +**`train land` and a single-PR `land` can no longer publish onto main at the same time, and a slot that forgot its version bump can no longer merge (#3599, #3531; GSTA-157).** Before, `train land` took no lock, so it and `land` could both read the same main and publish a combined tree neither tested. And train 33's `verify --train` said "may merge" for #3527's head after a bare commit dropped the stamp that restamp had written but not staged, so #3527 shipped under #3526's version. + +### Fixed +- **`train land` takes the shared land lock** (`refs/notes/gstack-land`, now in lib/ship-queue-land-lock.ts) as `train slot (#)`. Whichever of `land` and `train land` starts second refuses with `QUEUE_LAND_LOCKED`, naming the holder. Once a slot has merged, the train keeps the lock in phase `between` until its last slot lands, through a refusal on the next slot too, so no single land slips in between slots and sets off a freeze. A stop that freezes the queue leaves the lock in phase `freeze`, which only the admitted repair train takes over while the freeze lasts; once the freeze is cleared, the next `land` or `train land` takes over a freeze lock whose release push failed. A lock ref deleted on origin now reads as no lock, not as this clone's cached copy. +- **`--break-lock` prints the holder** (`LOCK_HOLDER:`), keeps land's staleness, mid-push and drift rules, never breaks a `freeze` lock, waits 10 minutes on a `between` lock, and refuses a live train run's lock unless `--break-train-lock` is also given. `train land` accepts both flags. +- **`verify --train` checks the slot stamp itself**: the head's VERSION must be above main's and its CHANGELOG headed with that version directly above main's top release, or it refuses with the new `QUEUE_TRAIN_UNSTAMPED`, naming the rule and both versions. It prints `STAMP_VERSION:` from the head. +- **`verify --train` calls a PR merged only when GitHub reports it MERGED.** An open release-only PR (#3530's shape) now gets the pre-merge check, and any other open or unreadable PR refuses with `QUEUE_PROOF_MISSING` without freezing the queue. So does a verify that could not read the PR's current head (`--no-fetch` or a failed fetch), instead of judging the unstamped pin. +- **`gstack-restamp` stages the files it writes** and prints `STAGED:`. It refuses before writing when a stamp file holds unstaged edits of your own (`RESTAMP_UNSTAGED_EDITS`); when staging fails, it says `RESTAMP_NOT_STAGED` and the next run stages the files. +- Two comments in lib/ship-queue-train-gate.ts now say "one spend cap per command gate across every prefix of the train", which is what the code does. + +### Tests +- `test/ship-queue-land-lock.test.ts`: two writers racing one lock sha, the four lock read states, owner kinds, the `--break-lock` rules, and both lock directions, the between phase, refusal and resume, and the freeze lock's repair takeover through `bin/gstack-ship-queue`. +- `test/ship-queue-train.test.ts`: the #3527 and #3530 shapes, the CHANGELOG rule, a claim-at-open pin, and no false freeze. `test/gstack-restamp-queue.test.ts`: staging after a no-commit merge, the unstaged-edits refusal, and a staging failure and its rerun. + +## [1.92.53.0] - 2026-11-02 + +**`reap` and `up` now destroy only your own Ubicloud VMs (GST-573).** Before, every `up` destroyed any VM in the project whose TTL had ended, whoever created it, which broke the rule that cloud deletes stay with the owner who made the resource. Now reap destroys only the caller's own expired VMs and its own old TTL-less ones, and only counts other owners' expired VMs in its log (`reap: left N expired VMs of other owners; only their owners reap them`). + +### Changed +- **Owner-scoped reap.** Another owner's VM, even an expired one, and an untagged one are never reaped. A lane's leftover VM is destroyed by that owner's own next `up` or `reap`, so a lane that never runs again leaves its VM until someone with that owner runs `reap` or `down`. + +## [1.92.52.0] - 2026-11-02 + +**/ship on Capy runs Exploratory QA again.** Since v1.91.345.0 (#3648) the driver sent Step 9.2.1 out as a QA reader subagent, but the section says only the parent runs report-only discovery, so the reader refused on every pass ("parent-only") and the parent ran the probes afterward anyway, an extra reader call each pass (the C4-36 and C5-36 replays). + +### Fixed +- **The parent runs Step 9.2.1's probes itself, as the section says.** Phase 3 does the section's method Reads and no probe; Phase 4's one program runs the smoke and plan checks after Step 8.1 collects the plan checks and before Scope Check, dedup and Fix-First, and the record's Checks heading carries the `Exploratory QA:` line. The call count is unchanged, and the section text is untouched. +- **`gstack-review-dispatch pass` refuses a QA extra.** An extra named `qa`, or one whose prompt holds Step 9.2.1's "Only the parent runs report-only discovery.", stops the pass before any reader is created. A prompt file that cannot be read stops as an unreadable prompt, not a raw read error. The Capy skill lists Steps 7, 8 and 14.5 as extras, not Step 9.2.1. + +## [1.92.51.0] - 2026-11-01 + +**PRs no longer wait on the six-shard Windows suite. A short `windows-smoke` check is the required Windows check, and the full suite runs nightly on main.** Windows has minimal support (docs/support-tiers.md). Since 2026-11-01 06:00 UTC, Windows Free Tests finished last on 71 of 73 PR heads, with a wall time of 892 s at p50 and 1,886 s at p90 over 71 green PR runs, and each run used about 59 Windows runner-minutes. The new smoke check finished in 198 s on this PR (run 41325664750). With smoke in its place, the slowest required check on a PR drops from 892 s to 562 s at p50 and from 1,877 s to 1,193 s at p90. + +### Changed +- **New `windows-smoke.yml`** (check `windows-smoke`, on pull requests, pushes to main and by hand). It installs gstack with `./setup --host claude` into a separate home, starts /ship, /review, /qa-only, /investigate and /browse with `gstack-skill-start`, runs the installed browse chain against gstack's own browser, and runs 15 Windows-safe test files. Each file's reason is written next to it in the workflow. No cache: a cold `bun install` took 16 to 17 s, while restoring the cache took 33 to 42 s in a three-round A/B. +- **`windows-free-tests.yml` runs nightly** (09:17 UTC) on main, and by hand. It no longer runs on pull requests or pushes and never blocks a merge. A red nightly gets a fix only when it shows a broken install, startup or core workflow. Other Windows-only failures close as won't fix with a link to docs/support-tiers.md. +- **Bun cache removed** from `windows-free-tests.yml` and `windows-setup-e2e.yml`. The 18 cache entries on pull-request refs (8.05 GB) were deleted. Main's entries were left alone. +- **`gstack-ship-queue ready --dispatch-ci`** skips a named workflow that main has but the PR branch predates, and prints a note instead of failing. GitHub reads the workflow file from the branch, so dispatching `windows-smoke.yml` on an older branch could only fail. + +### Gate integrity +- `.gstack/ship-policy.json` `required_checks`: `windows-free-tests` is replaced by `windows-smoke`. The ci-hint, `--dispatch-ci` docs, train CLI help and `free-durations-record.yml` now name `windows-smoke.yml`. +- `.gstack/pregate.json`: the `windows-free-tests.yml/windows-free-shard` remote lane is removed, since that suite no longer runs on PRs. Linux and macOS lanes are unchanged. +- `scripts/test-health-report.ts` measures Windows from `windows-smoke.yml` PR runs. +- No test file was deleted, skipped or re-tiered. `scripts/harness-version.json` records `scripts/test-pr-profile.ts` as a non-behavioral change, because it only lists `windows-smoke.yml` among files no paid case reads. + +## [1.92.50.0] - 2026-11-01 + +### Fixed +- **`/autoplan` no longer locks a Claude Code session after a snapshot init run as `cd && …` (#3651).** When that `cd` leaves the session's folder, Claude Code adds a `Shell cwd was reset to …` line to the command's output. The guard now removes exactly that one line before reading the init or checkpoint JSON; any other extra text still fails. An init whose output still can't be read now counts as a failed init with a "re-run" hint, so a later clean init in the same session works again. Before, every later phase read was denied with code `evidence` until a new session. + +### Fixed +- **`/eval-plan` pilots with a subscription arm can now be approved, and the cost projection is per arm (#3653).** `gstack-eval-plan pilot start --seat-metered ` declares an arm that runs on a subscription with no extra charge (for example `claude -p` on a seat). Its attempts hold none of the spend cap, settle as `unknown` (an arm that settles a real charge is projected like any priced arm), and get their own `PILOT_SEAT_METERED:` line, so the verdict covers the arms that cost money. `pilot report` now projects each arm from its own attempts (one `PILOT_ARM:` line each) and adds them up. Before, it averaged all arms together: a priced arm's cost was multiplied across arms with no price, and two arms at different prices got a band so wide that a pilot that fit the cap was told it exceeded it. The skill also says to stop a seat-metered run at its first quota or rate-limit error, with no retry (idea from #3273 by @merit-blake). + +**`/autoplan --ceo-only` runs the CEO review by itself and asks nothing from start to finish (GST-537).** Garry asked for a CEO review that "just automatically does the right thing", and the planning eval needed a gstack CEO arm that a harness can run without a simulated user. Before, /autoplan always ran the Eng review too and stopped at a final approval question, and standalone /plan-ceo-review asks for the mode, offers to chain to the next skill and adds a Codex voice. + +### Added +- **`/autoplan --ceo-only`** (`autoplan/sections/ceo-only.md`): runs Phase 0, the outside-reviewer preflight and the CEO phase only. Every question takes its recommendation and gets an audit-trail row; consent and destructive options take the conservative choice. The review mode comes from plan-ceo-review's 0E rule. User challenges are never applied; each one is listed at the top of `/ceo-review.md`, with scope added, conservative choices, findings and the audit trail. The run ends with `GSTACK_RESULT: skill=autoplan status=reviewed run=` and approves nothing. +- **`--no-outside`** with `--ceo-only` skips the outside voice, so one model does the whole review. +- **`gstack-ceo-mode-handoff --ceo-only`** prints `Auto-selected review mode → (CEO-only run: Step 0E's rule)` instead of crediting a saved preference. +- **`status=reviewed`** joins the `GSTACK_RESULT` statuses (`docs/unattended.md`, "CEO-only runs"). + +### For contributors +- The paid case `autoplan-ceo-only` fails if the mode asks anything, skips a later phase's file, or leaves new files in HOME outside its folders. It takes 6 to 16 minutes, so it runs in the weekly marathon lane, not on autoplan PRs. The mode needs Claude Code for now. + +**/autoplan fixes from a field report (GBRA-223): findings without a line number pair across voices, open Critical and High findings reach the gate, a spend cap binds on any host, `run` works with the API runner, and an earlier item-by-item answer carries forward.** OpenClaw's /autoplan run on a real plan hit six problems at once. We reproduced each on main with a failing test first (GST-672). A follow-up from GEVA-2 adds a record of whose words answered a gate. + +### Fixed +- **Plan reviews that name a file but no line now pair across voices.** A same-file pair, with a line missing on either side, is associated as `unverified` instead of both findings counting as new. Title matching ignores a leading DX cap code (`CAP4::`) and never pairs findings on two different files. The same-file fallback runs only after every title and line match is placed, and a same-file pair never merges two findings into one gate item. +- **DX cap findings carry real titles.** `CAP4` and `CAP6` findings are titled `CAP:: `, so the gate shows the problem and not only the item name. +- **Every open Critical or High finding becomes a gate item.** `a` applies the reviewer's fix to the plan and is recommended. `b` carries it into the build as a blocking task. `rec` and `p1b` apply the fix. An unanswered item keeps the plan as written unless the reply is `p1b`, and a partial reply never locks that choice early. A finding carried with `b` stays open, so a rerun that raises it again asks again. +- **A spend cap binds on any host.** With `--spend-cap` set and no `--estimate-usd`, each host-subagent attempt reserves the default $5 per voice, on Capy or off it, in any session kind, and so does an API call whose model has no price. `next --estimate-usd ` sets the reservation for the rest of the run. `run` reserves each API call's priced worst case (prompt size plus the output limit) and settles it at its real token cost, so `--spend-cap 0` refuses before any call. +- **`gstack-autoplan run` with `--native-runner api --runner api` reaches the gate.** Results are checked as autoplan results, so they bind. A response cut off at the output limit is named `truncated` and interrupts the run instead of binding a partial review. The default output limit is 16384 tokens for both providers (OpenAI counts reasoning tokens toward it), `--max-tokens ` sets it, and an outside result refused at submit leaves the run native-only instead of aborting it. +- **An earlier item-by-item answer carries forward when nothing changed.** When every item on the new gate is carried by origin, the owner's `p1` choice is replayed and their tokens are recorded verbatim as the basis. Any new or changed item still goes to the owner. +- **A spend cap stays closed when a call fails.** For the native voice, a call cut off at its output limit is settled at what it cost, and a retry with a larger `--max-tokens` is admitted against the cap afresh. A call that timed out, dropped, was cancelled or had its result refused after it was sent is settled at its usage, or its full reservation when the usage is unknown, because the provider may have billed it. A call still in flight when the process died is charged its reservation by the next `run`, which then sends a fresh attempt. A call refused before anything was sent (no API key, for example) charges nothing, and the rerun reuses its reservation. A failed outside call is still counted at its full reservation while the phase continues native-only. +- **A finding you chose to fix stays open until the revised plan is reviewed.** If the reviewer raises it again after a revise that missed it, it comes back to the gate instead of being treated as settled. A withdrawn fix leaves its finding open. +- **Two findings that only sit near each other are no longer merged.** A user challenge one line away from an unrelated Critical no longer hides the Critical. Findings are merged only when their titles match or the parent says they are the same problem. +- **A plan gate answered in an agent's own words is not owner approval.** `gstack-ship-receipt` rejects a plan-sha approval citation whose gate was answered with `--words-by agent:`. +- **A later gate answer replaces the earlier answer's authorship.** When someone else answers again, the earlier answerer's words, `words_by` and carry fields are cleared, so the approval record names who actually answered. + +### Added +- **`gstack-autoplan answer --words-by agent:`** records that the words came from the answering agent itself, as `answer_words` and `words_by`, never as `owner_reply`. The default stays `owner`. A later gate never carries those words forward, and `gstack-approvals import` lists that row under the agent, while `--by` names only the rows that hold the owner's words. + +### Tests +- New failing-first tests in test/autoplan-pairing.test.ts, test/autoplan-gate-judgment.test.ts, test/autoplan-budget.test.ts, test/autoplan-run-api.test.ts (a local fake provider, no key or network), test/autoplan-carry.test.ts and test/approval-record.test.ts. + +## [1.92.49.0] - 2026-11-01 + +**The timeline Stop hook returns within its 2-second budget even when the host keeps stdin open.** The hook read its payload with a blocking read that waits for end of input, so a host that writes the Stop payload and leaves the pipe open held the hook for as long as the pipe stayed open. A local run with the pipe held for 8 seconds took 8 seconds, past the hook's 2-second budget and on the way to Claude Code's 15-second hook window (#3628, reported on Windows). Rewritten from PR #3628 by shahab (mashtii85). + +### Fixed +- **`hosts/claude/hooks/timeline-stop-hook.ts`** reads stdin for at most 500 ms and stops at the first complete JSON payload, then repairs the timeline as before. The hook still exits 0 on every path. + +### Tests +- `test/timeline-stop-hook-stdin.test.ts` (runs on windows-latest): a complete payload on a pipe that never closes returns within budget and still writes the repair; an open pipe with no data returns within budget. Both failed before the fix (10 s, killed). + +## [1.92.48.0] - 2026-11-01 + +**On Windows, `gstack-review-scope summary` works again, and the JSONL merge driver no longer fails on non-English text or rewrites files with CRLF.** Before, the summary started the bash script `gstack-review-read` directly, which Windows cannot run, so it always refused with an empty "gstack-review-read failed:" message and /ship could not write its review-loop file (#3541). The `jsonl-append` merge driver wrote its result with Python's `print()`, so a line the console code page could not encode (Cyrillic text under cp1250, for example) made it exit 1 and git reported a conflict with no markers, and every merge on Windows turned LF files into CRLF (#3659). + +### Fixed +- **`gstack-review-scope summary`** runs `gstack-review-read` through `lib/script-invocation.ts` (Git Bash on Windows). When the helper cannot start, the refusal names the launch error instead of an empty string. +- **`gstack-jsonl-merge`** writes UTF-8 bytes with LF line endings on every platform and code page. + +### Tests +- `test/review-scope-windows-launch.test.ts` runs the summary against a seeded review log through the same launcher; it runs on windows-latest. +- `test/jsonl-merge-encoding.test.ts` merges a Cyrillic line with `PYTHONIOENCODING=cp1252` and checks the bytes are UTF-8 with no CR. It failed before the fix on Linux too. +- `test/script-invocation-tripwire.test.ts` now also catches a bin spawned through `path.join(path.dirname(...import.meta.url...), ...)` or `import.meta.dir`, the shape that hid #3541. `bin/gstack-ship-receipt`'s `gstack-session-kind` spawn is listed as a known site until its own follow-up. + +## [1.92.47.0] - 2026-11-01 + +- **`/ship` Step 5 runs its test lanes one at a time on machines with 4 or fewer cores, and `--lanes serial|parallel` picks the mode by hand** (`gstack-ship-preflight`, GST-851). Preflight used to start every `--lane` at once. On garrytan/gstack-evals#115 six lanes (a 1,451-test bun suite, tsc, a Docker tier and an arm tarball trial) started within 60 ms of each other on a 4-core, 16 GB machine, and running lanes together has taken machines down before (gstack-evals #101, #106, #107). Now, with no flag, preflight runs one lane at a time when `os.availableParallelism()` is 4 or less and all of them at once above that. `--lanes serial` or `--lanes parallel` overrides the default, and any other value, or `--lanes` with no value, is a usage error (exit 2). A new `LANES: serial|parallel (--lanes| cores)` line before the `LANE` lines says which mode ran and why. Each lane still prints its own `LANE` line with its own `tests_ran` count, in the order the lanes were given. Serial runs take the sum of the lane times instead of the longest one. There is no ship-policy key for this: a new key needs key versioning and a `min_gstack_version` bump, and the core-count default already covers the 4-core case. + +### Tests +- `test/ship-preflight.test.ts`: `--lanes serial` runs lanes one after another and keeps each lane's count; `--lanes parallel` keeps lane order when a later lane finishes first; an unflagged run names the core count; a bad or missing `--lanes` value is a usage error; and the core-count default is serial at 4 cores or fewer. + +## [1.92.46.0] - 2026-11-01 + +**brain-sync no longer publishes a synced JSON file that doesn't parse, and the profile and doctor now say when one is broken.** When two machines edit `developer-profile.json` before syncing, git's merge can leave conflict markers in it. brain-sync committed and pushed that file, so every machine got a profile that no longer parsed, while `gstack-developer-profile --profile` and `gstack-doctor` both reported everything ok (#3763). + +### Fixed +- **brain-sync holds back a synced `.json` file that doesn't parse** (`bin/gstack-brain-sync`). Before each commit it parses every staged `*.json` file (today `developer-profile.json` and `projects/*/decisions.active.json`). A file that doesn't parse is held with rule `invalid-json`, along with any file coupled to it (`decisions.jsonl` with its snapshot). It stays queued and syncs on the next run after you repair it; everything else syncs as usual. The fix line says to repair the file. It does not offer `--skip-file`, which would stop the profile syncing for good. +- **A held file in an unfinished git merge stops the sync instead of committing half the merge.** If a held file is part of a merge that git could not finish, committing the merge without it would quietly keep only this machine's version and drop the other machine's edit. brain-sync now commits nothing and reports `blocked` until you repair the file; the merge and the queue are kept. +- **Held-status messages name the right cause.** The skill-start attention line, `--status` and `gstack-doctor` no longer blame the secret scan for every held file; doctor reports how many held files are not valid JSON. +- **`gstack-doctor` warns when a synced JSON file holds conflict markers**, naming the file and how to restore it from the sync repo's history. + +### Changed +- **`gstack-developer-profile --profile` now exits 1 when the profile is not valid JSON.** It prints ` is not valid JSON: ` on stderr (noting conflict markers when it finds them) and nothing on stdout. It used to print the broken file and exit 0, so scripts that read `--profile` should check its exit status. + +### Tests +- `test/brain-sync.test.ts`: a queued profile with conflict markers, a marker-free syntax error, an invalid `decisions.active.json` holding its coupled `decisions.jsonl` until repaired, two machines editing the profile (the other machine's edit stays on the remote), and an unfinished merge with a held profile. `test/gstack-developer-profile.test.ts`: `--profile` on an unparseable profile. `test/gstack-doctor.test.ts`: a conflict-marker warning, and a JSON hold not attributed to the secret scan. All fail on main. + ## [1.92.45.0] - 2026-11-01 ### /ship review scales to risk, not size diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6132d56ac..634968ea6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,8 +73,8 @@ project where you actually felt the pain. **What CI does on a fork PR.** GitHub never gives a fork PR this repository's secrets, so CI runs everything that needs none: the required `free-tests` check -(the Linux free suite, typecheck, the macOS and Windows gates), Windows Free -Tests, Skill Docs Freshness, Workflow Lint, Version Gate, Quality gate, the +(the Linux free suite, typecheck, the macOS and Windows gates), Windows +Smoke, Skill Docs Freshness, Workflow Lint, Version Gate, Quality gate, the make-pdf gate, and any path-triggered gates your change touches. E2E Evals still builds the CI image from your `Dockerfile.ci` without publishing it, and skips its paid eval jobs, which need provider API keys. That skip is expected @@ -192,7 +192,7 @@ the new defaults. | Run full free acceptance before publishing | `bun run test` | Free, a few minutes | Bun 1.4.2 | | Run the full free suite from a small machine | `bun run test:ubicloud` | Free suite on a billed 16-vCPU VM, about 5 minutes | `UBICLOUD_API_KEY`, `UBI_OWNER` | | Clear pregate's test tier on a machine under 8 cores ([remote gate](docs/pregate.md#remote-gate)) | `gstack-pregate --stage tests --remote --spend-cap 2 --timeout 2700 --allow-repo-commands` (same command keyed or keyless) | Keyed: one billed 16-vCPU VM, about 7 minutes and $0.06 for the whole free tier, worst case $0.38; keyless: free, local parallel shards, practical for small selections only (98 files took 103 s on 4 cores) | Keyed: `UBICLOUD_API_KEY`, `UBI_OWNER`; keyless: nothing | -| Run the curated Windows-safe subset | `bun run test:windows` (CI: six `windows-latest` jobs) | Free | Windows, Git Bash | +| Run the curated Windows-safe subset | `bun run test:windows` (CI: six `windows-latest` jobs, nightly on main; PRs run the shorter `windows-smoke` check) | Free | Windows, Git Bash | | Refresh the Windows duration seed | `gh workflow run windows-free-tests.yml --ref -f record_durations=true`, then commit the `free-test-durations-windows` artifact | Free CI runners | Pushed branch, `gh` with workflow rights | | Run a native Windows qualification campaign | `gh workflow run native-qualification.yml --ref -f mode=` ([modes](docs/TESTING_INTERNALS.md#windows-free-lane)) | Free CI runners | Pushed branch, `gh` with workflow rights | | Preview which paid cases my diff selects | `bun run eval:select` (PR profile; `--profile full` for the plain touchfile selection) | Free | — | diff --git a/TODOS.md b/TODOS.md index a95320ccd..eb40e432b 100644 --- a/TODOS.md +++ b/TODOS.md @@ -17,6 +17,12 @@ - **Derive `carry.covers` from depth.** A carried receipt's `covers` is the fixed `CARRY_COVERS` list (lib/ship-receipt.ts); derive it from the recorded `review_depth` so a light review's carry does not claim the checks a full review ran (plan item 5, deferred at /ship). **Effort:** S. **Priority:** P1. - **Bound the queue's recompute time.** `gstack-ship-queue` recomputes each receipt's needed depth at READY and at train build; add a test that a five-PR train recomputes within a fixed budget, so a large diff cannot stall the train. **Effort:** S. **Priority:** P1. +### P2/P3: Windows smoke follow-ups (filed 2026-11-01, from the GSTA-203 PR 1 /ship review) +- **Branches cut before windows-smoke.yml still owe the old Windows lane (P2)** — pregate reads lanes from the branch's own workflows, so such a branch keeps an uncleared `windows-free-tests.yml/windows-free-shard` obligation, and `ready --dispatch-ci` skips `windows-smoke.yml` there. The lane clears it by passing `windows-free-tests.yml` to `--dispatch-ci` (the old cost) or with a main-merge grant. Have the skip note name that route, or dispatch the old workflow in smoke's place. The skip also treats a branch that deletes or renames `windows-smoke.yml` as one that predates it; check the merge base too. **Effort:** S. **Priority:** P2. +- **Someone owns the nightly (P2)** — nothing reads the scheduled `windows-free-tests.yml` result or its flake ledgers. Add a test-health-report metric for scheduled runs on main, or have the daily flaky pass read the latest nightly. **Effort:** S. **Priority:** P2. +- **Smoke /browse step waits for its page server (P3)** — `windows-smoke.yml` starts `Bun.serve` in the background and calls `browse chain goto` right away; poll the port first. **Effort:** XS. **Priority:** P3. +- **Smoke seed ceiling vs the weekly reseed (P3)** — `test/windows-native-workflows.test.ts` holds each smoke file to 5 s in the Windows seed, which `free-durations-record.yml` rewrites weekly; one slow recording turns the seed PR red. Report the overrun there or re-pick files at reseed. **Effort:** S. **Priority:** P3. + ### P2/P3: pregate head-policy follow-ups (filed 2026-10-31, from the GST-419/GST-420 /ship review) - **`finish` fallback drops head-policy lanes (P2)** — `lib/ship-release.ts` reruns pregate on the stamped tree under the base policy, so after a version bump a lane the branch added under `--policy-from head` is gone and `finish` reports `PREGATE: fail (rerun on the stamped tree)`. GSTA-194 owns that file and has the note. **Effort:** S. **Priority:** P2. - **The 3600 s preflight and `finish` wrapper deadline (P3)** — it caps the whole pregate run, so several runners that each fit `timeout_s` can still be cut off together. Derive the wrapper deadline from `timeout_s` and the lane count, or document the cap. **Effort:** S. **Priority:** P3. diff --git a/VERSION b/VERSION index 603bff1c3..dd192841b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.92.45.0 +1.92.56.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index d00c52753..e8c563d3f 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.92.45.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.92.56.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/autoplan/DRIVER.md b/autoplan/DRIVER.md index 875ea1068..ff551298e 100644 --- a/autoplan/DRIVER.md +++ b/autoplan/DRIVER.md @@ -57,28 +57,28 @@ line yourself; gstack never writes the plan. ### Commands -- `gstack-autoplan next --out --plan [scope flags] [--light] [--spend-cap ] [--max-rounds ] [--scaffold]` - first call: init the run, print PHASE:, SCOPE:, DRIVER: and one ATTEMPT: per voice; --max-rounds caps review rounds per phase (default 2 unattended), and an unattended Capy run reserves 5.00 per host attempt unless --estimate-usd says otherwise, so --spend-cap binds. +- `gstack-autoplan next --out --plan [scope flags] [--light] [--spend-cap ] [--max-rounds ] [--estimate-usd ] [--scaffold]` + first call: init the run, print PHASE:, SCOPE:, DRIVER: and one ATTEMPT: per voice; --max-rounds caps review rounds per phase (default 2 unattended); a run with --spend-cap, in any session kind (or any unattended Capy run), reserves 5.00 per voice for each attempt, so the cap binds; --estimate-usd sets that reservation and the run keeps it for later phases. +- `gstack-autoplan run --out --plan --native-model --outside-model --native-runner api --runner api [--max-tokens ] [--spend-cap ]` + hosts without subagents: one command calls both models directly (ANTHROPIC_API_KEY, OPENAI_API_KEY) and loops next/submit to the gate; a response cut off at the output limit is reported truncated, so raise --max-tokens (default 16384 for both providers; OpenAI counts reasoning tokens toward it); each api call reserves its priced worst case (a model with no price reserves the per-voice default under a cap), a native call that went out but bound no result settles at its usage, or its full reservation when the usage is unknown, and a failed outside call stays counted at its reservation. - `gstack-autoplan next --out [--redispatch ]` later calls: open the next phase or reprint open attempts; never redispatches on its own. - `gstack-autoplan submit --out --phase

--voice native|outside --record --model [--attempt ]` bind a free-form review record (canonical fence, one findings table or list, or `No findings.`). - `gstack-autoplan submit ... --record --attest "" --attempt ` bind a record with no INPUT: line as receipt: attested (a wrong INPUT: always refuses). -- `gstack-autoplan submit --out

--phase

--voice native|outside --result --model --attempt ` - bind a canonical result (the RESULT FORMAT below). +- `gstack-autoplan submit --out

--phase

--voice native|outside --result --model --attempt [--usage-usd ] [--reconcile ]` + bind a canonical result (the RESULT FORMAT below); `--usage-usd` settles the attempt at what the reviewer actually cost (else it stays priced unknown at its reservation); on the submit that closes the phase, `--reconcile` takes your own pairing of findings both voices raised, one `{"outside_id","native_id","disposition","resolution"}` row each, over the structural one. - `gstack-autoplan submit --out

--phase

--voice outside --unavailable|--failed ""` close the phase native-only when no outside voice can run. -- `gstack-autoplan answer --out

--gate-rev --reply "" --by human|agent: [--owner-reply |-] [--rec-hash ]` - answer the gate with the reply grammar below; an agent answer stores the owner's words verbatim. +- `gstack-autoplan answer --out --gate-rev --reply "" --by human|agent: [--owner-reply |-] [--words-by agent:] [--rec-hash ]` + answer the gate with the reply grammar below; an agent answer stores the owner's words verbatim, and passes --words-by agent: whenever the words are its own (the default is owner). - `gstack-autoplan recommend --out --gate-rev --item --option --basis ""` change one recommendation before relay; then render and relay again (a `rec` needs the new `--rec-hash`). - `gstack-autoplan revise --out --plan --incorporates ` bind the amended plan; reopens every phase whose reviewed plan changed, Eng last. -- `gstack-autoplan revise --out --settle ` - discharge a packaging or ordering change with no plan edit; reopens nothing. -- `gstack-autoplan revise --out --withdraw ` - drop a pending amendment instead of applying it. +- `gstack-autoplan revise --out --settle | --withdraw ` + --settle discharges a packaging or ordering change with no plan edit and reopens nothing; --withdraw drops a pending amendment instead of applying it. - `gstack-autoplan close --out --phase

--deferred "" --by human|agent:` close a phase accepted-with-findings: its open findings go to tasks.jsonl, and Critical, urgent or user-challenge findings become gate items; also the recovery from `STOP: reason=budget_exhausted`. - `gstack-autoplan budget --out

[--spend-cap ] [--max-rounds ] --by human|agent:` @@ -111,7 +111,7 @@ A reply binds to the `gate_rev` you read (`--gate-rev`). Bare `yes`/`no` is unpa - `rec`: every item takes its recommendation, plan changes included (each prints APPLY:); `all` and `do it` are aliases. - `as-is`: keep every direction; no plan change. -- `d3b uc1a`: one