v1.92.19.0 Skip the Aside probe off macOS so Linux and Windows render through gstack's own browser (#2864) (#3734)

* aside-render: probeAside answers NEEDS_ASIDE off macOS without spawning (#2864)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: off-macOS probe detail no longer claims gstack's browser is in use

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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-01 07:56:05 +00:00
1 parent 93d73bf755
commit dcc1c51bfc
7 files changed
+50 -9

No files matched your search

+9
View File
@@ -1,5 +1,14 @@
# Changelog
## [1.92.19.0] - 2026-11-01
**On Linux and Windows the renderer goes straight to gstack's own browser instead of asking for Aside (#2864).** `probeAside()` in `lib/aside-render.ts`, used by /make-pdf, /diagram and `gstack-render`, used to run `aside --version` on any platform. When that failed it told the user to install a browser that only ships for macOS. Off macOS it now answers `NEEDS_ASIDE` with "Aside is macOS-only (this is linux)", without starting a process, and the renderer moves on to gstack's own browser. macOS behavior and `GSTACK_SKIP_ASIDE=1` are unchanged.
### Changed
- **`probeAside(timeoutMs, platform)`** takes the platform, which defaults to `process.platform`. The fake-CLI probe tests pass `darwin`, so they still run on Linux CI.
Thanks to @furkankoykiran for the report. The bash readiness block inside browsing skills already prints `NEEDS_ASIDE: Linux` and is unchanged.
## [1.92.18.0] - 2026-11-01
- `install.sh --project` (through `gstack-capy-install`) resolves the directory it names to its git toplevel, which is where pregate admission looks for the install verdict. `--project <repo>/<subdir>` now writes, and reads pins from, the same place as `--project <repo>`, so a Bun pin declared only in a subpackage no longer applies. A value that isn't a directory, such as a repo slug or a directory this shell can't enter, is a usage error that asks for the checkout's path, and it stops before anything is cloned or installed. Before this change, a slug or a subdirectory keyed the verdict somewhere admission never looks, so a later pregate admission check read a stale verdict (GST-743).
+1 -1
View File
@@ -1 +1 @@
1.92.18.0
1.92.19.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.92.18.0 — regenerate/re-copy after upgrading gstack
# gstack digest v1.92.19.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,
+5 -2
View File
@@ -76,11 +76,14 @@ export type AsideProbe =
| { ok: true; version: string }
| { ok: false; reason: 'NEEDS_ASIDE' | 'ASIDE_NOT_RUNNING'; detail: string };
/** Same probe the skills run in BROWSER SETUP: binary present, app answering. */
export function probeAside(timeoutMs = 30_000): AsideProbe {
/** Same probe the skills run in BROWSER SETUP: binary present, app answering. Aside ships only for macOS, so any other platform answers NEEDS_ASIDE without spawning. */
export function probeAside(timeoutMs = 30_000, platform: NodeJS.Platform = process.platform): AsideProbe {
if (process.env.GSTACK_SKIP_ASIDE === '1') {
return { ok: false, reason: 'NEEDS_ASIDE', detail: 'GSTACK_SKIP_ASIDE=1 — Aside skipped by request' };
}
if (platform !== 'darwin') {
return { ok: false, reason: 'NEEDS_ASIDE', detail: `Aside is macOS-only (this is ${platform})` };
}
const which = spawnSync('aside', ['--version'], { encoding: 'utf8', timeout: 10_000 });
if (which.error) {
return { ok: false, reason: 'NEEDS_ASIDE', detail: 'the `aside` CLI is not on PATH — install the Aside browser (macOS 15+) from aside.com' };
+13 -1
View File
@@ -36,6 +36,7 @@ const ROOT = path.resolve(import.meta.dir, "..", "..");
const CLI = path.join(ROOT, "make-pdf", "src", "cli.ts");
const TYPES = path.join(ROOT, "make-pdf", "src", "types.ts");
const isWin = process.platform === "win32";
const isMac = process.platform === "darwin";
let tmp: string;
/** PATH for children: nothing on it (no `aside`, no `browse`, no `pdftotext`). */
@@ -316,7 +317,7 @@ describe.skipIf(isWin)("cli.ts through fake browsers (GSTACK_BROWSE_BIN + a scru
expect(fs.existsSync(out)).toBe(false);
});
test("Aside answers the probe but cannot run the script: the PDF comes from gstack's browser, stderr says so, exit 0", () => {
test.skipIf(!isMac)("Aside answers the probe but cannot run the script: the PDF comes from gstack's browser, stderr says so, exit 0", () => {
const out = path.join(tmp, "mid-run", "out.pdf");
const r = runCli(["generate", inputMd, out, "--quiet"], {
env: childEnv({ PATH: fakeAsideBin, GSTACK_SKIP_ASIDE: undefined, GSTACK_BROWSE_BIN: browseOk }),
@@ -326,4 +327,15 @@ describe.skipIf(isWin)("cli.ts through fake browsers (GSTACK_BROWSE_BIN + a scru
expect(r.stderr).toContain("Aside was unavailable mid-run; the PDF was rendered through gstack's own browser.");
expect(fs.readFileSync(out, "utf8")).toBe("%PDF-1.4 fake\n");
});
test.skipIf(isMac)("off macOS an `aside` on PATH is never asked: the PDF comes straight from gstack's browser, --quiet keeps stderr empty, exit 0 (#2864)", () => {
const out = path.join(tmp, "off-mac", "out.pdf");
const r = runCli(["generate", inputMd, out, "--quiet"], {
env: childEnv({ PATH: fakeAsideBin, GSTACK_SKIP_ASIDE: undefined, GSTACK_BROWSE_BIN: browseOk }),
});
expect(r.code).toBe(ExitCode.Success);
expect(r.stdout).toBe(`${out}\n`);
expect(r.stderr).toBe("");
expect(fs.readFileSync(out, "utf8")).toBe("%PDF-1.4 fake\n");
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "1.92.18",
"version": "1.92.19",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
+20 -3
View File
@@ -350,7 +350,7 @@ function writeDriver(dir: string): string {
fs.writeFileSync(driver, `const M = await import(${JSON.stringify(LIB)});
const job = JSON.parse(process.argv[2]);
let out;
if (job.fn === 'probeAside') out = M.probeAside(job.timeoutMs);
if (job.fn === 'probeAside') out = M.probeAside(job.timeoutMs, job.platform);
else if (job.fn === 'renderWithAside') out = await M.renderWithAside(job.spec);
else if (job.fn === 'render') {
if (job.primeAside) M.pickEngine(true, { probe: () => ({ ok: true, version: 'fake-aside' }) });
@@ -383,9 +383,9 @@ describe.skipIf(!HERMETIC)('aside-render: probeAside classifies a fake CLI the w
let driver: string;
beforeAll(() => { fs.mkdirSync(bin); driver = writeDriver(tmp); });
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
const probe = (env?: Record<string, string>): AsideProbe => {
const probe = (env?: Record<string, string>, platform = 'darwin'): AsideProbe => {
fs.rmSync(log, { force: true });
return runDriver<AsideProbe>(driver, { fn: 'probeAside', timeoutMs: 5_000 }, { binDir: bin, env });
return runDriver<AsideProbe>(driver, { fn: 'probeAside', timeoutMs: 5_000, platform }, { binDir: bin, env });
};
test('no `aside` on PATH → NEEDS_ASIDE (install it), never "not running"', () => {
@@ -442,6 +442,23 @@ describe.skipIf(!HERMETIC)('aside-render: probeAside classifies a fake CLI the w
expect(r.detail).toContain('GSTACK_SKIP_ASIDE=1');
expect(fs.existsSync(log)).toBe(false);
});
test.each(['linux', 'win32'])('on %s → NEEDS_ASIDE naming the platform, and the CLI is never invoked, even when one is on PATH (#2864)', platform => {
writeFakeAside(bin, { repl: 'echo "ASIDE_READY /x"', log });
const r = probe(undefined, platform);
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.reason).toBe('NEEDS_ASIDE');
expect(r.detail).toBe(`Aside is macOS-only (this is ${platform})`);
expect(fs.existsSync(log)).toBe(false);
});
test('GSTACK_SKIP_ASIDE=1 keeps precedence over the platform check', () => {
const r = probe({ GSTACK_SKIP_ASIDE: '1' }, 'linux');
expect(r.ok).toBe(false);
if (r.ok) return;
expect(r.detail).toContain('GSTACK_SKIP_ASIDE=1');
});
});
describe.skipIf(!HERMETIC)('aside-render: serveDir — loopback server contract (nonce, containment, no listings)', () => {