mirror of https://github.com/garrytan/gstack.git
363 lines
13 KiB
Plaintext
Executable File
363 lines
13 KiB
Plaintext
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* gstack-grok-compat-audit — Definition of Done gate for Grok Build packaging
|
|
* + bridge honesty (plan U9 / R11).
|
|
*
|
|
* Exit 0 only when packaging checks pass for installed gstack membership
|
|
* packages and bridge gates are not theater-greened.
|
|
*
|
|
* Usage:
|
|
* bin/gstack-grok-compat-audit
|
|
* bin/gstack-grok-compat-audit --skills-dir ~/.grok/skills
|
|
* bin/gstack-grok-compat-audit --phase a # packaging only
|
|
* bin/gstack-grok-compat-audit --phase ab # packaging + bridge gates (default)
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { execFileSync } from 'child_process';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const args = process.argv.slice(2);
|
|
|
|
function arg(name: string, def?: string): string | undefined {
|
|
const idx = args.findIndex(a => a === name || a.startsWith(name + '='));
|
|
if (idx < 0) return def;
|
|
const eq = args[idx].indexOf('=');
|
|
if (eq >= 0) return args[idx].slice(eq + 1);
|
|
return args[idx + 1] ?? def;
|
|
}
|
|
|
|
const SKILLS_DIR = path.resolve(
|
|
(arg('--skills-dir') ?? path.join(os.homedir(), '.grok', 'skills')).replace(/^~/, os.homedir()),
|
|
);
|
|
const PHASE = (arg('--phase') ?? 'ab').toLowerCase();
|
|
const GSTACK_ROOT = path.join(SKILLS_DIR, 'gstack');
|
|
|
|
type Severity = 'fail' | 'warn' | 'ok';
|
|
interface Finding {
|
|
severity: Severity;
|
|
area: string;
|
|
message: string;
|
|
}
|
|
|
|
const findings: Finding[] = [];
|
|
|
|
function fail(area: string, message: string) {
|
|
findings.push({ severity: 'fail', area, message });
|
|
}
|
|
function warn(area: string, message: string) {
|
|
findings.push({ severity: 'warn', area, message });
|
|
}
|
|
function ok(area: string, message: string) {
|
|
findings.push({ severity: 'ok', area, message });
|
|
}
|
|
|
|
function exists(p: string): boolean {
|
|
try {
|
|
fs.accessSync(p);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function readText(p: string): string {
|
|
try {
|
|
return fs.readFileSync(p, 'utf-8');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function isGstackMembership(dirName: string): boolean {
|
|
if (dirName === 'gstack' || dirName === 'connect-chrome' || dirName === 'browse') return true;
|
|
if (dirName.startsWith('gstack-')) return true;
|
|
// bare aliases that point at gstack packages (symlink or real SKILL with gstack markers)
|
|
const skill = path.join(SKILLS_DIR, dirName, 'SKILL.md');
|
|
if (!exists(skill)) return false;
|
|
const body = readText(skill);
|
|
return body.includes('$GSTACK_ROOT') || body.includes('gstack') || body.includes('AUTO-GENERATED');
|
|
}
|
|
|
|
function listMembershipPackages(): string[] {
|
|
if (!exists(SKILLS_DIR)) return [];
|
|
return fs
|
|
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
.filter(d => d.isDirectory() || d.isSymbolicLink())
|
|
.map(d => d.name)
|
|
.filter(isGstackMembership)
|
|
.sort();
|
|
}
|
|
|
|
// ─── Packaging suite (Phase A) ────────────────────────────────
|
|
|
|
function auditRuntimeRoot() {
|
|
const area = 'runtime-root';
|
|
if (!exists(GSTACK_ROOT)) {
|
|
fail(area, `missing runtime root: ${GSTACK_ROOT}`);
|
|
return;
|
|
}
|
|
const requiredDirs = [
|
|
'bin',
|
|
'browse/dist',
|
|
'browse/src',
|
|
'review/specialists',
|
|
'scripts',
|
|
];
|
|
for (const rel of requiredDirs) {
|
|
const p = path.join(GSTACK_ROOT, rel);
|
|
if (!exists(p)) fail(area, `missing required asset: ${rel}`);
|
|
else ok(area, `present: ${rel}`);
|
|
}
|
|
// Conditional when monorepo had them at install — soft-check if src monorepo has them
|
|
const monorepoHas = (rel: string) => exists(path.join(ROOT, rel));
|
|
if (monorepoHas('design/dist') && !exists(path.join(GSTACK_ROOT, 'design/dist'))) {
|
|
fail(area, 'monorepo has design/dist but runtime root does not');
|
|
}
|
|
if (monorepoHas('extension') && !exists(path.join(GSTACK_ROOT, 'extension'))) {
|
|
fail(area, 'monorepo has extension/ but runtime root does not');
|
|
}
|
|
if (monorepoHas('make-pdf/dist') && !exists(path.join(GSTACK_ROOT, 'make-pdf/dist'))) {
|
|
warn(area, 'monorepo has make-pdf/dist but runtime root does not (membership optional)');
|
|
}
|
|
for (const f of ['checklist.md', 'TODOS-format.md']) {
|
|
const p = path.join(GSTACK_ROOT, 'review', f);
|
|
if (!exists(p)) fail(area, `missing review/${f}`);
|
|
else ok(area, `present: review/${f}`);
|
|
}
|
|
}
|
|
|
|
function auditNoDoubleHome() {
|
|
const area = 'no-double-home';
|
|
const pkgs = listMembershipPackages();
|
|
let hits = 0;
|
|
for (const name of pkgs) {
|
|
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
if (!exists(skill)) continue;
|
|
const body = readText(skill);
|
|
if (/\$HOME\$GSTACK/.test(body)) {
|
|
fail(area, `${name}: contains $HOME$GSTACK (double-home path bug)`);
|
|
hits++;
|
|
}
|
|
}
|
|
if (hits === 0) ok(area, `no $HOME$GSTACK in ${pkgs.length} packages`);
|
|
}
|
|
|
|
function auditNoUngeneratedMonorepoPointers() {
|
|
const area = 'no-monorepo-skill-pointer';
|
|
// Skills under ~/.grok/skills must not be bare monorepo dirs with unrewritten Claude SKILL.md
|
|
// Heuristic: ungenerated monorepo package often has MODEL_OVERLAY: claude section + ~/.claude paths without rewrite
|
|
const pkgs = listMembershipPackages();
|
|
for (const name of pkgs) {
|
|
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
if (!exists(skill)) continue;
|
|
const body = readText(skill);
|
|
// Skip runtime root
|
|
if (name === 'gstack' && !body.includes('name:')) continue;
|
|
if (body.includes('~/.claude/skills/gstack') && !body.includes('$GSTACK_ROOT')) {
|
|
// Still has unrewritten Claude global path without env var form
|
|
fail(area, `${name}: unrewritten ~/.claude/skills/gstack pointer`);
|
|
}
|
|
}
|
|
ok(area, 'membership packages scanned for unrewritten Claude monorepo pointers');
|
|
}
|
|
|
|
function auditConnectChrome() {
|
|
const area = 'connect-chrome';
|
|
const link = path.join(SKILLS_DIR, 'connect-chrome');
|
|
if (!exists(link)) {
|
|
fail(area, 'connect-chrome missing under skills dir');
|
|
return;
|
|
}
|
|
try {
|
|
const target = fs.realpathSync(link);
|
|
if (!/open-gstack-browser/.test(target) && !/gstack-open-gstack-browser/.test(target)) {
|
|
fail(area, `connect-chrome does not resolve to open-gstack-browser (got ${target})`);
|
|
} else {
|
|
ok(area, `connect-chrome → ${target}`);
|
|
}
|
|
} catch (e) {
|
|
fail(area, `connect-chrome realpath failed: ${(e as Error).message}`);
|
|
}
|
|
}
|
|
|
|
function auditHostBleedSample() {
|
|
const area = 'host-bleed';
|
|
const samples = ['gstack-ship', 'ship', 'gstack-browse', 'browse', 'gstack-claude', 'claude'];
|
|
for (const name of samples) {
|
|
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
if (!exists(skill)) continue;
|
|
const body = readText(skill);
|
|
if (/MODEL_OVERLAY:\s*claude/.test(body)) {
|
|
fail(area, `${name}: MODEL_OVERLAY: claude still present`);
|
|
}
|
|
if (/## Model-Specific Behavioral Patch \(claude\)/.test(body)) {
|
|
fail(area, `${name}: Claude model overlay section body present`);
|
|
}
|
|
// Claude skill is allowed to mention claude CLI extensively
|
|
if (name !== 'claude' && name !== 'gstack-claude') {
|
|
if (/\bAskUserQuestion\b/.test(body) && !/ask_user_question/.test(body)) {
|
|
// May still appear in examples — soft
|
|
warn(area, `${name}: raw AskUserQuestion token (prefer ask_user_question rewrite)`);
|
|
}
|
|
}
|
|
if (name === 'claude' || name === 'gstack-claude') {
|
|
if (!/MULTI_CLI_BRIDGE/.test(body) && !/Step 0: Check Claude CLI/.test(body)) {
|
|
fail(area, `${name}: missing MULTI_CLI_BRIDGE / Step 0 detect`);
|
|
} else {
|
|
ok(area, `${name}: Step 0 / multi-CLI packaging present`);
|
|
}
|
|
}
|
|
}
|
|
ok(area, 'host-bleed sample scan complete');
|
|
}
|
|
|
|
function auditSkillifySdkPath() {
|
|
const area = 'skillify-sdk';
|
|
const src = path.join(GSTACK_ROOT, 'browse', 'src', 'browse-client.ts');
|
|
if (exists(src)) ok(area, 'browse/src/browse-client.ts resolvable under $GSTACK_ROOT');
|
|
else fail(area, 'browse/src/browse-client.ts missing — skillify SDK path broken');
|
|
}
|
|
|
|
// ─── Bridge gates (Phase B) ───────────────────────────────────
|
|
|
|
function auditBridges() {
|
|
// Install prose alone must not claim primary READY when CLI missing
|
|
const area = 'bridge-policy';
|
|
|
|
// Prefer installed runtime root binary (what users run); fall back to monorepo.
|
|
const installedBench = path.join(GSTACK_ROOT, 'bin', 'gstack-model-benchmark');
|
|
const monorepoBench = path.join(ROOT, 'bin', 'gstack-model-benchmark');
|
|
const benchBin = exists(installedBench) ? installedBench : monorepoBench;
|
|
if (!exists(installedBench) && exists(monorepoBench)) {
|
|
warn(area, 'benchmark: using monorepo binary (installed runtime bin missing) — packaging incomplete?');
|
|
}
|
|
try {
|
|
// No shell: argv list avoids monorepo-path injection.
|
|
const help = execFileSync(benchBin, ['--prompt', 'x', '--models', 'grok', '--dry-run'], {
|
|
encoding: 'utf-8',
|
|
timeout: 15000,
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
if (/unknown provider.*grok/i.test(help)) {
|
|
fail(area, 'benchmark: grok still unknown provider');
|
|
} else if (/grok:\s*(OK|NOT READY)/i.test(help)) {
|
|
ok(area, `benchmark: grok provider wired via ${benchBin} (dry-run READY/NOT READY is boolean auth)`);
|
|
} else {
|
|
warn(area, `benchmark dry-run unexpected output: ${help.slice(0, 200)}`);
|
|
}
|
|
} catch (e) {
|
|
const msg = (e as { stdout?: string; message?: string }).stdout
|
|
?? (e as Error).message
|
|
?? String(e);
|
|
if (/unknown provider.*grok/i.test(msg)) fail(area, 'benchmark: grok unknown provider');
|
|
else warn(area, `benchmark dry-run error: ${String(msg).slice(0, 200)}`);
|
|
}
|
|
|
|
// spec: at least one installed package must exist and use Grok-native spawn
|
|
let specSeen = 0;
|
|
for (const name of ['gstack-spec', 'spec']) {
|
|
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
if (!exists(skill)) continue;
|
|
specSeen++;
|
|
const body = readText(skill);
|
|
if (/\$\(cat\s+["']?\$ARCHIVE/.test(body) || /grok[^\n]*\$\(cat/.test(body)) {
|
|
fail(area, `${name}: banned $(cat …) into grok argv`);
|
|
}
|
|
if (/grok --prompt-file/.test(body) || /Spawn \*\*Grok\*\*/.test(body)) {
|
|
ok(area, `${name}: Grok-native spawn present`);
|
|
} else if (/Spawned:.*claude -p/.test(body) && !/--execute-claude/.test(body)) {
|
|
fail(area, `${name}: default execute still only claude -p (no Grok spawn)`);
|
|
} else {
|
|
fail(area, `${name}: missing researched grok --prompt-file spawn`);
|
|
}
|
|
}
|
|
if (specSeen === 0) {
|
|
fail(area, 'spec: neither gstack-spec nor spec package installed under skills dir');
|
|
}
|
|
|
|
// setup-gbrain: must not hard-require which claude for success
|
|
let gbrainSeen = 0;
|
|
for (const name of ['gstack-setup-gbrain', 'setup-gbrain']) {
|
|
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
if (!exists(skill)) continue;
|
|
gbrainSeen++;
|
|
const body = readText(skill);
|
|
if (/Grok Build success path/.test(body) || /no Claude required/i.test(body)) {
|
|
ok(area, `${name}: Grok success path documented`);
|
|
} else {
|
|
warn(area, `${name}: missing explicit Grok success path (CLI+AGENTS)`);
|
|
}
|
|
}
|
|
if (gbrainSeen === 0) {
|
|
warn(area, 'setup-gbrain: package not installed (bridge not audited)');
|
|
}
|
|
|
|
// pair-agent: local must not hard-require ngrok; remote has install+security+teardown
|
|
let pairSeen = 0;
|
|
for (const name of ['gstack-pair-agent', 'pair-agent']) {
|
|
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
if (!exists(skill)) continue;
|
|
pairSeen++;
|
|
const body = readText(skill);
|
|
if (/teardown/i.test(body) && /ngrok/i.test(body) && /127\.0\.0\.1|security/i.test(body)) {
|
|
ok(area, `${name}: remote ngrok install+security+teardown present`);
|
|
} else {
|
|
warn(area, `${name}: remote security/teardown checklist incomplete`);
|
|
}
|
|
if (/DEPENDENT/.test(body)) ok(area, `${name}: honest DEPENDENT labeling`);
|
|
}
|
|
if (pairSeen === 0) {
|
|
warn(area, 'pair-agent: package not installed (bridge not audited)');
|
|
}
|
|
}
|
|
|
|
// ─── Main ─────────────────────────────────────────────────────
|
|
|
|
function main() {
|
|
console.log('== gstack-grok-compat-audit ==');
|
|
console.log(` skills-dir: ${SKILLS_DIR}`);
|
|
console.log(` phase: ${PHASE}`);
|
|
console.log(` monorepo: ${ROOT}`);
|
|
console.log('');
|
|
|
|
if (!exists(SKILLS_DIR)) {
|
|
fail('install', `skills dir missing: ${SKILLS_DIR} — run: cd ${ROOT} && ./setup --host grok-build`);
|
|
} else {
|
|
const pkgs = listMembershipPackages();
|
|
console.log(` membership packages: ${pkgs.length}`);
|
|
auditRuntimeRoot();
|
|
auditNoDoubleHome();
|
|
auditNoUngeneratedMonorepoPointers();
|
|
auditConnectChrome();
|
|
auditHostBleedSample();
|
|
auditSkillifySdkPath();
|
|
|
|
if (PHASE === 'ab' || PHASE === 'b' || PHASE === 'full') {
|
|
auditBridges();
|
|
}
|
|
}
|
|
|
|
const fails = findings.filter(f => f.severity === 'fail');
|
|
const warns = findings.filter(f => f.severity === 'warn');
|
|
const oks = findings.filter(f => f.severity === 'ok');
|
|
|
|
for (const f of findings) {
|
|
const tag = f.severity === 'fail' ? 'FAIL' : f.severity === 'warn' ? 'WARN' : 'OK ';
|
|
console.log(`[${tag}] ${f.area}: ${f.message}`);
|
|
}
|
|
|
|
console.log('');
|
|
console.log(`Summary: ${oks.length} ok, ${warns.length} warn, ${fails.length} fail`);
|
|
if (fails.length > 0) {
|
|
console.log('VERDICT: INCOMPATIBLE (packaging/bridge gate failed)');
|
|
process.exit(1);
|
|
}
|
|
console.log('VERDICT: packaging COMPATIBLE' + (PHASE.includes('b') || PHASE === 'full' ? ' (+ bridge gates honest)' : ''));
|
|
process.exit(0);
|
|
}
|
|
|
|
main();
|