diff --git a/.github/workflows/windows-scoped-tests.yml b/.github/workflows/windows-scoped-tests.yml new file mode 100644 index 000000000..1675d15e7 --- /dev/null +++ b/.github/workflows/windows-scoped-tests.yml @@ -0,0 +1,206 @@ +name: Windows Scoped Tests + +# The path-scoped Windows free shard (ship-90-90 plan D). windows-smoke.yml is +# still the required Windows check on every pull request, and the full +# curated suite (windows-free-tests.yml) still runs nightly on main. This +# workflow sits between them: when a pull request touches path, file-name or +# process code, one windows-latest job runs the test files the pull request +# changed that are in the curated Windows-safe subset. +# +# "Path, file-name or process code" (the windows-scope filter step): +# - file names: anything under a bin/ directory (shell entry points and +# skill hooks such as freeze/bin/check-freeze.sh, #3758), shell and +# PowerShell scripts, hosts/*/hooks, setup, bun.lock, the Windows +# curation (scripts/test-free-shards.ts, scripts/lib/windows-curation.ts, +# the Windows duration seed, windows-free-tests.yml), and code files whose +# names say path, spawn, exec, process, shell, launch, win32, windows, +# tmp, temp, symlink, state-root, slug, claude-bin, bun-isolation or +# script-invocation; +# - added or removed code lines that spawn processes or spell paths: +# child_process, spawn/exec calls, Bun.spawn, Bun.$, path.win32 and the +# other separator-sensitive path calls, process.platform/execPath, win32, +# tmpdir(), /dev/null, /tmp/, symlink, realpath, chmod, cygpath, +# USERPROFILE, homedir(), fileURLToPath and .exe. #3829's Windows break was +# one such line in a test it added: spawnSync(process.execPath, [..., +# '--config=/dev/null']), which native Windows Bun cannot open. +# test/windows-path-scope-workflow.test.ts proves the filter catches #3758 +# and #3829 and passes over a docs-only and a pure-logic change. +# +# The windows-scoped-tests job is the check to read: it passes when nothing +# is in scope (no Windows job ran) and otherwise requires the Windows job, so +# a skipped run reports success. It is not one of the required checks, but +# gstack-ship-queue ready and land refuse a head with any failed check, so a +# red Windows job blocks the pull request it runs on: in a pull request that +# touches path, file-name or process code, a changed curated test file has to +# pass on Windows. A change to path or process code with no changed curated +# test selects nothing; the nightly suite covers it. +# gstack-pregate reads a job keyed on a dispatch input as manual, never a PR +# obligation: this check runs on the pull request itself. A workflow_dispatch +# run with the files input reruns chosen Windows-safe files on a branch. + +on: + pull_request: + branches: [main] + workflow_dispatch: + inputs: + files: + description: Space-separated Windows-safe free test files to run (from bun run test:windows --list) + type: string + required: true + +concurrency: + group: windows-scoped-tests-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +# Test-only lane — no token writes. +permissions: + contents: read + +jobs: + windows-scope: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + files: ${{ steps.select.outputs.files }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: github.event_name == 'pull_request' + with: + fetch-depth: 2 + persist-credentials: false + - id: filter + name: Does the diff touch path, file-name or process code? + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + git diff --name-only --no-renames HEAD^1 HEAD > "$RUNNER_TEMP/changed-files" + code_re='\.(ts|tsx|js|mjs|cjs|sh|bash|ps1|cmd|bat)$|(^|/)bin/[^/.]+$|(^|/)setup$' + path_re='(^|/)bin/|\.(sh|bash|ps1|cmd|bat)$|(^|/)hosts/[^/]+/hooks/|(^|/)setup$|^bun\.lock$|^scripts/test-free-shards\.ts$|^scripts/lib/windows-curation\.ts$|^scripts/free-test-durations-windows\.json$|^\.github/workflows/windows-free-tests\.yml$' + name_re='(path|spawn|exec|process|shell|launch|win32|windows|tmp|temp|symlink|state-root|slug|claude-bin|bun-isolation|script-invocation)[^/]*$' + content_re='child_process|\b(spawn|spawnSync|execSync|execFile|execFileSync)[[:space:]]*\(|Bun\.(spawn|spawnSync|\$)|\bpath\.(win32|posix|sep|delimiter|relative|isAbsolute|normalize)\b|process\.(platform|execPath)|\bwin32\b|tmpdir\(|/dev/null|/tmp/|symlink|realpath|chmod|cygpath|USERPROFILE|homedir\(|fileURLToPath|\.exe\b' + { + grep -E "$path_re" "$RUNNER_TEMP/changed-files" | sed 's/^/path: /' || true + grep -E "$code_re" "$RUNNER_TEMP/changed-files" | grep -E "$name_re" | sed 's/^/name: /' || true + { grep -E "$code_re" "$RUNNER_TEMP/changed-files" || true; } | while IFS= read -r file; do + git diff -U0 --no-renames HEAD^1 HEAD -- "$file" | grep -E '^[+-]' | grep -vE '^(\+\+\+|---) ' | grep -m1 -E "$content_re" | awk -v f="$file" '{ print "content: " f ": " $0 }' || true + done + } > "$RUNNER_TEMP/windows-scope" + if [ -s "$RUNNER_TEMP/windows-scope" ]; then + echo 'windows=true' >> "$GITHUB_OUTPUT" + { echo 'This pull request touches path, file-name or process code:'; echo '~~~'; head -n 40 "$RUNNER_TEMP/windows-scope"; echo '~~~'; } >> "$GITHUB_STEP_SUMMARY" + else + echo 'windows=false' >> "$GITHUB_OUTPUT" + echo 'This pull request touches no path, file-name or process code, so no Windows job runs. windows-smoke still runs on every pull request.' >> "$GITHUB_STEP_SUMMARY" + fi + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + if: steps.filter.outputs.windows == 'true' + with: + bun-version: 1.4.2 + - id: select + name: Pick the changed test files in the curated Windows-safe subset + if: steps.filter.outputs.windows == 'true' + run: | + set -euo pipefail + GITHUB_STEP_SUMMARY=/dev/null bun run scripts/test-free-shards.ts --windows-only --ci-plan "$RUNNER_TEMP/curated.json" --shards 1 > /dev/null + jq -r '.shards[].files[]' "$RUNNER_TEMP/curated.json" | LC_ALL=C sort -u > "$RUNNER_TEMP/curated" + LC_ALL=C sort -u "$RUNNER_TEMP/changed-files" | LC_ALL=C comm -12 - "$RUNNER_TEMP/curated" > "$RUNNER_TEMP/selected" + if [ ! -s "$RUNNER_TEMP/selected" ]; then + echo 'No changed test file is in the curated Windows-safe subset, so no Windows job runs.' >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + { echo 'files<> "$GITHUB_OUTPUT" + { echo "The Windows job runs $(wc -l < "$RUNNER_TEMP/selected" | tr -d ' ') changed Windows-safe test file(s):"; sed 's/^/- /' "$RUNNER_TEMP/selected"; } >> "$GITHUB_STEP_SUMMARY" + + windows-scoped-shard: + needs: windows-scope + if: ${{ (github.event_name == 'workflow_dispatch' && inputs.files != '') || needs.windows-scope.outputs.files != '' }} + runs-on: windows-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24.18.0 + + # The same inputs as a windows-free-tests.yml shard, with no bun install + # cache (a cold install beats restoring it on windows-latest). + - name: Configure git identity (required by tests that init temp repos) + run: | + git config --global user.email "windows-ci@gstack.test" + git config --global user.name "Windows CI" + git config --global init.defaultBranch main + shell: bash + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build server-node.mjs (required by Windows browse path) + run: bash browse/scripts/build-node-server.sh + shell: bash + + - name: Generate host SKILL.md outputs (.agents, .factory) + run: bun run gen:skill-docs --host all + shell: bash + + - name: Install Chromium for the Node worker smoke + run: bunx playwright install chromium + + - name: Run the scoped Windows-safe test files + env: + FILES: ${{ inputs.files || needs.windows-scope.outputs.files }} + GSTACK_FREE_RETRY_FLAKY: '1' + GSTACK_FLAKE_LEDGER: ${{ runner.temp }}/flake-ledger.jsonl + TEMP: ${{ runner.temp }} + TMP: ${{ runner.temp }} + run: | + set -euo pipefail + printf '%s\n' "$FILES" | tr -d '\r' | tr ' ' '\n' | sed '/^$/d' > "$RUNNER_TEMP/windows-scoped-files.txt" + bun run scripts/test-free-shards.ts --files-from "$RUNNER_TEMP/windows-scoped-files.txt" + shell: bash + + - name: Upload full test logs + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + with: + name: windows-scoped-test-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-scoped + path: ${{ runner.temp }}/flake-ledger.jsonl + if-no-files-found: ignore + retention-days: 90 + + windows-scoped-tests: + needs: [windows-scope, windows-scoped-shard] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Pass when nothing is in scope, else require the Windows job + env: + SCOPE_RESULT: ${{ needs.windows-scope.result }} + SHARD_RESULT: ${{ needs.windows-scoped-shard.result }} + FILES: ${{ inputs.files || needs.windows-scope.outputs.files }} + run: | + set -eu + test "$SCOPE_RESULT" = success + if [ -z "$FILES" ]; then + echo 'Nothing in scope (no changed Windows-safe test file in a pull request that touches path, file-name or process code); passing without a Windows run.' >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + test "$SHARD_RESULT" = success diff --git a/.github/workflows/windows-setup-e2e.yml b/.github/workflows/windows-setup-e2e.yml index 2ca74e7b4..37cee929a 100644 --- a/.github/workflows/windows-setup-e2e.yml +++ b/.github/workflows/windows-setup-e2e.yml @@ -32,6 +32,13 @@ name: Windows Setup E2E # Separate from windows-free-tests.yml because that one runs a curated # unit-test subset; this one exercises the install path itself. # +# Runs by path (ship-90-90 plan D): the paths below are the install files +# this workflow runs or ./setup sources or runs. The windows-scope job then +# drops a pull request whose only install change is package.json's version +# line, which is what the merge-train stamp commit puts on every PR head. +# windows-scope's list must match the paths list (pinned by +# test/windows-path-scope-workflow.test.ts). +# # Runner: GitHub-hosted free windows-latest, two parallel jobs. ~10-25 min. on: @@ -39,6 +46,7 @@ on: branches: [main] paths: - 'package.json' + - 'bun.lock' - 'scripts/build.sh' - 'scripts/build-cso.sh' - 'scripts/build-cso-windows.ps1' @@ -64,6 +72,18 @@ on: - 'bin/gstack-claude-root.sh' - 'bin/gstack-browser-ensure' - 'bin/gstack-path-spelling.sh' + - 'bin/gstack-config' + - 'bin/gstack-patch-names' + - 'bin/gstack-state-root.sh' + - 'bin/gstack-skill-name.sh' + - 'bin/gstack-install-registry.sh' + - 'bin/gstack-render-claude.sh' + - 'bin/gstack-host-renders.sh' + - 'bin/gstack-migrate-claude-code' + - 'bin/gstack-settings-hook' + - 'bin/gstack-relink' + - 'bin/gstack-harness' + - 'scripts/preflight-codex-overlap.ts' - 'lib/path-spelling.ts' - 'hosts/claude/hooks/**' - 'gstack-upgrade/**' @@ -81,7 +101,94 @@ permissions: contents: read jobs: + windows-scope: + # The paths filter above starts this workflow; this job decides whether + # the Windows jobs run. A workflow_dispatch run always runs them. + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + install: ${{ steps.scope.outputs.install }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: github.event_name == 'pull_request' + with: + fetch-depth: 2 + persist-credentials: false + - id: scope + name: Run unless the only install change is package.json's version line + run: | + set -euo pipefail + if [ "$GITHUB_EVENT_NAME" != pull_request ]; then + echo 'install=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + pkg_version_only=true + if git diff -U0 --no-renames HEAD^1 HEAD -- package.json | grep -E '^[+-]' | grep -vE '^(\+\+\+|---) ' \ + | grep -vE '^[+-] "version": "[^"]*",?$' > /dev/null; then + pkg_version_only=false + fi + git diff --name-only --no-renames HEAD^1 HEAD | while IFS= read -r file; do + if [ "$file" = package.json ] && [ "$pkg_version_only" = true ]; then continue; fi + while IFS= read -r glob; do + # shellcheck disable=SC2254 + case "$file" in ${glob//\*\*/*}) echo "$file"; break ;; esac + done <<'PATHS' + package.json + bun.lock + scripts/build.sh + scripts/build-cso.sh + scripts/build-cso-windows.ps1 + scripts/write-version-files.sh + setup + setup.ps1 + test/setup-ps1.test.ts + docs/troubleshooting.md + browse/src/cli.ts + browse/src/find-browse.ts + browse/src/server.ts + browse/src/meta-commands.ts + browse/scripts/build-node-server.sh + design/src/cli.ts + make-pdf/src/cli.ts + bin/gstack-global-discover.ts + bin/gstack-paths + bin/gstack-doctor + bin/gstack-launch-probe.sh + bin/gstack-hook-check + bin/gstack-session-update + bin/gstack-ff-update + bin/gstack-claude-root.sh + bin/gstack-browser-ensure + bin/gstack-path-spelling.sh + bin/gstack-config + bin/gstack-patch-names + bin/gstack-state-root.sh + bin/gstack-skill-name.sh + bin/gstack-install-registry.sh + bin/gstack-render-claude.sh + bin/gstack-host-renders.sh + bin/gstack-migrate-claude-code + bin/gstack-settings-hook + bin/gstack-relink + bin/gstack-harness + scripts/preflight-codex-overlap.ts + lib/path-spelling.ts + hosts/claude/hooks/** + gstack-upgrade/** + .github/workflows/windows-setup-e2e.yml + PATHS + done > "$RUNNER_TEMP/install-files" + if [ -s "$RUNNER_TEMP/install-files" ]; then + echo 'install=true' >> "$GITHUB_OUTPUT" + { echo 'Install files changed, so the Windows jobs run:'; sed 's/^/- /' "$RUNNER_TEMP/install-files"; } >> "$GITHUB_STEP_SUMMARY" + else + echo 'install=false' >> "$GITHUB_OUTPUT" + echo "The only install change is package.json's version line (the merge-train stamp), so the Windows jobs do not run." >> "$GITHUB_STEP_SUMMARY" + fi + windows-setup: + needs: windows-scope + if: needs.windows-scope.outputs.install == 'true' runs-on: windows-latest timeout-minutes: 35 @@ -377,6 +484,8 @@ jobs: # docs/troubleshooting.md, against the runner's real ~/.claude install # path ($HOME in PowerShell). GSTACK_E2E_POWERSHELL and # GSTACK_E2E_UPDATER_PATH_PREFIX exist only to run these steps off Windows. + needs: windows-scope + if: needs.windows-scope.outputs.install == 'true' runs-on: windows-latest timeout-minutes: 50 env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9332270ac..35d5f9f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # Changelog +## [1.94.11.0] - 2026-11-03 + +**A declared placeholder restamped to the new version is a stamp, and a carried receipt names the head's version (GST-1026, GST-1030).** gbrain writes a PR's own BEHAVIOR_CHANGES row as `since: 'TBD'` and its `release:restamp` fills in the version. Finish refused that receipt ("behavior, tests or build inputs changed since review"), and the receipt carry and the tree receipt would have refused the same row (gbrain #8444). Separately, a receipt carried after a main merge and a restamp kept the version of the head it was written at (GGST-1). + +### Added +- **`stamp_placeholders` policy key** (default `[]`, so nothing changes for a repo that does not set it): literal placeholders such as `["TBD"]`, at most 16, one line and 32 characters each, never `{version}` and never a version. It needs `min_gstack_version` at the release that introduced it. + +### Fixed +- **`lib/stamp-only.ts`:** `stampOnlyLine` takes the policy's placeholders. On a line the branch added itself (named by its position), a placeholder inside a `stamp_patterns` template may become that template at the new version, alongside any stamp-form version move on the same line. The built-in forms, a bare `{version}` template, base lines (including one whose text duplicates a branch line) and undeclared placeholders are still refused. Finish's `stampedFiles`, the receipt carry and the tree receipt share it. +- **`bin/gstack-ship-receipt` carry:** the carried receipt's `version` is HEAD's, read the way `write` reads it, instead of the version copied from the old receipt. A head with no version source keeps the version `write --version` recorded. +- **`bin/gstack-ship-receipt` carry, pin-bump PRs:** a `package.json` the branch changed as product (a dependency pin) that both main and a restamp moved since review is compared with only its `version` field masked, at all four sides. The receipt carries when nothing else in the file moved, and still refuses when the pin or any other field changed (GBEV-3, gbrain-evals #306). + +### Tests +- `test/tree-receipt-stamp-sensitive.test.ts`, `test/gstack-ship-receipt-docs.test.ts`: the shared predicate and the carry classifier accept gbrain's `since: 'TBD'` → `since: '0.60.553.0'` row and refuse an undeclared placeholder, a base row, a duplicated base copy, a built-in form and a line no template matches. +- `test/ship-rereview-triggers.test.ts`: finish routes the declared row as metadata and stops on the same controls. +- `test/restamp-mentions.test.ts`: the tree receipt and the carry accept the declared row and refuse it undeclared or on a base row; the carried receipt records the head's version after a main merge and a restamp; a version-only restamp of an EVIDENCE.md declared in `stamp_files` carries, and an undeclared one stays product; a pin-bump PR's receipt carries after a main merge and a restamp, and refuses when the pin moved since review. +- `test/gstack-ship-policy.test.ts`: the key's default, floor and validation. + +The EVIDENCE.md refusal (GST-1030's second point) reproduces on main and is already fixed by the GST-980 own-row carry this branch builds on, so this change adds only the test for it. + +## [1.94.10.0] - 2026-11-03 + +**Windows checks now run by path.** Windows Setup E2E, the 10-to-25-minute install test, no longer starts its Windows jobs on a pull request whose only install change is `package.json`'s version line, which the merge-train stamp puts on every pull request before it merges. Of the 61 pull requests in its last 100 runs (as of 2026-11-03), 37 changed no install file besides `package.json`. Its path list also gains `bun.lock` and the `bin/` scripts that the test or `./setup` runs. A new `Windows Scoped Tests` workflow runs the Windows-safe test files a pull request changed, on one Windows job, when the pull request touches path, file-name or process code, such as a hook script or a new `spawnSync` call. On the last 120 commits to main (through 923fc0a) it would have run on 50, with a median of one test file. It catches both Windows breaks the plan names: /freeze's path check (#3758) and a test that started Bun with `--config=/dev/null` (#3829). A red run blocks its pull request like any failed check, so this release also fixes #3829's test, which has failed every nightly Windows run since it landed. `windows-smoke` still runs on every pull request, and the full Windows suite still runs nightly. + +### CI +- `.github/workflows/windows-setup-e2e.yml`: a Linux `windows-scope` job runs first and skips both Windows jobs when the only install change is `package.json`'s `"version"` line. Paths add `bun.lock`, `bin/gstack-config`, `bin/gstack-patch-names` and the scripts `./setup` sources or runs. +- `.github/workflows/windows-scoped-tests.yml` (new): a Linux scope job reads the diff's file names and changed code lines, picks the changed files in the curated Windows-safe subset, and a `windows-latest` job runs them. The `windows-scoped-tests` check passes when nothing is in scope, so a skipped run still reports success. A `workflow_dispatch` with a `files` input reruns chosen Windows-safe files on a branch. + +### Tests +- `test/review-risk-linear.test.ts`: the rules-hash case starts Bun with `bunIsolationArgs()` (`--config=NUL` on native Windows) instead of `--config=/dev/null`, which native Windows Bun cannot open (nightly Windows Free Tests run 41420490350, EINVAL). +- `scripts/test-pr-profile.ts`: the new workflow is listed as free-lane only beside the other Windows workflows (test/free-fixtures.test.ts requires every workflow to be classified). +- `test/windows-path-scope-workflow.test.ts`: runs both scope steps in a two-commit git repository. The filter catches #3758's and #3829's changed files and passes over a docs-and-stamp change and a pure-logic change. The Setup E2E scope skips a version-only `package.json` bump and runs on a dependency change. `windows-smoke` stays unfiltered, and no job that can skip is a required check. + +## [1.94.9.0] - 2026-11-03 + +**A pull request can clear its remote gate obligation from a run with named flaky failures, once the owner grants it (GST-1096).** A timing flake in a test file the pull request never touched used to keep the pregate red with no way through but another paid run. `gstack-pregate clear --remote --run --flaky-exception "::" --pr --grant ` now accepts such a run when every failure is named from the run's own shard results and logs, at most three cases, each file outside the pull request's diff and its import closure, none paid and none a security scan test, and a gstack-approvals row in the owner's words binds the repository, PR, head, tree, lane, run attempt and cases. It covers the pregate only, and a red required CI check still needs its own green run. + +### Added +- **`--flaky-exception`, `--pr` and `--grant` on `gstack-pregate clear`** (`lib/pregate/flaky-exception.ts`). Every job of the run attempt is reconciled (only test-shard jobs and their aggregate may fail), every planned shard needs a complete result, and each failed shard's log must name no failure its result leaves out. Without `--grant` the clear checks everything and prints the exact `gstack-approvals add` command, whose scope is a digest of everything the grant binds, so a grant can never be moved to another head or case list. +- **`pass-with-exception`.** pregate.json records `exceptions`; `gstack-pregate verify` re-checks the binding offline and prints `PREGATE: pass-with-exception (; grant )`; `gstack-ship-queue ready` accepts it and prints a `FLAKY_EXCEPTION:` line; the ship receipt records `flaky_exception`. +- **docs/pregate.md#flaky-exception**, including why `base_red` is not used: it proves a failure by reproducing it on the base, and a timing flake passes there. + +### Tests +- `test/pregate-flaky-exception.test.ts`: a red run with one named flake clears once the bound grant exists, and verify, ready and the receipt report it; refusals for an unnamed or extra failure, a case that did not fail, more than three cases, an unattributed failure, a failed non-shard job, a missing result or log, a timed-out shard, a log failure the result omits, a file in the diff or its import closure, a paid or security test, another head, another PR head, a newer run, a run from another repository or fork, a stale or unfinished run, a moved attempt (also after the inventory runs), and a grant with another scope, outcome or speaker; ready still refuses red CI, and refuses an exception granted for another PR or head. The printed grant command quotes case names for the shell. These tests failed before the change. + +## [1.94.8.0] - 2026-11-03 + +**`gstack-approvals render` no longer puts the owner's exact words, autoplan run ids or plan hashes into PR bodies on public repositories (GST-1160).** On a public repo, /ship pasted the full `## Approvals` table into the PR description. That table carries the owner's verbatim quote, the run id and the plan sha256, and GitHub keeps every earlier version of a description in its edit history, which no API can delete. + +### Fixed +- **`bin/gstack-approvals render`** asks gh for the visibility of the repository gh picks by default (`GH_REPO`, its default repo, an `upstream` remote) and of the `origin` repository the PR is opened against. When every one of them is private (or internal), it prints the full table as before. When any is public, or gh can't tell, it prints only `Approved: yes|no (record r-<12 hex>)`. The record id is a hash of the row ids, so the Captain can match it to the private record without anything identifying being published. A stderr line, `APPROVALS_RENDER: redacted (repository visibility …)`, says which mode was used. +- **`--full`** prints the full table when gh can't read the visibility and you know the repository is private. It never prints the full table for a repository GitHub reports as public. `gstack-approvals list` still shows the full record. +- **/ship Step 19** (`ship/sections/capy-pr.md`) says a public repository gets only the one line, and never to pass `--full` for a PR body. + +## [1.94.7.0] - 2026-11-03 + +### Fixed + +- **/autoplan no longer stalls after a review phase when its reviewers share your machine (GST-1161).** A session installed from the skill's own folder gets an install verdict with no project, and its `GSTACK_LAUNCH` line points admission at that file. When a reviewer on the same machine ran `install.sh --for review-voice` from the project, it wrote a review-voice verdict keyed by that project. Admission read the project's file first, so the next `gstack-autoplan submit` refused with INSTALL_UNVERIFIED ("certified for=review-voice") until `install.sh --for autoplan` was run again. Both admission readers (`lib/install-verdict.ts` and `bin/gstack-install-verdict.sh`) now skip a review-voice verdict at the project path when the session's launched `GSTACK_INSTALL_VERDICT` exists, since a review-voice verdict can't admit any workflow. With no launched verdict, the review-voice refusal and its fix line are unchanged. `scripts/install.sh` and its user-scope copy don't change. + +## [1.94.6.0] - 2026-11-03 + +Two opt-in Claude Code hooks so a model actually runs gstack's skills, both off by default (GST-1088). `./setup --skill-routing` adds a UserPromptSubmit hook that names the gstack skill a prompt fits, scored against each installed skill's `triggers:` with no model call. `./setup --skill-contracts` adds a Stop hook that blocks once when a skill with a `contract:` block ends without its required outputs (a report with real values, a test after the last edit, the specialist and adversarial dispatches). `gstack-skill-route --explain` shows how a prompt scores; `gstack-skill-contract check|report|waive` checks a transcript, lists misses and acknowledges a waiver. Turn either off with `gstack-config set skill_routing off` or `gstack-config set skill_contracts off`. Docs: docs/skill-routing-and-contracts.md. + +## [1.94.5.0] - 2026-11-03 + +**Security: the lean /autoplan reviewer no longer gets `.envrc`, `.env-production` or a key pasted into an ordinary file (GST-1164, #3914).** Since v1.93.36.0 (#3897), the lean run puts the branch's draft code into a prompt that goes to an outside model provider. It left out only some secret-named untracked files: `.env` and `.env.*`, but not `.envrc` or `.env-production`. It applied no name rule to tracked files and no content check at all. So a direnv `.envrc`, a `.env-production`, a committed or edited secret file, or a key sitting in any source file could reach the provider. + +### Fixed +- **`lib/autoplan-lean.ts` `captureDraft`:** the secret-name rule now covers the whole `.env*` family, `.envrc` included, every `*credentials*` name, `_netrc`, `.pgpass`, `*.jks` and `id_*_sk` keys. Matching files are never diffed, tracked or untracked, and the draft names each one it left out. A secret-named file that git sees renamed or copied on the branch stays out under both names, even when the new name looks ordinary. A secret moved without `git add`, copied unchanged, or renamed and heavily edited still reaches the draft under its new name, guarded only by the content check. Every other file's patch goes through gstack's redact engine (`redactFindingSpans`), so a key in an ordinary file reaches the reviewer as ``. A patch the engine can't redact safely, including one over 1 MiB, is left out with a note. + +### Tests +- `test/autoplan-lean.test.ts`: an untracked `.envrc`, `.env-production` and six other secret files stay out of `draft.diff` and the prompt; a committed and edited `.envrc` stays out of the tracked diff, and so do an `.envrc` renamed to `.envrc.local` and a `.pgpass` renamed to `pg.txt`; a fake key in an untracked file and in a working-tree edit to a tracked file comes out redacted. All four fail on main at b42ad1e. + +Thanks to the /ship pre-landing review of #3901, which filed the finding. + ## [1.94.4.0] - 2026-11-03 - The captain skill, the CREW template, the lane brief, docs/captain.md and the Capy gstack skill now follow R65 instead of telling a lane that every fix needs a one-round, eng-only plan. A bug fix is exempt from plan review (`gstack-autoplan next --kind fix`; the brief's reason is `bug fix (R65)`), and its PR carries a repro on current main or a failing test written first. A feature gets the lean run (`--kind feature`): one reviewer reading the plan and the draft code together. The S1 and audited-problem-list exemptions are unchanged. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c145b637..3df1c01ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,7 +193,8 @@ the new defaults. | 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 from CI, the default on gstack ([CI carry](docs/pregate.md#ci-carry)) | push, `gstack-ci-carry wait --base main`, then `gstack-pregate --allow-repo-commands` (a branch behind main first runs `gh workflow run free-tests.yml --ref `) | Free; reuses the green Free Tests run at your pushed tree | Pushed branch, `gh` | | Clear pregate's test tier on a machine under 8 cores when no CI run covers your tree ([remote gate](docs/pregate.md#remote-gate), opt-in) | `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, nightly on main; PRs run the shorter `windows-smoke` check) | 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, and a PR that touches path, file-name or process code also runs its changed Windows-safe test files in `Windows Scoped Tests`) | Free | Windows, Git Bash | +| Rerun chosen Windows-safe test files on Windows CI | `gh workflow run windows-scoped-tests.yml --ref -f files=' '` (pick files from `bun run test:windows --list`) | Free CI runners | Pushed branch, `gh` with workflow rights | | 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/VERSION b/VERSION index 5cd01ae7d..693802e83 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.94.4.0 +1.94.11.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index e0bdb5e62..8606681ff 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.94.4.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.94.11.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/bin/gstack-approvals b/bin/gstack-approvals index 3d3d5a9ef..79bd5e21e 100755 --- a/bin/gstack-approvals +++ b/bin/gstack-approvals @@ -6,7 +6,7 @@ * [--pending ] [--outcome approved|rejected|pending|standing] * [--date YYYY-MM-DD] [--source ] [--branch ] [--json] * gstack-approvals import --autoplan [--by ] [--branch | --stack ] [--json] - * gstack-approvals render [--no-heading] [--branch ] + * gstack-approvals render [--no-heading] [--full] [--branch ] * gstack-approvals list [--json] [--branch ] * gstack-approvals sync [--branch ] * gstack-approvals path [--branch ] @@ -20,7 +20,7 @@ */ import * as path from 'node:path'; import { EXIT, exitTableHelp } from '../lib/headless-artifacts'; -import { appendRows, branchExists, buildRow, importAutoplan, locate, readRecord, renderRecord, syncRecord, type RecordError, type StoreLocation } from '../lib/approval-record'; +import { appendRows, branchExists, buildRow, importAutoplan, locate, readRecord, renderMode, renderRecord, repoVisibility, syncRecord, type RecordError, type StoreLocation } from '../lib/approval-record'; import { resultLine } from '../lib/result-codes'; const HELP = `gstack-approvals — the per-thread approval record (capy/skill/gstack/captain.md) @@ -29,7 +29,9 @@ const HELP = `gstack-approvals — the per-thread approval record (capy/skill/gs [--pending ] [--outcome approved|rejected|pending|standing] [--date YYYY-MM-DD] [--source ] import --autoplan [--by ] [--stack ] --stack writes the same rows to every branch of a stack; each branch is checked first, and one unknown branch writes none - render [--no-heading] the ## Approvals section for a PR body or report + render [--no-heading] [--full] the ## Approvals section for a PR body or report; a public repository, or one whose + visibility gh cannot read, gets only "Approved: yes|no (record )", because a PR description keeps + its edit history; --full prints the rows when gh cannot read the visibility and you know the repo is private list [--json] sync re-mirror the record (GSTACK_APPROVALS_DIR, the Captain's drive approvals/, or the project-user Drive) path print the record and mirror paths, and which rule chose the mirror @@ -52,7 +54,7 @@ function parse(args: string[]): { flags: Record; bools: Set. It is # the one record the bun shim, lib/pregate/runtime.ts and admission read. # Readers fall back to the schema 1 file keyed by the checkout alone, -# /install-verdicts/.json. +# /install-verdicts/.json. An install +# profile's verdict (for=review-voice) at the keyed path yields to the launched +# GSTACK_INSTALL_VERDICT, so a reviewer's install never shadows it (GST-1161). # # Admission (gstack-skill-start, after SESSION_KIND): an unattended session, # or an explicit GSTACK_SESSION_KIND=spawned session with GSTACK_CAPY=1, @@ -129,7 +131,10 @@ gstack_install_admission() { src=$(gstack_source_root "$bin") || src="" project=$(gstack_install_project "$src") file=$(gstack_install_verdict_path "${_GH:-$HOME/.gstack}" "$src" "$project") - [ -f "$file" ] || [ -z "${GSTACK_INSTALL_VERDICT:-}" ] || file="$GSTACK_INSTALL_VERDICT" + case "$(gstack_install_verdict_field "$file" schema_version 2>/dev/null)" in + 1|2) if gstack_doctor_install_profile "$(gstack_install_verdict_field "$file" for)" && [ -f "${GSTACK_INSTALL_VERDICT:-}" ]; then file="$GSTACK_INSTALL_VERDICT"; fi ;; + esac + [ -f "$file" ] || [ ! -f "${GSTACK_INSTALL_VERDICT:-}" ] || file="$GSTACK_INSTALL_VERDICT" [ -f "$file" ] || file=$(gstack_install_verdict_legacy_path "${_GH:-$HOME/.gstack}" "$src") code=INSTALL_UNVERIFIED fix="run the Capy skill installer (scripts/install.sh --for $skill) on this machine, then launch with the GSTACK_LAUNCH line it prints" diff --git a/bin/gstack-pregate b/bin/gstack-pregate index 3ea31c0bd..c14279033 100755 --- a/bin/gstack-pregate +++ b/bin/gstack-pregate @@ -7,6 +7,7 @@ * [--timeout ] [--stall-timeout ] [--remote --spend-cap ] [--run ] [--from-run ] [--no-ci-carry] [--explain] [--json] * gstack-pregate verify [--out ] [--json] publication check: current tree, no fail/incomplete, obligations cleared * gstack-pregate clear --remote --run-url | --run [--run …] [--allow-repo-commands] | --bundle-lane