feat(grok-build): full packaging + multi-CLI bridge compatibility

Make Grok Build a first-class gstack host with complete thin runtime root
assets, fixed SETUP path resolution, host-bleed cleanup, and honest multi-CLI
bridge packaging.

Phase A (packaging):
- Expand grok-build runtimeRoot + create_grok_runtime_root (specialists,
  browse/src, design/dist, make-pdf/dist, extension, scripts) with monorepo
  realpath healthchecks and fail-closed required links
- Fix $HOME$GSTACK_* double-home in browse/design/make-pdf SETUP resolvers
- Link connect-chrome → open-gstack-browser on Grok install; export
  GSTACK_MAKE_PDF in preamble; suppress Claude MODEL_OVERLAY for grok-build
- Label /claude as MULTI_CLI_BRIDGE with Step 0 detect

Phase B (bridges):
- /spec --execute spawns grok --prompt-file (researched flags) on Grok host
- GrokAdapter for gstack-model-benchmark --models grok
- setup/sync-gbrain Grok success without Claude MCP hard-require
- pair-agent remote ngrok install + security + teardown checklist

U9: bin/gstack-grok-compat-audit is the DoD gate (packaging + bridge honesty).
This commit is contained in:
Nehr 2026-07-13 01:19:38 +07:00
parent 11de390be1
commit 13d2f7dcdf
32 changed files with 4357 additions and 78 deletions

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ bin/gstack-global-discover*
.opencode/
.slate/
.cursor/
.grok/
.openclaw/
.hermes/
.gbrain/

View File

@ -20,11 +20,12 @@ allowed-tools:
## When to invoke this skill
Runs the same prompt through Claude,
GPT (via Codex CLI), and Gemini side-by-side — compares latency, tokens, cost,
and optionally quality via LLM judge. Answers "which model is actually best
for this skill?" with data instead of vibes. Separate from /benchmark, which
measures web page performance. Use when: "benchmark models", "compare models",
"which model is best for X", "cross-model comparison", "model shootout".
GPT (via Codex CLI), Gemini, and Grok side-by-side — compares latency, tokens,
cost, and optionally quality via LLM judge. Answers "which model is actually
best for this skill?" with data instead of vibes. Separate from /benchmark,
which measures web page performance. Use when: "benchmark models", "compare
models", "which model is best for X", "cross-model comparison", "model
shootout".
Voice triggers (speech-to-text aliases): "compare models", "model shootout", "which model is best".
@ -578,12 +579,12 @@ If C: ask for the path. Verify it exists. Use as positional argument.
## Step 2: Choose providers
```bash
"$BIN" --prompt "unused, dry-run" --models claude,gpt,gemini --dry-run
"$BIN" --prompt "unused, dry-run" --models claude,gpt,gemini,grok --dry-run
```
Show the dry-run output. The "Adapter availability" section tells the user which providers will actually run (OK) vs skip (NOT READY — remediation hint included).
If ALL three show NOT READY: stop with a clear message — benchmark can't run without at least one authed provider. Suggest `claude login`, `codex login`, or `gemini login` / `export GOOGLE_API_KEY`.
If ALL show NOT READY: stop with a clear message — benchmark can't run without at least one authed provider. Suggest `claude login`, `codex login`, `gemini login` / `export GOOGLE_API_KEY`, or `grok` login / `export XAI_API_KEY`.
If at least one is OK: AskUserQuestion:
- **Simplify:** "Which models should we include? The dry-run above showed which are authed. Unauthed ones will be skipped cleanly — they won't abort the batch."
@ -591,7 +592,8 @@ If at least one is OK: AskUserQuestion:
- **Options:**
- A) All authed providers. Completeness: 10/10.
- B) Only Claude. Completeness: 6/10 (no cross-model signal — use /ship's review for solo claude benchmarks instead).
- C) Pick two — specify on next turn. Completeness: 8/10.
- C) Only Grok (Grok-only machine). Completeness: 7/10.
- D) Pick two — specify on next turn. Completeness: 8/10.
---
@ -608,7 +610,8 @@ If judge is available, AskUserQuestion:
- A) Enable judge (adds ~$0.05). Completeness: 10/10.
- B) Skip judge — speed/cost/tokens only. Completeness: 7/10.
If judge is NOT available, skip this question and omit the `--judge` flag.
If judge is NOT available (Grok-only / no ANTHROPIC_API_KEY): skip this question
and omit `--judge` (Grok-or-skip). Speed/cost/tokens still work.
---

View File

@ -4,11 +4,12 @@ preamble-tier: 1
version: 1.0.0
description: |
Cross-model benchmark for gstack skills. Runs the same prompt through Claude,
GPT (via Codex CLI), and Gemini side-by-side — compares latency, tokens, cost,
and optionally quality via LLM judge. Answers "which model is actually best
for this skill?" with data instead of vibes. Separate from /benchmark, which
measures web page performance. Use when: "benchmark models", "compare models",
"which model is best for X", "cross-model comparison", "model shootout". (gstack)
GPT (via Codex CLI), Gemini, and Grok side-by-side — compares latency, tokens,
cost, and optionally quality via LLM judge. Answers "which model is actually
best for this skill?" with data instead of vibes. Separate from /benchmark,
which measures web page performance. Use when: "benchmark models", "compare
models", "which model is best for X", "cross-model comparison", "model
shootout". (gstack)
voice-triggers:
- "compare models"
- "model shootout"
@ -69,12 +70,12 @@ If C: ask for the path. Verify it exists. Use as positional argument.
## Step 2: Choose providers
```bash
"$BIN" --prompt "unused, dry-run" --models claude,gpt,gemini --dry-run
"$BIN" --prompt "unused, dry-run" --models claude,gpt,gemini,grok --dry-run
```
Show the dry-run output. The "Adapter availability" section tells the user which providers will actually run (OK) vs skip (NOT READY — remediation hint included).
If ALL three show NOT READY: stop with a clear message — benchmark can't run without at least one authed provider. Suggest `claude login`, `codex login`, or `gemini login` / `export GOOGLE_API_KEY`.
If ALL show NOT READY: stop with a clear message — benchmark can't run without at least one authed provider. Suggest `claude login`, `codex login`, `gemini login` / `export GOOGLE_API_KEY`, or `grok` login / `export XAI_API_KEY`.
If at least one is OK: AskUserQuestion:
- **Simplify:** "Which models should we include? The dry-run above showed which are authed. Unauthed ones will be skipped cleanly — they won't abort the batch."
@ -82,7 +83,8 @@ If at least one is OK: AskUserQuestion:
- **Options:**
- A) All authed providers. Completeness: 10/10.
- B) Only Claude. Completeness: 6/10 (no cross-model signal — use /ship's review for solo claude benchmarks instead).
- C) Pick two — specify on next turn. Completeness: 8/10.
- C) Only Grok (Grok-only machine). Completeness: 7/10.
- D) Pick two — specify on next turn. Completeness: 8/10.
---
@ -99,7 +101,8 @@ If judge is available, AskUserQuestion:
- A) Enable judge (adds ~$0.05). Completeness: 10/10.
- B) Skip judge — speed/cost/tokens only. Completeness: 7/10.
If judge is NOT available, skip this question and omit the `--judge` flag.
If judge is NOT available (Grok-only / no ANTHROPIC_API_KEY): skip this question
and omit `--judge` (Grok-or-skip). Speed/cost/tokens still work.
---

342
bin/gstack-grok-compat-audit Executable file
View File

