Files
gstack/lib/autoplan-admission.ts
Garry Tan 47d639abff v1.91.101.0 fix: close the field wave's residual gaps (#3173)
The field wave's residual gaps are closed. v1.91.96.0 through v1.91.100.0 
shipped the install-honesty and unattended-`/autoplan` wave 
(`docs/designs/GSTACK_FIELD_WAVE_2026_10_12.md`); running that release on a 
Capy machine left six gaps, each reproduced here with a test that failed 
first.
2026-10-14 17:36:39 +00:00

41 lines
2.3 KiB
TypeScript

/**
* autoplan-admission — who may run `gstack-autoplan`'s mutating verbs. The
* decision is lib/install-verdict.ts `checkInstallAdmission` (the same one
* `gstack-skill-start` makes): on a Capy host (GSTACK_CAPY=1) the install
* verdict the Capy wrapper wrote must pass for this checkout, HEAD and
* workflow; any unattended session also needs the PATH Bun (the one every
* `#!/usr/bin/env bun` child runs) at gstack's supported minimum. GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 admits anyway and the
* caller records the returned deviation. Interactive sessions, and other
* hosts' ambient spawned sessions (a warning line), are not refused.
*/
import { spawnSync } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { RunError } from './autoplan-state';
import { checkInstallAdmission } from './install-verdict';
const ROOT = path.resolve(import.meta.dir, '..');
export interface Admission { lines: string[]; deviation?: string }
/**
* A workflow lease on the shared installation (bin/gstack-lease): the upgrader
* refuses UPGRADE_BUSY while a live lease it does not own exists, so a
* session-start fast-forward cannot move the checkout under a running
* autoplan driver. Fail-open: a missing or failing bin never blocks the run.
*/
export function touchInstallLease(env: NodeJS.ProcessEnv = process.env): void {
const bin = path.join(ROOT, 'bin', 'gstack-lease');
if (process.platform === 'win32' || !fs.existsSync(bin)) return;
try { spawnSync(bin, ['touch', '--root', ROOT, '--from', String(process.ppid)], { encoding: 'utf8', timeout: 10_000, env, stdio: 'ignore' }); } catch { /* fail-open */ }
}
/** Throws RunError (exit 3) unless the session may write; an override or a warning returns the line to print. */
export function admit(env: NodeJS.ProcessEnv = process.env): Admission {
const bunVersion = spawnSync('bun', ['--version'], { encoding: 'utf8', timeout: 30_000, env }).stdout?.trim() || '0.0.0';
const a = checkInstallAdmission({ skill: 'autoplan', gstackRoot: ROOT, env, bunVersion });
if (a.overridden) return { lines: [`ADMISSION: override GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 (${a.detail})`], deviation: `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1: admitted despite ${a.detail}` };
if (a.refuse) throw new RunError(a.code ?? 'INSTALL_UNVERIFIED', a.detail, 3);
return { lines: a.warn ? [a.line] : [] };
}