Files
gstack/bin/gstack-autoplan-timing
T
Garry Tanandcapy-ai[bot] 4c6fa89cdc v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1) (#3312)
* security: isolate every gstack Bun entry point from the audited checkout's .env and bunfig.toml

Bun auto-loads .env, .env.local and .env.<NODE_ENV> from its cwd and runs a
cwd bunfig.toml preload before the program starts. gstack runs inside the repo
it audits, so a committed .env could steer state roots, the claude binary or
spend caps, and a bunfig preload ran arbitrary code in every bin and hook.

Every bun shebang, shell and skill caller, TypeScript spawn and compiled build
now disables both (--no-env-file --config=/dev/null, bunIsolationArgs(),
--no-compile-autoload-dotenv/bunfig), with a ratchet and hostile-fixture
regressions.

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

* test: name the directly executed scripts in the isolation ratchet

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

* test: follow the isolation flags in touchfiles, copy lists, recipe assertions and the harness pin

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

* fix: keep browse/src/bun-child-config.ts unchanged so the native cookie qualification still binds

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

* fix(windows): the empty bunfig outlives its process so the timeline hook's detached repair child can read it

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

* v1.91.159.0 security: gstack's Bun entry points ignore the audited repo's .env and bunfig.toml (S1)

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

* test: plan-mode render pattern accepts the isolation flags

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>
2026-10-20 00:24:28 +00:00

99 lines
5.0 KiB
TypeScript
Executable File

#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-autoplan-timing — wall time per autoplan phase and the pre-run estimate (plan B10).
*
* gstack-autoplan-timing start --run <id> --phase <p> --out <dir>
* gstack-autoplan-timing close --run <id> --phase <p> --out <dir> [--outside-s N] [--native-s N] [--session-kind K] [--json]
* gstack-autoplan-timing estimate [--phases ceo,design,dx,eng] [--json]
* gstack-autoplan-timing summary --run <id> --out <dir> [--phases ...] [--json]
*
* Writes <out>/timing.json (lib/headless-artifacts.ts `timing` v1) and appends to
* <state root>/analytics/autoplan-timing.jsonl unless GSTACK_EPHEMERAL=1, in which
* case it prints the skip line. Query commands (estimate, summary) print no GSTACK_RESULT.
*/
import * as path from 'node:path';
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
import { TIMING_SKIPPED_LINE, analyticsPath, estimate, phaseClose, phaseStart, readTiming, renderEstimate, renderSummary, runStart } from '../lib/autoplan-timing';
const HELP = `gstack-autoplan-timing — phase wall time and the Phase 0 estimate
start --run <id> --phase <p> --out <dir> record the phase start in <out>/timing.json
close --run <id> --phase <p> --out <dir> [--outside-s N] [--native-s N] [--session-kind K] [--json]
record the end, wall time and voices; append to analytics
estimate [--run <id> --out <dir>] [--phases ...] [--json] ESTIMATE: median of the last 10 recorded runs per phase, or no history;
with --run/--out also records the run start in <out>/timing.json
summary --run <id> --out <dir> [--phases ...] [--json] CYCLE: actual whole-cycle time next to the estimate
Analytics: <state root>/analytics/autoplan-timing.jsonl; skipped with "${TIMING_SKIPPED_LINE}" when GSTACK_EPHEMERAL=1.
${exitTableHelp()}`;
function flag(args: string[], name: string): string | undefined {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
}
function usage(message: string): number {
process.stderr.write(`gstack-autoplan-timing: ${message}\n${HELP}\n`);
return EXIT.usage;
}
function seconds(args: string[], name: string): number | undefined {
const v = flag(args, name);
if (v === undefined) return undefined;
if (!/^[0-9]+$/.test(v)) throw new Error(`${name} needs a whole number of seconds`);
return Number(v);
}
function main(argv: string[]): number {
const [cmd, ...rest] = argv;
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; }
const json = rest.includes('--json');
const phases = (flag(rest, '--phases') ?? 'ceo,design,dx,eng').split(',').map(s => s.trim()).filter(Boolean);
if (cmd === 'estimate') {
const run0 = flag(rest, '--run');
const out0 = flag(rest, '--out');
if ((run0 === undefined) !== (out0 === undefined)) return usage('estimate takes --run and --out together');
if (run0 && out0) runStart(path.join(out0, 'timing.json'), run0);
const rows = estimate(analyticsPath(), phases);
process.stdout.write(json ? JSON.stringify({ phases: rows }) + '\n' : renderEstimate(rows) + '\n');
return EXIT.ok;
}
const run = flag(rest, '--run');
const out = flag(rest, '--out');
if (!run || !out) return usage(`${cmd} needs --run <id> and --out <dir>`);
const file = path.join(out, 'timing.json');
if (cmd === 'summary') {
const timing = readTiming(file, run);
const est = estimate(analyticsPath(), phases);
process.stdout.write(json ? JSON.stringify({ timing, estimate: est }) + '\n' : renderSummary(timing, est) + '\n');
return EXIT.ok;
}
const phase = flag(rest, '--phase');
if (!phase) return usage(`${cmd} needs --phase <p>`);
if (cmd === 'start') {
const entry = phaseStart(file, run, phase);
process.stdout.write(json ? JSON.stringify(entry) + '\n' : `TIMING: phase=${phase} started_at=${entry.started_at}\n`);
return EXIT.ok;
}
if (cmd === 'close') {
let outside_s: number | undefined, native_s: number | undefined;
try { outside_s = seconds(rest, '--outside-s'); native_s = seconds(rest, '--native-s'); } catch (e: any) { return usage(e.message); }
const result = phaseClose(file, run, phase, { outside_s, native_s, session_kind: flag(rest, '--session-kind') });
if (result.analytics === 'skipped_ephemeral') process.stderr.write(TIMING_SKIPPED_LINE + '\n');
process.stdout.write(json
? JSON.stringify(result) + '\n'
: `TIMING: phase=${phase} wall_s=${result.entry.wall_s}${outside_s !== undefined ? ` outside_s=${outside_s}` : ''}${native_s !== undefined ? ` native_s=${native_s}` : ''} total_wall_s=${result.total_wall_s} analytics=${result.analytics}\n`);
return EXIT.ok;
}
return usage(`unknown command ${cmd}`);
}
try {
process.exitCode = main(process.argv.slice(2));
} catch (e: any) {
process.stderr.write(`gstack-autoplan-timing: ${e.message}\n`);
process.exitCode = EXIT.fail;
}