@ -0,0 +1,342 @@
#!/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 { execSync } 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';
// benchmark-models: grok provider must exist in binary
try {
const help = execSync(`"${path.join(ROOT, 'bin/gstack-model-benchmark')}" --prompt "x" --models grok --dry-run 2>&1`, {
encoding: 'utf-8',
timeout: 15000,
});
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 (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: Grok package should not default to claude -p outside execute-claude
for (const name of ['gstack-spec', 'spec']) {
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
if (!exists(skill)) continue;
const body = readText(skill);
if (/\$\(cat\s+["']?\$ARCHIVE/.test(body) || /grok[^\n]*\$\(cat/.test(body)) {
fail(area, `${name}: banned $(cat …) into grok argv`);
}
if (/Spawned:.*claude -p/.test(body) && !/--execute-claude/.test(body) && !/optional/i.test(body)) {
// Default path still Claude — fail for Grok host package
if (/grok --prompt-file/.test(body) || /Spawn \*\*Grok\*\*/.test(body)) {
ok(area, `${name}: Grok-native spawn present`);
} else {
fail(area, `${name}: default execute still only claude -p (no Grok spawn)`);
}
} else if (/grok --prompt-file/.test(body)) {
ok(area, `${name}: uses researched grok --prompt-file`);
}
}
// setup-gbrain: must not hard-require which claude for success
for (const name of ['gstack-setup-gbrain', 'setup-gbrain']) {
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
if (!exists(skill)) continue;
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)`);
}
}
// pair-agent: local must not hard-require ngrok; remote has install+security+teardown
for (const name of ['gstack-pair-agent', 'pair-agent']) {
const skill = path.join(SKILLS_DIR, name, 'SKILL.md');
if (!exists(skill)) continue;
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`);
}
}
// ─── 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();

View File

@ -7,7 +7,7 @@
* gstack-model-benchmark <skill-or-prompt-file> [options]
*
* Options:
* --models claude,gpt,gemini Comma-separated provider list (default: claude)
* --models claude,gpt,gemini,grok Comma-separated provider list (default: claude)
* --prompt "<text>" Inline prompt instead of a file
* --workdir <path> Working dir passed to each CLI (default: cwd)
* --timeout-ms <n> Per-provider timeout (default: 300000)
@ -15,27 +15,29 @@
* --skip-unavailable Skip providers that fail available() check
* (default: include them with unavailable marker)
* --judge Run Anthropic SDK judge on outputs for quality score
* (requires ANTHROPIC_API_KEY; adds ~$0.05 per call)
* (requires ANTHROPIC_API_KEY; Grok-only skips judge)
* --dry-run Validate flags + resolve auth, don't invoke providers
*
* Examples:
* gstack-model-benchmark --prompt "Write a haiku about databases" --models claude,gpt
* gstack-model-benchmark ./test-prompt.txt --models claude,gpt,gemini --judge
* gstack-model-benchmark --prompt "hi" --models claude,gpt,gemini --dry-run
* gstack-model-benchmark --prompt "hi" --models grok --dry-run
*/
import '../lib/conductor-env-shim';
import * as fs from 'fs';
import * as path from 'path';
import { runBenchmark, formatTable, formatJson, formatMarkdown, type BenchmarkInput } from '../test/helpers/benchmark-runner';
import { runBenchmark, formatTable, formatJson, formatMarkdown, type BenchmarkInput, type ProviderName } from '../test/helpers/benchmark-runner';
import { ClaudeAdapter } from '../test/helpers/providers/claude';
import { GptAdapter } from '../test/helpers/providers/gpt';
import { GeminiAdapter } from '../test/helpers/providers/gemini';
import { GrokAdapter } from '../test/helpers/providers/grok';
const ADAPTER_FACTORIES = {
const ADAPTER_FACTORIES: Record<ProviderName, () => { name: string; available: () => Promise<{ ok: boolean; reason?: string }> }> = {
claude: () => new ClaudeAdapter(),
gpt: () => new GptAdapter(),
gemini: () => new GeminiAdapter(),
grok: () => new GrokAdapter(),
};
type OutputFormat = 'table' | 'json' | 'markdown';
@ -76,13 +78,13 @@ function positionalArgs(args: string[]): string[] {
return positional;
}
function parseProviders(s: string | undefined): Array<'claude' | 'gpt' | 'gemini'> {
function parseProviders(s: string | undefined): ProviderName[] {
if (!s) return ['claude'];
const seen = new Set<'claude' | 'gpt' | 'gemini'>();
const seen = new Set<ProviderName>();
for (const p of s.split(',').map(x => x.trim()).filter(Boolean)) {
if (p === 'claude' || p === 'gpt' || p === 'gemini') seen.add(p);
if (p === 'claude' || p === 'gpt' || p === 'gemini' || p === 'grok') seen.add(p);
else {
console.error(`WARN: unknown provider '${p}' — skipping. Valid: claude, gpt, gemini.`);
console.error(`WARN: unknown provider '${p}' — skipping. Valid: claude, gpt, gemini, grok.`);
}
}
return seen.size ? Array.from(seen) : ['claude'];
@ -129,11 +131,16 @@ async function main(): Promise<void> {
const report = await runBenchmark(input);
if (doJudge) {
try {
const { judgeEntries } = await import('../test/helpers/benchmark-judge');
await judgeEntries(report);
} catch (err) {
console.error(`WARN: judge unavailable: ${(err as Error).message}`);
// Grok-or-skip: judge requires Anthropic; Grok-only machines skip cleanly
if (!process.env.ANTHROPIC_API_KEY) {
console.error('WARN: judge skipped — ANTHROPIC_API_KEY not set (Grok-or-skip judge).');
} else {
try {
const { judgeEntries } = await import('../test/helpers/benchmark-judge');
await judgeEntries(report);
} catch (err) {
console.error(`WARN: judge unavailable: ${(err as Error).message}`);
}
}
}
@ -149,7 +156,7 @@ async function main(): Promise<void> {
async function dryRunReport(opts: {
prompt: string;
providers: Array<'claude' | 'gpt' | 'gemini'>;
providers: ProviderName[];
workdir: string;
timeoutMs: number;
output: OutputFormat;

View File

@ -30,6 +30,11 @@ modify files.
The generated external invocation name is `gstack-claude`.
**MULTI_CLI_BRIDGE:** Primary modes always call the Claude Code CLI. Packaging and
Step 0 detect/install are first-class on this host; this skill does **not** work
without Claude installed and authenticated. Residual foreign-CLI dependency is
expected and labeled — never claim "works without Claude."
---
## Step 0: Check Claude CLI

113
hosts/grok-build.ts Normal file
View File

@ -0,0 +1,113 @@
import type { HostConfig } from '../scripts/host-config';
/**
* Grok Build (xAI) host based on community PR #2028 (adamouabakar),
* aligned with cursor/factory host patterns and Grok skill discovery.
*
* Skills generate into <repo>/.grok/skills/gstack-* and install under
* ~/.grok/skills/ (flat skill packages + runtime root at ~/.grok/skills/gstack).
*
* Grok discovers user skills from ~/.grok/skills/<name>/SKILL.md.
* Frontmatter `name:` stays unprefixed (browse, ship, ) so slash commands
* remain /browse, /ship, etc. Directory names use gstack- prefix (external hosts).
*/
const grokBuild: HostConfig = {
name: 'grok-build',
displayName: 'Grok Build',
cliCommand: 'grok',
cliAliases: ['grok-build'],
// Relative to $HOME for global install path docs / preamble
globalRoot: '.grok/skills/gstack',
// Project-local runtime root (optional team mode)
localSkillRoot: '.grok/skills/gstack',
// Gitignored generated skill docs live here
hostSubdir: '.grok',
usesEnvVars: true,
frontmatter: {
mode: 'allowlist',
// Keep triggers so Grok model-invocation routing still works
keepFields: ['name', 'description', 'triggers', 'allowed-tools'],
descriptionLimit: null,
},
generation: {
generateMetadata: false,
// Upstream convention: /codex skill is a Claude↔Codex bridge; all external
// hosts skip it (host-config.test.ts). Grok users already have the openai-codex
// plugin; keep /claude as an optional outside-voice skill.
skipSkills: ['codex'],
},
pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' },
{ from: '.claude/skills/gstack', to: '.grok/skills/gstack' },
{ from: '.claude/skills/review', to: '.grok/skills/gstack/review' },
{ from: '.claude/skills', to: '.grok/skills' },
{ from: '~/.claude/skills', to: '~/.grok/skills' },
{ from: 'CLAUDE.md', to: 'AGENTS.md' },
// Defense-in-depth if a Claude model overlay ever slips through (U3 / KTD6)
{ from: 'MODEL_OVERLAY: claude', to: 'MODEL_OVERLAY: none' },
{ from: 'use the Skill tool', to: 'invoke the skill via slash command or skill load' },
],
toolRewrites: {
'use the Bash tool': 'run this command in the shell',
'use the Write tool': 'create this file',
'use the Read tool': 'read the file',
'use the Edit tool': 'edit the file',
'use the Agent tool': 'dispatch a subagent',
'use the Grep tool': 'search for',
'use the Glob tool': 'find files matching',
'use the Skill tool': 'invoke the skill via slash command or skill load',
AskUserQuestion: 'ask_user_question',
ExitPlanMode: 'exit_plan_mode',
},
// Suppress Claude-only outside-voice orchestration that assumes Claude can
// spawn Codex as itself. Keep plan/review skills; they still run on Grok.
suppressedResolvers: [
'GBRAIN_CONTEXT_LOAD',
'GBRAIN_SAVE_RESULTS',
],
// Thin runtime root: every asset skills resolve via $GSTACK_ROOT (R1 / U1).
// Dual-write with setup create_grok_runtime_root — keep lists in sync.
runtimeRoot: {
globalSymlinks: [
'bin',
'browse/dist',
'browse/bin',
'browse/src',
'design/dist',
'make-pdf/dist',
'extension',
'scripts',
'review/specialists',
'gstack-upgrade',
'ETHOS.md',
],
globalFiles: {
review: [
'checklist.md',
'TODOS-format.md',
'design-checklist.md',
'greptile-triage.md',
],
},
},
install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},
learningsMode: 'basic',
boundaryInstruction:
'IMPORTANT: Prefer ~/.grok/skills/gstack and $GSTACK_ROOT over ~/.claude/skills/gstack. ' +
'Do not assume Claude Code tools (TodoWrite, Skill tool, Claude-in-Chrome MCP). ' +
'Use Grok shell/read/edit/web tools and ask_user_question. Prefer /browse over browser MCPs.',
};
export default grokBuild;

View File

@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';
import gbrain from './gbrain';
import grokBuild from './grok-build';
/** All registered host configs. Add new hosts here. */
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain];
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, grokBuild];
/** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
@ -65,4 +66,4 @@ export function getExternalHosts(): HostConfig[] {
}
// Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain };
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, grokBuild };

View File

@ -994,6 +994,27 @@ browser to the internet securely).
STOP here. Wait for the user to install ngrok and re-invoke.
**Bridge status:** Remote pair is **DEPENDENT** on ngrok until the tunnel binary
is installed and authenticated. Local same-machine pair remains zero-extra-dep
and must never hard-require ngrok.
### Remote path security + teardown checklist
Remote pairing expands attack surface vs local pair. Always:
1. **Authenticated tunnel only** — never run an unauthenticated public expose.
2. **Bind local pair endpoint to 127.0.0.1** — do not advertise LAN bind.
3. **Authtoken out-of-band** — user pastes token into `ngrok config add-authtoken`;
never echo the token or tunnel URL into session transcripts beyond what the
CLI already printed for the user to copy.
4. **Teardown after pair** — when pairing ends, stop the ngrok tunnel process
(or tell the user: `pkill -f 'ngrok http'` / dashboard stop). Do not leave
a long-lived public tunnel running.
5. **Verify:** `ngrok config check` after auth; `which ngrok` for install.
If ngrok is missing, report bridge DEPENDENT (install path present) — do **not**
mark the remote primary path READY/COMPATIBLE from install prose alone.
## Step 5: Verify connection
After the user pastes the instructions into the other agent, wait a moment then check:

View File

@ -189,6 +189,27 @@ browser to the internet securely).
STOP here. Wait for the user to install ngrok and re-invoke.
**Bridge status:** Remote pair is **DEPENDENT** on ngrok until the tunnel binary
is installed and authenticated. Local same-machine pair remains zero-extra-dep
and must never hard-require ngrok.
### Remote path security + teardown checklist
Remote pairing expands attack surface vs local pair. Always:
1. **Authenticated tunnel only** — never run an unauthenticated public expose.
2. **Bind local pair endpoint to 127.0.0.1** — do not advertise LAN bind.
3. **Authtoken out-of-band** — user pastes token into `ngrok config add-authtoken`;
never echo the token or tunnel URL into session transcripts beyond what the
CLI already printed for the user to copy.
4. **Teardown after pair** — when pairing ends, stop the ngrok tunnel process
(or tell the user: `pkill -f 'ngrok http'` / dashboard stop). Do not leave
a long-lived public tunnel running.
5. **Verify:** `ngrok config check` after auth; `which ngrok` for install.
If ngrok is missing, report bridge DEPENDENT (install path present) — do **not**
mark the remote primary path READY/COMPATIBLE from install prose alone.
## Step 5: Verify connection
After the user pastes the instructions into the other agent, wait a moment then check:

View File

@ -91,9 +91,11 @@ let HOST: Host = HOST_ARG_VAL === 'all' ? 'claude' : HOST_ARG_VAL;
// ─── Model Overlay Selection ────────────────────────────────
// --model is explicit. We do NOT auto-detect from host (host ≠ model).
// Default is 'claude'. Missing overlay file → empty string (graceful).
// Default is 'claude' for most hosts. Grok Build suppresses the Claude
// model overlay body unless --model is passed explicitly (U3 / KTD6).
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
const MODEL_ARG = process.argv.find(a => a.startsWith('--model'));
const MODEL_EXPLICIT = !!MODEL_ARG;
const MODEL_ARG_VAL: Model = (() => {
if (!MODEL_ARG) return 'claude';
const val = MODEL_ARG.includes('=') ? MODEL_ARG.split('=')[1] : process.argv[process.argv.indexOf(MODEL_ARG) + 1];
@ -729,9 +731,12 @@ function buildContext(
const preambleTier = tierMatch ? parseInt(tierMatch[1], 10) : undefined;
const interactiveMatch = tmplContent.match(/^interactive:\s*(true|false)\s*$/m);
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined;
// Grok-native packaging: no Claude MODEL_OVERLAY section unless user forced --model
const modelForHost: Model | undefined =
host === 'grok-build' && !MODEL_EXPLICIT ? undefined : MODEL_ARG_VAL;
return {
skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host],
preambleTier, model: MODEL_ARG_VAL, interactive, explainLevel: EXPLAIN_LEVEL,
preambleTier, model: modelForHost, interactive, explainLevel: EXPLAIN_LEVEL,
};
}

View File

@ -15,7 +15,7 @@
},
"benchmark-models": {
"lead": "Cross-model benchmark for gstack skills.",
"routing": "Runs the same prompt through Claude,\nGPT (via Codex CLI), and Gemini side-by-side — compares latency, tokens, cost,\nand optionally quality via LLM judge. Answers \"which model is actually best\nfor this skill?\" with data instead of vibes. Separate from /benchmark, which\nmeasures web page performance. Use when: \"benchmark models\", \"compare models\",\n\"which model is best for X\", \"cross-model comparison\", \"model shootout\".",
"routing": "Runs the same prompt through Claude,\nGPT (via Codex CLI), Gemini, and Grok side-by-side — compares latency, tokens,\ncost, and optionally quality via LLM judge. Answers \"which model is actually\nbest for this skill?\" with data instead of vibes. Separate from /benchmark,\nwhich measures web page performance. Use when: \"benchmark models\", \"compare\nmodels\", \"which model is best for X\", \"cross-model comparison\", \"model\nshootout\".",
"voice_line": "Voice triggers (speech-to-text aliases): \"compare models\", \"model shootout\", \"which model is best\"."
},
"browse": {

View File

@ -99,14 +99,24 @@ export function generateSnapshotFlags(_ctx: TemplateContext): string {
return lines.join('\n');
}
/** Resolve dist binary path: env-var hosts use $GSTACK_* (never $HOME+$GSTACK_*). */
export function resolveDistBinary(dir: string, binary: string): string {
if (dir.startsWith('$')) {
// e.g. $GSTACK_BROWSE already points at .../browse/dist
return `${dir}/${binary}`;
}
return `$HOME${dir.replace(/^~/, '')}/${binary}`;
}
export function generateBrowseSetup(ctx: TemplateContext): string {
const globalBrowse = resolveDistBinary(ctx.paths.browseDir, 'browse');
return `## SETUP (run this check BEFORE any browse command)
\`\`\`bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" ] && B="$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse"
[ -z "$B" ] && B="$HOME${ctx.paths.browseDir.replace(/^~/, '')}/browse"
[ -z "$B" ] && B="${globalBrowse}"
if [ -x "$B" ]; then
echo "READY: $B"
else

View File

@ -1,5 +1,6 @@
import type { TemplateContext } from './types';
import { AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS } from './constants';
import { resolveDistBinary } from './browse';
export function generateDesignReviewLite(ctx: TemplateContext): string {
const litmusList = OPENAI_LITMUS_CHECKS.map((item, i) => `${i + 1}. ${item}`).join(' ');
@ -786,13 +787,15 @@ Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developer
}
export function generateDesignSetup(ctx: TemplateContext): string {
const globalDesign = resolveDistBinary(ctx.paths.designDir, 'design');
const globalBrowse = resolveDistBinary(ctx.paths.browseDir, 'browse');
return `## DESIGN SETUP (run this check BEFORE any design mockup command)
\`\`\`bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
D=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design"
[ -z "$D" ] && D="$HOME${ctx.paths.designDir.replace(/^~/, '')}/design"
[ -z "$D" ] && D="${globalDesign}"
if [ -x "$D" ]; then
echo "DESIGN_READY: $D"
else
@ -800,7 +803,7 @@ else
fi
B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" ] && B="$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse"
[ -z "$B" ] && B="$HOME${ctx.paths.browseDir.replace(/^~/, '')}/browse"
[ -z "$B" ] && B="${globalBrowse}"
if [ -x "$B" ]; then
echo "BROWSE_READY: $B"
else
@ -831,13 +834,14 @@ data, not project files. They persist across branches, conversations, and worksp
}
export function generateDesignMockup(ctx: TemplateContext): string {
const globalDesign = resolveDistBinary(ctx.paths.designDir, 'design');
return `## Visual Design Exploration
\`\`\`bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
D=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design"
[ -z "$D" ] && D="$HOME${ctx.paths.designDir.replace(/^~/, '')}/design"
[ -z "$D" ] && D="${globalDesign}"
[ -x "$D" ] && echo "DESIGN_READY" || echo "DESIGN_NOT_AVAILABLE"
\`\`\`

View File

@ -36,6 +36,7 @@ import { generateMakePdfSetup } from './make-pdf';
import { generateTasksSectionEmit, generateTasksSectionAggregate } from './tasks-section';
import { SECTION, SECTION_INDEX } from './sections';
import { generateRedactTaxonomyTable, generateRedactInvocationBlock } from './redact-doc';
import { generateSpecSpawn, generateSpecExecuteFlag } from './spec-spawn';
export const RESOLVERS: Record<string, ResolverValue> = {
SLUG_EVAL: generateSlugEval,
@ -100,6 +101,8 @@ export const RESOLVERS: Record<string, ResolverValue> = {
MAKE_PDF_SETUP: generateMakePdfSetup,
TASKS_SECTION_EMIT: generateTasksSectionEmit,
TASKS_SECTION_AGGREGATE: generateTasksSectionAggregate,
SPEC_SPAWN: generateSpecSpawn,
SPEC_EXECUTE_FLAG: generateSpecExecuteFlag,
SECTION,
SECTION_INDEX,
};

View File

@ -1,4 +1,5 @@
import type { TemplateContext } from './types';
import { resolveDistBinary } from './browse';
/**
* {{MAKE_PDF_SETUP}} emits the shell preamble that resolves $P to the
@ -8,10 +9,11 @@ import type { TemplateContext } from './types';
*
* Resolution order (matches src/browseClient.ts::resolveBrowseBin):
* 1. Local skill root: $_ROOT/{localSkillRoot}/make-pdf/dist/pdf
* 2. Global: ~/{globalRoot}/make-pdf/dist/pdf
* 2. Global: $GSTACK_MAKE_PDF/pdf or ~/{globalRoot}/make-pdf/dist/pdf
* 3. Env override (MAKE_PDF_BIN) for contributor dev builds
*/
export function generateMakePdfSetup(ctx: TemplateContext): string {
const globalPdf = resolveDistBinary(ctx.paths.makePdfDir, 'pdf');
return `## MAKE-PDF SETUP (run this check BEFORE any make-pdf command)
\`\`\`bash
@ -19,7 +21,7 @@ _ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
P=""
[ -n "$MAKE_PDF_BIN" ] && [ -x "$MAKE_PDF_BIN" ] && P="$MAKE_PDF_BIN"
[ -z "$P" ] && [ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/make-pdf/dist/pdf" ] && P="$_ROOT/${ctx.paths.localSkillRoot}/make-pdf/dist/pdf"
[ -z "$P" ] && P="$HOME${ctx.paths.makePdfDir.replace(/^~/, '')}/pdf"
[ -z "$P" ] && P="${globalPdf}"
if [ -x "$P" ]; then
echo "MAKE_PDF_READY: $P"
alias _p_="$P" # shellcheck alias helper (not exported)

View File

@ -10,6 +10,7 @@ GSTACK_ROOT="$HOME/${hostConfig.globalRoot}"
GSTACK_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/dist"
GSTACK_MAKE_PDF="$GSTACK_ROOT/make-pdf/dist"
`
: '';

View File

@ -0,0 +1,88 @@
import type { TemplateContext } from './types';
/**
* Host-native agent spawn for /spec --execute (U4 / R3).
*
* Grok research matrix (local CLI grok 0.2.x):
* -p / --single <PROMPT> short single-turn
* --prompt-file <PATH> single-turn from file (preferred for archives)
* --cwd <CWD> working directory
* --always-approve elevated auto-approve (opt-in / documented)
* Never: $(cat ) into argv (ARG_MAX); never invent --permission-mode acceptEdits.
*
* Claude host generation unchanged: stdin pipe into claude -p.
*/
export function generateSpecSpawn(ctx: TemplateContext): string {
if (ctx.host === 'grok-build') {
return `If A and worktree created: spawn **Grok** headless with the archived
spec as a prompt file (never \`$(cat …)\` into argv — ARG_MAX / quoting risk).
**Auth gate (fail closed):** before spawn, verify Grok is available and configured:
\`\`\`bash
command -v grok >/dev/null 2>&1 || { echo "STOP: grok CLI not on PATH. Install Grok Build, or re-run with --no-execute / --file-only."; exit 1; }
if [ ! -f "$HOME/.grok/auth.json" ] && [ -z "\${XAI_API_KEY:-}\${GROK_API_KEY:-}" ]; then
echo "STOP: Grok not authenticated (no ~/.grok/auth.json and no XAI_API_KEY/GROK_API_KEY). Log in via \`grok\`, or use --no-execute."
exit 1
fi
# ARCHIVE_PATH must stay under SPAWN_PATH parent or the gstack projects allowlist
ARCHIVE_REAL=$(cd "$(dirname "$ARCHIVE_PATH")" && pwd -P)/$(basename "$ARCHIVE_PATH")
SPAWN_REAL=$(cd "$SPAWN_PATH" 2>/dev/null && pwd -P || echo "")
case "$ARCHIVE_REAL" in
"$HOME/.gstack/projects"/*|"$HOME/.gstack/projects"/*/*) ;; # allowlisted archive dir
*)
if [ -n "$SPAWN_REAL" ]; then
case "$ARCHIVE_REAL" in
"$SPAWN_REAL"/*) ;;
*) echo "STOP: ARCHIVE_PATH realpath not under SPAWN_PATH or allowlisted archive dir."; exit 1 ;;
esac
fi
;;
esac
\`\`\`
**Security:** \`--always-approve\` auto-approves tool use (elevated). Only spawn
when the user confirmed the D16 gate. Spec archives must not contain secrets.
Third-party note: archive body is sent to xAI for processing.
\`\`\`bash
# Prefer --prompt-file (researched). Optional elevated approve is documented opt-in.
(cd "$SPAWN_PATH" && grok --prompt-file "$ARCHIVE_PATH" --cwd "$SPAWN_PATH" --always-approve 2>&1) &
SPAWN_PID=$!
echo "Spawned: PID $SPAWN_PID in $SPAWN_PATH (branch $SPAWN_BRANCH)"
echo "Follow with: cd $SPAWN_PATH && grok --continue"
\`\`\`
**Optional Claude execute:** if the user asked for \`--execute-claude\` instead of
default Grok execute, and \`claude\` is on PATH, you may spawn:
\`\`\`bash
cat "$ARCHIVE_PATH" | (cd "$SPAWN_PATH" && claude -p 2>&1) &
\`\`\`
Do **not** silently fall through to Claude when Grok is missing STOP instead.
If no safe multi-line file ingest is available on an older Grok CLI (no
\`--prompt-file\`), demote to \`--no-execute\` / file-only and tell the user:
"This Grok CLI lacks --prompt-file; filed the issue only. Upgrade Grok Build or
use --execute-claude if Claude is installed."`;
}
// Claude + all other hosts: classic claude -p stdin pipe
return `If A and worktree created: spawn \`claude -p\` with the spec piped via stdin:
\`\`\`bash
cat "$ARCHIVE_PATH" | (cd "$SPAWN_PATH" && claude -p 2>&1) &
SPAWN_PID=$!
echo "Spawned: PID $SPAWN_PID in $SPAWN_PATH (branch $SPAWN_BRANCH)"
echo "Follow with: cd $SPAWN_PATH && claude --resume"
\`\`\``;
}
/** Flag-table row for --execute (host-aware description). */
export function generateSpecExecuteFlag(ctx: TemplateContext): string {
if (ctx.host === 'grok-build') {
return '| `--execute` | conditional default (see Phase 5) | Spawn `grok --prompt-file` headless in a fresh worktree after filing the issue. |';
}
return '| `--execute` | conditional default (see Phase 5) | Spawn `claude -p` in a fresh worktree after filing the issue. |';
}

244
setup
View File

@ -24,6 +24,8 @@ FACTORY_SKILLS="$HOME/.factory/skills"
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
GROK_SKILLS="$HOME/.grok/skills"
GROK_GSTACK="$GROK_SKILLS/gstack"
IS_WINDOWS=0
case "$(uname -s)" in
@ -85,7 +87,7 @@ NO_TEAM_MODE=0
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
while [ $# -gt 0 ]; do
case "$1" in
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, grok-build, grok, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host=*) HOST="${1#--host=}"; shift ;;
--local) LOCAL_INSTALL=1; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
@ -100,8 +102,11 @@ while [ $# -gt 0 ]; do
esac
done
# Alias: --host grok → grok-build
if [ "$HOST" = "grok" ]; then HOST="grok-build"; fi
case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;;
claude|codex|kiro|factory|opencode|grok-build|auto) ;;
openclaw)
echo ""
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
@ -136,7 +141,7 @@ case "$HOST" in
echo "GBrain setup and brain skills ship from the GBrain repo."
echo ""
exit 0 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, grok-build, grok, or auto)" >&2; exit 1 ;;
esac
# ─── Resolve skill prefix preference ─────────────────────────
@ -200,14 +205,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
INSTALL_OPENCODE=0
INSTALL_GROK=0
if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
command -v grok >/dev/null 2>&1 && INSTALL_GROK=1
# If none found, default to claude
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_GROK" -eq 0 ]; then
INSTALL_CLAUDE=1
fi
elif [ "$HOST" = "claude" ]; then
@ -220,6 +227,8 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1
elif [ "$HOST" = "grok-build" ]; then
INSTALL_GROK=1
fi
migrate_direct_codex_install() {
@ -475,6 +484,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
)
fi
# 1e. Generate .grok/ Grok Build skill docs
if [ "$INSTALL_GROK" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
log "Generating .grok/ skill docs for Grok Build..."
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host grok-build
)
fi
# 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then
echo "Installing Playwright Chromium..."
@ -972,6 +991,212 @@ link_opencode_skill_dirs() {
fi
}
# ─── Grok Build (xAI) runtime root + skill links ───────────────────────────
# Grok discovers skills from ~/.grok/skills/<pkg>/SKILL.md.
# Generated packages live in <repo>/.grok/skills/gstack-* (path-rewritten).
# Runtime assets (bin, browse, design, specialists, …) live at ~/.grok/skills/gstack.
#
# Security: every required link target must realpath under monorepo install root.
# Dual-write with hosts/grok-build.ts runtimeRoot — keep lists in sync (R1 / U1).
_grok_link_under_monorepo() {
local monorepo_root="$1"
local src="$2"
local dst="$3"
local required="${4:-0}" # 1 = fail closed if missing when expected
if [ ! -e "$src" ]; then
if [ "$required" = "1" ]; then
echo "error: required Grok runtime asset missing: $src" >&2
echo " monorepo root: $monorepo_root" >&2
echo " Re-run from a full gstack checkout after builds complete." >&2
return 1
fi
return 0
fi
local src_real monorepo_real
src_real=$(cd "$(dirname "$src")" 2>/dev/null && pwd -P)/$(basename "$src")
monorepo_real=$(cd "$monorepo_root" 2>/dev/null && pwd -P)
case "$src_real" in
"$monorepo_real"|"$monorepo_real"/*) ;;
*)
echo "error: refusing Grok runtime link outside monorepo:" >&2
echo " src=$src_real" >&2
echo " monorepo=$monorepo_real" >&2
return 1
;;
esac
_link_or_copy "$src" "$dst"
}
create_grok_runtime_root() {
local gstack_dir="$1"
local grok_gstack="$2"
local generated_root="$gstack_dir/.grok/skills/gstack"
if [ -L "$grok_gstack" ]; then
rm -f "$grok_gstack"
elif [ -d "$grok_gstack" ] && [ "$grok_gstack" != "$gstack_dir" ]; then
# Replace previous thin packages / monorepo symlinks
rm -rf "$grok_gstack"
fi
mkdir -p "$grok_gstack" \
"$grok_gstack/browse" \
"$grok_gstack/design" \
"$grok_gstack/make-pdf" \
"$grok_gstack/gstack-upgrade" \
"$grok_gstack/review"
if [ -f "$generated_root/SKILL.md" ]; then
_link_or_copy "$generated_root/SKILL.md" "$grok_gstack/SKILL.md"
elif [ -f "$gstack_dir/SKILL.md" ]; then
_link_or_copy "$gstack_dir/SKILL.md" "$grok_gstack/SKILL.md"
fi
# Required core assets (fail closed)
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/bin" "$grok_gstack/bin" 1 || return 1
if [ -d "$gstack_dir/browse/dist" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/browse/dist" "$grok_gstack/browse/dist" 1 || return 1
else
echo "error: required monorepo target missing: $gstack_dir/browse/dist (run ./setup build first)" >&2
return 1
fi
if [ -d "$gstack_dir/browse/bin" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/browse/bin" "$grok_gstack/browse/bin" 0 || return 1
fi
if [ -d "$gstack_dir/browse/src" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/browse/src" "$grok_gstack/browse/src" 1 || return 1
else
echo "error: required monorepo target missing: $gstack_dir/browse/src" >&2
return 1
fi
# design/dist — required when monorepo has design package
if [ -d "$gstack_dir/design/dist" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/design/dist" "$grok_gstack/design/dist" 1 || return 1
elif [ -d "$gstack_dir/design" ]; then
echo "error: monorepo has design/ but design/dist is missing — run design build" >&2
return 1
fi
# make-pdf/dist — link when present (membership skill)
if [ -d "$gstack_dir/make-pdf/dist" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/make-pdf/dist" "$grok_gstack/make-pdf/dist" 0 || return 1
fi
# extension/ — required when monorepo has it
if [ -d "$gstack_dir/extension" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/extension" "$grok_gstack/extension" 1 || return 1
fi
# Full scripts/ tree (prefer full; jargon-only is debt)
if [ -d "$gstack_dir/scripts" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/scripts" "$grok_gstack/scripts" 1 || return 1
else
echo "error: required monorepo target missing: $gstack_dir/scripts" >&2
return 1
fi
# Review assets + specialists
for f in checklist.md TODOS-format.md design-checklist.md greptile-triage.md; do
if [ -f "$gstack_dir/review/$f" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/review/$f" "$grok_gstack/review/$f" 0 || return 1
fi
done
if [ -d "$gstack_dir/review/specialists" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/review/specialists" "$grok_gstack/review/specialists" 1 || return 1
else
echo "error: required monorepo target missing: $gstack_dir/review/specialists" >&2
return 1
fi
if [ -f "$gstack_dir/gstack-upgrade/SKILL.md" ] || [ -f "$gstack_dir/.grok/skills/gstack-upgrade/SKILL.md" ]; then
if [ -f "$gstack_dir/.grok/skills/gstack-upgrade/SKILL.md" ]; then
_link_or_copy "$gstack_dir/.grok/skills/gstack-upgrade/SKILL.md" "$grok_gstack/gstack-upgrade/SKILL.md"
else
_link_or_copy "$gstack_dir/gstack-upgrade/SKILL.md" "$grok_gstack/gstack-upgrade/SKILL.md"
fi
fi
if [ -f "$gstack_dir/ETHOS.md" ]; then
_grok_link_under_monorepo "$gstack_dir" "$gstack_dir/ETHOS.md" "$grok_gstack/ETHOS.md" 0 || return 1
fi
}
link_grok_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local grok_dir="$gstack_dir/.grok/skills"
local linked=()
if [ ! -d "$grok_dir" ]; then
echo " Generating .grok/ skill docs..."
( cd "$gstack_dir" && bun run gen:skill-docs --host grok-build )
fi
if [ ! -d "$grok_dir" ]; then
echo " warning: .grok/skills/ generation failed — run 'bun run gen:skill-docs --host grok-build' manually" >&2
return 1
fi
mkdir -p "$skills_dir"
for skill_dir in "$grok_dir"/gstack*/; do
if [ -f "$skill_dir/SKILL.md" ]; then
skill_name="$(basename "$skill_dir")"
# Runtime root is installed separately as ~/.grok/skills/gstack
[ "$skill_name" = "gstack" ] && continue
target="$skills_dir/$skill_name"
# Replace stale monorepo flat symlinks (browse → ~/gstack/browse) with generated packages
if [ -L "$target" ] || [ ! -e "$target" ]; then
_link_or_copy "$skill_dir" "$target"
linked+=("$skill_name")
elif [ -d "$target" ] && [ ! -f "$target/SKILL.md" ]; then
rm -rf "$target"
_link_or_copy "$skill_dir" "$target"
linked+=("$skill_name")
else
# Force refresh: generated content is authoritative for this host
rm -rf "$target"
_link_or_copy "$skill_dir" "$target"
linked+=("$skill_name")
fi
# Also expose unprefixed alias (browse) so slash names match frontmatter
# without depending on Grok's name: field alone.
# Deny bare `codex` alias (OpenAI plugin owns /codex); optional gstack-codex only.
bare_name="${skill_name#gstack-}"
if [ -n "$bare_name" ] && [ "$bare_name" != "$skill_name" ] && [ "$bare_name" != "codex" ]; then
bare_target="$skills_dir/$bare_name"
# Only alias if missing or already a symlink (don't clobber real non-gstack skills)
if [ -L "$bare_target" ] || [ ! -e "$bare_target" ]; then
_link_or_copy "$skill_dir" "$bare_target"
fi
fi
fi
done
# Backwards-compat alias: /connect-chrome → generated open-gstack-browser (U2 / KTD7)
# Force-refresh stale monorepo targets that pointed at source tree packages.
local _ogb_src=""
if [ -d "$skills_dir/gstack-open-gstack-browser" ]; then
_ogb_src="$skills_dir/gstack-open-gstack-browser"
elif [ -d "$grok_dir/gstack-open-gstack-browser" ]; then
_ogb_src="$grok_dir/gstack-open-gstack-browser"
fi
if [ -n "$_ogb_src" ]; then
rm -rf "$skills_dir/connect-chrome"
_link_or_copy "$_ogb_src" "$skills_dir/connect-chrome"
linked+=("connect-chrome→open-gstack-browser")
fi
if [ ${#linked[@]} -gt 0 ]; then
echo " linked skills: ${linked[*]}"
fi
}
# 4. Install for Claude (default)
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
@ -1193,6 +1418,17 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
echo " opencode skills: $OPENCODE_SKILLS"
fi
# 6d. Install for Grok Build (xAI)
if [ "$INSTALL_GROK" -eq 1 ]; then
mkdir -p "$GROK_SKILLS"
create_grok_runtime_root "$SOURCE_GSTACK_DIR" "$GROK_GSTACK"
link_grok_skill_dirs "$SOURCE_GSTACK_DIR" "$GROK_SKILLS"
echo "gstack ready (grok-build)."
echo " browse: $BROWSE_BIN"
echo " grok skills: $GROK_SKILLS"
echo " runtime root: $GROK_GSTACK"
fi
# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs.

View File

@ -788,8 +788,22 @@ Claude Code) can call it as both a CLI and an MCP tool.
**Scope honesty:** This skill's MCP registration step (5a) uses
`claude mcp add` and targets Claude Code specifically. Other local hosts
(Cursor, Codex CLI, etc.) will still get the gbrain CLI on PATH — they can
register `gbrain serve` in their own MCP config manually after setup.
(Cursor, Codex CLI, **Grok Build**, etc.) will still get the gbrain CLI on
PATH — they can register `gbrain serve` in their own MCP config manually
after setup.
**Grok Build success path (no Claude required):** setup is **COMPATIBLE** when
(1) `gbrain` is on PATH, (2) the engine is healthy (`gbrain doctor`), and
(3) AGENTS.md (or project docs) carry gbrain guidance via `/sync-gbrain`.
Claude MCP registration is optional. Never mark setup FAILED only because
`claude` is missing. Optional Grok MCP into `~/.grok/config.toml` is a stretch
(absolute path, structured TOML RMW, gbrain stanza only, atomic write +
backup; non-interactive refuses non-equivalent overwrite unless `--force` or
a real TTY confirm).
**Bridge policy:** install prose alone does not green the primary path when
the CLI is missing — detect/install guidance may be READY; overall bridge is
DEPENDENT until `gbrain` is present.
**Audience:** local-Mac users. openclaw/hermes agents typically run in cloud
docker containers with their own gbrain; "sharing" a brain between them and
@ -1258,9 +1272,14 @@ doctor output and STOP.
---
## Step 5a: Register gbrain as Claude Code MCP (D18)
## Step 5a: Register gbrain as Claude Code MCP (D18) — optional
Only if `which claude` resolves. Ask: "Give Claude Code a typed tool surface
Only if `which claude` resolves. If `claude` is **not** on PATH (typical on
Grok-only machines): emit "MCP registration skipped — Claude Code not present.
Grok/other hosts succeed via CLI + AGENTS.md; register `gbrain serve` in host
MCP config only if desired." Continue to step 6 — **do not fail setup**.
If `claude` is present, ask: "Give Claude Code a typed tool surface
for gbrain? (recommended yes)"
The registration form depends on the path picked in Step 2:

View File

@ -34,8 +34,22 @@ Claude Code) can call it as both a CLI and an MCP tool.
**Scope honesty:** This skill's MCP registration step (5a) uses
`claude mcp add` and targets Claude Code specifically. Other local hosts
(Cursor, Codex CLI, etc.) will still get the gbrain CLI on PATH — they can
register `gbrain serve` in their own MCP config manually after setup.
(Cursor, Codex CLI, **Grok Build**, etc.) will still get the gbrain CLI on
PATH — they can register `gbrain serve` in their own MCP config manually
after setup.
**Grok Build success path (no Claude required):** setup is **COMPATIBLE** when
(1) `gbrain` is on PATH, (2) the engine is healthy (`gbrain doctor`), and
(3) AGENTS.md (or project docs) carry gbrain guidance via `/sync-gbrain`.
Claude MCP registration is optional. Never mark setup FAILED only because
`claude` is missing. Optional Grok MCP into `~/.grok/config.toml` is a stretch
(absolute path, structured TOML RMW, gbrain stanza only, atomic write +
backup; non-interactive refuses non-equivalent overwrite unless `--force` or
a real TTY confirm).
**Bridge policy:** install prose alone does not green the primary path when
the CLI is missing — detect/install guidance may be READY; overall bridge is
DEPENDENT until `gbrain` is present.
**Audience:** local-Mac users. openclaw/hermes agents typically run in cloud
docker containers with their own gbrain; "sharing" a brain between them and
@ -504,9 +518,14 @@ doctor output and STOP.
---
## Step 5a: Register gbrain as Claude Code MCP (D18)
## Step 5a: Register gbrain as Claude Code MCP (D18) — optional
Only if `which claude` resolves. Ask: "Give Claude Code a typed tool surface
Only if `which claude` resolves. If `claude` is **not** on PATH (typical on
Grok-only machines): emit "MCP registration skipped — Claude Code not present.
Grok/other hosts succeed via CLI + AGENTS.md; register `gbrain serve` in host
MCP config only if desired." Continue to step 6 — **do not fail setup**.
If `claude` is present, ask: "Give Claude Code a typed tool surface
for gbrain? (recommended yes)"
The registration form depends on the path picked in Step 2:

View File

@ -60,7 +60,7 @@ separated tokens starting with `--`. Last flag wins on conflict.
| `--no-dedupe` | — | Skip the dedupe check. |
| `--no-gate` | OFF (gate is ON) | Skip the codex quality-score gate between Phase 4 and Phase 5. **Redaction (Phase 4.5a semantic + 4.5b regex) still runs — there is no flag that disables it.** |
| `--audit` | OFF | Route Phase 5 to the Audit/Cleanup template (instead of Standard). |
| `--execute` | conditional default (see Phase 5) | Spawn `claude -p` in a fresh worktree after filing the issue. |
{{SPEC_EXECUTE_FLAG}}
| `--no-execute` | — | File issue only; do NOT spawn agent (alias: `--file-only`). |
| `--file-only` | — | Same as `--no-execute`. |
| `--plan-file <path>` | inferred from harness | Load the spec into the specified plan file instead of inferring. |
@ -447,14 +447,7 @@ git worktree add "$SPAWN_PATH" -b "$SPAWN_BRANCH" "$PIN_SHA" 2>&1
in-progress changes will be visible to the agent. Cancel with Ctrl+C if not
desired." Then fall back to current dir (still spawn).
If A and worktree created: spawn `claude -p` with the spec piped via stdin:
```bash
cat "$ARCHIVE_PATH" | (cd "$SPAWN_PATH" && claude -p 2>&1) &
SPAWN_PID=$!
echo "Spawned: PID $SPAWN_PID in $SPAWN_PATH (branch $SPAWN_BRANCH)"
echo "Follow with: cd $SPAWN_PATH && claude --resume"
```
{{SPEC_SPAWN}}
Update archive frontmatter with `spec_worktree_path: $SPAWN_PATH` and
`spec_executed: true` (atomic re-write).

View File

@ -785,8 +785,12 @@ Skills that run plan reviews (`/plan-*-review`, `/codex review`) include the EXI
You are running the canonical "keep this brain up to date" verb. /setup-gbrain
installs gbrain once; /sync-gbrain runs every time the user wants the brain
refreshed against this repo's current state, and refreshes the agent-side
guidance in CLAUDE.md so the coding agent knows when to prefer `gbrain`
search over Grep.
guidance in CLAUDE.md / AGENTS.md so the coding agent knows when to prefer
`gbrain` search over Grep.
**Grok Build:** success does not require Claude MCP. Require `gbrain` on PATH +
engine healthy + AGENTS.md guidance refresh. If gbrain is absent, give install
steps (DEPENDENT bridge) — do not silent-fail or demand `which claude`.
**Architecture (post-codex review):** This skill uses gbrain v0.20.0+'s
**native code surfaces** (`gbrain sources add`, `gbrain sync --strategy code`,

View File

@ -31,8 +31,12 @@ allowed-tools:
You are running the canonical "keep this brain up to date" verb. /setup-gbrain
installs gbrain once; /sync-gbrain runs every time the user wants the brain
refreshed against this repo's current state, and refreshes the agent-side
guidance in CLAUDE.md so the coding agent knows when to prefer `gbrain`
search over Grep.
guidance in CLAUDE.md / AGENTS.md so the coding agent knows when to prefer
`gbrain` search over Grep.
**Grok Build:** success does not require Claude MCP. Require `gbrain` on PATH +
engine healthy + AGENTS.md guidance refresh. If gbrain is absent, give install
steps (DEPENDENT bridge) — do not silent-fail or demand `which claude`.
**Architecture (post-codex review):** This skill uses gbrain v0.20.0+'s
**native code surfaces** (`gbrain sources add`, `gbrain sync --strategy code`,

View File

@ -19,6 +19,7 @@ GSTACK_ROOT="$HOME/.codex/skills/gstack"
GSTACK_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/dist"
GSTACK_MAKE_PDF="$GSTACK_ROOT/make-pdf/dist"
_UPD=$($GSTACK_BIN/gstack-update-check 2>/dev/null || .agents/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
[ -n "$_UPD" ] && echo "$_UPD" || true
mkdir -p ~/.gstack/sessions

View File

@ -21,6 +21,7 @@ GSTACK_ROOT="$HOME/.factory/skills/gstack"
GSTACK_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/dist"
GSTACK_MAKE_PDF="$GSTACK_ROOT/make-pdf/dist"
_UPD=$($GSTACK_BIN/gstack-update-check 2>/dev/null || .factory/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
[ -n "$_UPD" ] && echo "$_UPD" || true
mkdir -p ~/.gstack/sessions

File diff suppressed because it is too large Load Diff

View File

@ -7,26 +7,29 @@
* one. Per-provider auth/timeout/rate-limit errors don't abort the batch.
*/
import type { ProviderAdapter, RunOpts, RunResult } from './providers/types';
import type { ProviderAdapter, RunOpts, RunResult, Family } from './providers/types';
import { ClaudeAdapter } from './providers/claude';
import { GptAdapter } from './providers/gpt';
import { GeminiAdapter } from './providers/gemini';
import { GrokAdapter } from './providers/grok';
export type ProviderName = 'claude' | 'gpt' | 'gemini' | 'grok';
export interface BenchmarkInput {
prompt: string;
workdir: string;
timeoutMs?: number;
/** Adapter names to run (e.g., ['claude', 'gpt', 'gemini']). */
providers: Array<'claude' | 'gpt' | 'gemini'>;
/** Adapter names to run (e.g., ['claude', 'gpt', 'gemini', 'grok']). */
providers: ProviderName[];
/** Optional per-provider model overrides. */
models?: Partial<Record<'claude' | 'gpt' | 'gemini', string>>;
models?: Partial<Record<ProviderName, string>>;
/** If true, skip providers whose available() returns !ok. If false, include them with error. */
skipUnavailable?: boolean;
}
export interface BenchmarkEntry {
provider: string;
family: 'claude' | 'gpt' | 'gemini';
family: Family;
available: boolean;
unavailable_reason?: string;
result?: RunResult;
@ -44,10 +47,11 @@ export interface BenchmarkReport {
entries: BenchmarkEntry[];
}
const ADAPTERS: Record<'claude' | 'gpt' | 'gemini', () => ProviderAdapter> = {
const ADAPTERS: Record<ProviderName, () => ProviderAdapter> = {
claude: () => new ClaudeAdapter(),
gpt: () => new GptAdapter(),
gemini: () => new GeminiAdapter(),
grok: () => new GrokAdapter(),
};
export async function runBenchmark(input: BenchmarkInput): Promise<BenchmarkReport> {

View File

@ -32,6 +32,10 @@ export const PRICING: Record<string, ModelPricing> = {
// Google
'gemini-2.5-pro': { input_per_mtok: 1.25, output_per_mtok: 5.00, as_of: '2026-04' },
'gemini-2.5-flash': { input_per_mtok: 0.30, output_per_mtok: 1.20, as_of: '2026-04' },
// xAI Grok (placeholder rates — update when public API pricing is pinned)
'grok': { input_per_mtok: 3.00, output_per_mtok: 15.00, as_of: '2026-07' },
'grok-4': { input_per_mtok: 3.00, output_per_mtok: 15.00, as_of: '2026-07' },
};
const WARNED = new Set<string>();

View File

@ -0,0 +1,112 @@
import type { ProviderAdapter, RunOpts, RunResult, AvailabilityCheck } from './types';
import { estimateCostUsd } from '../pricing';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
/**
* Grok adapter wraps the `grok` CLI via -p / --single.
*
* Auth readiness is boolean only: CLI present + (~/.grok/auth.json OR
* XAI_API_KEY / GROK_API_KEY env names present). Never log token values.
*/
export class GrokAdapter implements ProviderAdapter {
readonly name = 'grok';
readonly family = 'grok' as const;
async available(): Promise<AvailabilityCheck> {
// Boolean PATH presence only — never log secrets
let hasBinary = false;
try {
execFileSync('which', ['grok'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
hasBinary = true;
} catch {
try {
execFileSync('grok', ['--version'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 2000,
});
hasBinary = true;
} catch {
hasBinary = false;
}
}
if (!hasBinary) {
return {
ok: false,
reason: 'grok CLI not found on PATH. Install Grok Build from xAI, or ensure `grok` is on PATH.',
};
}
const authPath = path.join(os.homedir(), '.grok', 'auth.json');
const hasAuthFile = fs.existsSync(authPath);
// Presence of env *names* only — never read/log values
const hasKey = !!(process.env.XAI_API_KEY || process.env.GROK_API_KEY);
if (!hasAuthFile && !hasKey) {
return {
ok: false,
reason: 'No Grok auth found. Log in via `grok` interactive session, or export XAI_API_KEY / GROK_API_KEY.',
};
}
return { ok: true };
}
async run(opts: RunOpts): Promise<RunResult> {
const start = Date.now();
// Short single-turn: -p / --single. Prefer file-free short prompts for benchmark.
const args = ['--single', opts.prompt];
if (opts.model) args.push('--model', opts.model);
if (opts.extraArgs) args.push(...opts.extraArgs);
try {
const out = execFileSync('grok', args, {
cwd: opts.workdir,
timeout: opts.timeoutMs,
encoding: 'utf-8',
maxBuffer: 32 * 1024 * 1024,
env: { ...process.env, GSTACK_HEADLESS: '1' },
});
return {
output: typeof out === 'string' ? out : String(out),
tokens: { input: 0, output: 0 },
durationMs: Date.now() - start,
toolCalls: 0,
modelUsed: opts.model || 'grok',
};
} catch (err: unknown) {
const durationMs = Date.now() - start;
const e = err as { code?: string; stderr?: Buffer; signal?: string; message?: string };
const stderr = e.stderr?.toString() ?? '';
if (e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT') {
return this.emptyResult(durationMs, { code: 'timeout', reason: `exceeded ${opts.timeoutMs}ms` }, opts.model);
}
if (/unauthorized|auth|login|api.?key/i.test(stderr)) {
return this.emptyResult(durationMs, { code: 'auth', reason: stderr.slice(0, 400) }, opts.model);
}
if (/rate[- ]?limit|429/i.test(stderr)) {
return this.emptyResult(durationMs, { code: 'rate_limit', reason: stderr.slice(0, 400) }, opts.model);
}
if (/ENOENT|not found/i.test(e.message ?? '') || e.code === 'ENOENT') {
return this.emptyResult(durationMs, { code: 'binary_missing', reason: 'grok CLI not found' }, opts.model);
}
return this.emptyResult(durationMs, { code: 'unknown', reason: (e.message ?? stderr ?? 'unknown').slice(0, 400) }, opts.model);
}
}
estimateCost(tokens: { input: number; output: number; cached?: number }, model?: string): number {
return estimateCostUsd(tokens, model ?? 'grok');
}
private emptyResult(durationMs: number, error: RunResult['error'], model?: string): RunResult {
return {
output: '',
tokens: { input: 0, output: 0 },
durationMs,
toolCalls: 0,
modelUsed: model || 'grok',
error,
};
}
}

View File

@ -55,7 +55,7 @@ export interface AvailabilityCheck {
reason?: string;
}
export type Family = 'claude' | 'gpt' | 'gemini';
export type Family = 'claude' | 'gpt' | 'gemini' | 'grok';
export interface ProviderAdapter {
/** Stable name used in output tables and config (e.g., 'claude', 'gpt', 'gemini'). */

View File

@ -22,16 +22,18 @@ import {
slate,
cursor,
openclaw,
grokBuild,
} from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types';
import { resolveDistBinary } from '../scripts/resolvers/browse';
const ROOT = path.resolve(import.meta.dir, '..');
// ─── hosts/index.ts ─────────────────────────────────────────
describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 10 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(10);
test('ALL_HOST_CONFIGS has 11 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(11);
});
test('ALL_HOST_NAMES matches config names', () => {
@ -409,6 +411,12 @@ describe('golden-file regression', () => {
const current = fs.readFileSync(path.join(ROOT, '.factory', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden);
});
test('Grok Build ship skill matches golden baseline', () => {
const golden = fs.readFileSync(path.join(GOLDEN_DIR, 'grok-build-ship-SKILL.md'), 'utf-8');
const current = fs.readFileSync(path.join(ROOT, '.grok', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden);
});
});
// ─── Individual host config correctness ─────────────────────
@ -535,4 +543,43 @@ describe('host config correctness', () => {
expect(config.runtimeRoot.globalSymlinks).toContain('ETHOS.md');
}
});
test('grok-build runtime root lists complete packaging assets (U1)', () => {
expect(grokBuild.name).toBe('grok-build');
expect(grokBuild.usesEnvVars).toBe(true);
const links = grokBuild.runtimeRoot.globalSymlinks;
for (const asset of [
'browse/src',
'design/dist',
'make-pdf/dist',
'extension',
'scripts',
'review/specialists',
]) {
expect(links).toContain(asset);
}
expect(grokBuild.runtimeRoot.globalFiles?.review).toContain('checklist.md');
expect(grokBuild.runtimeRoot.globalFiles?.review).toContain('design-checklist.md');
expect(grokBuild.generation.skipSkills).toContain('codex');
});
test('grok-build path rewrites suppress Claude host bleed (U3)', () => {
expect(grokBuild.pathRewrites.some(r => r.from === 'CLAUDE.md' && r.to === 'AGENTS.md')).toBe(true);
expect(grokBuild.pathRewrites.some(r => r.from === 'MODEL_OVERLAY: claude' && r.to === 'MODEL_OVERLAY: none')).toBe(true);
expect(grokBuild.toolRewrites?.['AskUserQuestion']).toBe('ask_user_question');
});
});
describe('resolveDistBinary (U2 double-home fix)', () => {
test('env-var browseDir never prefixes $HOME', () => {
expect(resolveDistBinary('$GSTACK_BROWSE', 'browse')).toBe('$GSTACK_BROWSE/browse');
expect(resolveDistBinary('$GSTACK_DESIGN', 'design')).toBe('$GSTACK_DESIGN/design');
expect(resolveDistBinary('$GSTACK_MAKE_PDF', 'pdf')).toBe('$GSTACK_MAKE_PDF/pdf');
});
test('tilde paths keep $HOME prefix', () => {
expect(resolveDistBinary('~/.claude/skills/gstack/browse/dist', 'browse')).toBe(
'$HOME/.claude/skills/gstack/browse/dist/browse',
);
});
});