From f5cb6ec1eb3ea25b579e022dfd735d7b452f6bb5 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 1 Nov 2026 19:46:04 -0800 Subject: [PATCH] v1.92.56.0 fix(windows): gstack-ship-receipt starts gstack-session-kind through Git Bash (#3541) (#3823) * fix(windows): gstack-ship-receipt starts gstack-session-kind through Git Bash (#3541) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(windows): the #3541 ship-receipt session_kind test runs in the Windows lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: drop bin/gstack-ship-receipt from the tripwire's KNOWN_SITES now that its spawn goes through runScriptSync Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 10 +++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bin/gstack-ship-receipt | 3 +- package.json | 2 +- scripts/lib/windows-curation.ts | 1 + test/script-invocation-tripwire.test.ts | 2 - test/ship-receipt-session-kind.test.ts | 56 +++++++++++++++++++++++++ 8 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 test/ship-receipt-session-kind.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 453ca5230..1a662182b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 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. diff --git a/VERSION b/VERSION index 837153bbc..dd192841b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.92.55.0 +1.92.56.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index c7c35d1f8..e8c563d3f 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.92.55.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/bin/gstack-ship-receipt b/bin/gstack-ship-receipt index b2d902b65..3de38d032 100755 --- a/bin/gstack-ship-receipt +++ b/bin/gstack-ship-receipt @@ -37,6 +37,7 @@ import * as path from 'node:path'; import { EXIT, exitTableHelp, type ShipReceipt } from '../lib/headless-artifacts'; import { resultLine, resultLineWithDocs } from '../lib/result-codes'; import { versionAtRef } from '../lib/restamp'; +import { runScriptSync } from '../lib/script-invocation'; import { readPregateFile, verifyPregate } from '../lib/pregate/index'; import { isUnavailable, loadPolicyFromBase, matchesAny, policyUnavailableLine, resolveBaseBranch, runningGstackVersion, stampFilesInTree, validatePolicy, type ShipPolicy } from '../lib/ship-policy'; import { approvalForReceipt } from '../lib/approval-citations'; @@ -179,7 +180,7 @@ function cmdWrite(rest: string[], json: boolean): number { const tree = receiptFile ? JSON.parse(fs.readFileSync(receiptFile, 'utf8')) : null; let sessionKind = flag(rest, '--session-kind') ?? process.env.GSTACK_SESSION_KIND; if (!sessionKind) { - const r = spawnSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-session-kind'), [], { encoding: 'utf8', timeout: 15_000 }); + const r = runScriptSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-session-kind'), [], { encoding: 'utf8', timeout: 15_000 }); sessionKind = r.status === 0 ? r.stdout.trim() || undefined : undefined; } let pregate: PregateField | undefined; diff --git a/package.json b/package.json index 4568b4a9e..bd579db43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.92.55", + "version": "1.92.56", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/scripts/lib/windows-curation.ts b/scripts/lib/windows-curation.ts index 99b720fe7..f9f351c9f 100644 --- a/scripts/lib/windows-curation.ts +++ b/scripts/lib/windows-curation.ts @@ -411,6 +411,7 @@ export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [ { file: 'test/gstack-wtree-windows-paths.test.ts', reason: '#3101: runs bin/gstack-wtree through explicit bash in an os.tmpdir linked worktree with forward-slash paths; never executes a shebang; the drive-letter simulation is skipIf win32' }, { file: 'test/setup-hook-syntax-gate.test.ts', reason: '#3088/#3100: runs bin/gstack-hook-check and the setup hook sections through explicit bash with forward-slash paths; never executes a shebang. Its Bun drive-path spelling is the Windows bug it guards' }, { file: 'test/bun-config-shell-bins.test.ts', reason: '#3415: runs the record bins through lib/script-invocation.ts (Git Bash on Windows) with MSYS path conversion off, the case it guards; its bun and cygpath stand-ins and simulated-OSTYPE cases are skipIf win32' }, + { file: 'test/ship-receipt-session-kind.test.ts', reason: '#3541: runs gstack-ship-receipt write through lib/script-invocation.ts, which must reach the bash gstack-session-kind through Git Bash; os.tmpdir git fixture only' }, { file: 'test/path-spelling.test.ts', reason: 'sources bin/gstack-path-spelling.sh through explicit bash and checks lib/path-spelling.ts; on Windows it exercises the real Git Bash spellings' }, { file: 'test/timeline-stop-hook-stdin.test.ts', reason: '#3628: the hook runs through Bun argv with a stdin pipe held open; the return-within-budget timing must meet real Windows process startup' }, { file: 'test/state-root-parity.test.ts', reason: 'runs the bash twin and lib/state-root.ts over an env table with PATH empty; no shebang execution, raw-string comparison is platform-neutral' }, diff --git a/test/script-invocation-tripwire.test.ts b/test/script-invocation-tripwire.test.ts index f7e0a2b2d..bc16fb04c 100644 --- a/test/script-invocation-tripwire.test.ts +++ b/test/script-invocation-tripwire.test.ts @@ -34,8 +34,6 @@ const KNOWN_SITES: Record = { 'bin/gstack-issue-links': 'outside this wave: the gstack-issue-guard spawn moves to runScriptSync', // Found when the bin-constant rule was added (fix wave PR 5, item 10b). 'bin/gstack-brain-cache': 'outside this wave: the gstack-config salience read moves to runScriptSync', - // Found when the scanner learned the import.meta.url bin path (#3541). - 'bin/gstack-ship-receipt': 'the gstack-session-kind spawn moves to runScriptSync in the #3541 ship-receipt follow-up', 'lib/autoplan-admission.ts': 'returns before the spawn on win32; the bin/gstack-lease touch moves to runScriptSync with that bin\'s owner', }; diff --git a/test/ship-receipt-session-kind.test.ts b/test/ship-receipt-session-kind.test.ts new file mode 100644 index 000000000..48defdd94 --- /dev/null +++ b/test/ship-receipt-session-kind.test.ts @@ -0,0 +1,56 @@ +/** + * #3541 (the /ship half): `gstack-ship-receipt write` with no --session-kind + * and no GSTACK_SESSION_KIND asks bin/gstack-session-kind, a bash script. + * Spawned by path it never starts on Windows (no shebangs), so every receipt + * written there carried no session_kind. + * + * Windows-safe by construction: both bins launch through + * lib/script-invocation.ts (Git Bash on win32), state lives under os.tmpdir(). + */ +import { afterEach, beforeEach, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { runScriptSync } from '../lib/script-invocation'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +let tmp = '', repo = '', env: NodeJS.ProcessEnv = {}; + +beforeEach(() => { + tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-receipt-kind-'))); + repo = path.join(tmp, 'repo'); + fs.mkdirSync(repo); + const gitconfig = path.join(tmp, 'gitconfig'); + fs.writeFileSync(gitconfig, ''); + env = { + ...process.env, HOME: tmp, USERPROFILE: tmp, GSTACK_STATE_ROOT: path.join(tmp, 'state'), GSTACK_HOME: path.join(tmp, 'state'), GSTACK_SESSION_KIND: '', + GIT_CONFIG_GLOBAL: gitconfig, GIT_CONFIG_NOSYSTEM: '1', GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined, + }; + const git = (...args: string[]) => { + const r = spawnSync('git', args, { cwd: repo, env, encoding: 'utf8', timeout: 30_000 }); + expect(r.status, r.stderr).toBe(0); + }; + git('init', '-q', '-b', 'main'); + fs.writeFileSync(path.join(repo, 'a.txt'), 'a\n'); + git('add', 'a.txt'); + git('-c', 'user.email=fixture@example.test', '-c', 'user.name=Fixture', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture'); + git('update-ref', 'refs/remotes/origin/main', 'HEAD'); + git('switch', '-q', '-c', 'feat'); +}); + +afterEach(() => fs.rmSync(tmp, { recursive: true, force: true })); + +test('write without a session kind records what gstack-session-kind prints (#3541)', () => { + const kind = runScriptSync(path.join(ROOT, 'bin', 'gstack-session-kind'), [], { cwd: repo, env, encoding: 'utf8', timeout: 30_000 }); + expect(kind.status, kind.stderr).toBe(0); + const body = path.join(tmp, 'body.md'); + fs.writeFileSync(body, '## Summary\n'); + const w = runScriptSync(path.join(ROOT, 'bin', 'gstack-ship-receipt'), ['write', '--pr', '1', '--base', 'main', '--body', body, '--json'], { + cwd: repo, env, encoding: 'utf8', timeout: 120_000, + }); + expect(w.error).toBeUndefined(); + expect(w.status, w.stderr).toBe(0); + expect(JSON.parse(w.stdout).session_kind).toBe(kind.stdout.trim()); +}, 150_000);