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/ .opencode/
.slate/ .slate/
.cursor/ .cursor/
.grok/
.openclaw/ .openclaw/
.hermes/ .hermes/
.gbrain/ .gbrain/

View File

@ -20,11 +20,12 @@ allowed-tools:
## When to invoke this skill ## When to invoke this skill
Runs the same prompt through Claude, Runs the same prompt through Claude,
GPT (via Codex CLI), and Gemini side-by-side — compares latency, tokens, cost, GPT (via Codex CLI), Gemini, and Grok side-by-side — compares latency, tokens,
and optionally quality via LLM judge. Answers "which model is actually best cost, and optionally quality via LLM judge. Answers "which model is actually
for this skill?" with data instead of vibes. Separate from /benchmark, which best for this skill?" with data instead of vibes. Separate from /benchmark,
measures web page performance. Use when: "benchmark models", "compare models", which measures web page performance. Use when: "benchmark models", "compare
"which model is best for X", "cross-model comparison", "model shootout". 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". 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 ## Step 2: Choose providers
```bash ```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). 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: 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." - **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:** - **Options:**
- A) All authed providers. Completeness: 10/10. - 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). - 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. - A) Enable judge (adds ~$0.05). Completeness: 10/10.
- B) Skip judge — speed/cost/tokens only. Completeness: 7/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 version: 1.0.0
description: | description: |
Cross-model benchmark for gstack skills. Runs the same prompt through Claude, 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, GPT (via Codex CLI), Gemini, and Grok side-by-side — compares latency, tokens,
and optionally quality via LLM judge. Answers "which model is actually best cost, and optionally quality via LLM judge. Answers "which model is actually
for this skill?" with data instead of vibes. Separate from /benchmark, which best for this skill?" with data instead of vibes. Separate from /benchmark,
measures web page performance. Use when: "benchmark models", "compare models", which measures web page performance. Use when: "benchmark models", "compare
"which model is best for X", "cross-model comparison", "model shootout". (gstack) models", "which model is best for X", "cross-model comparison", "model
shootout". (gstack)
voice-triggers: voice-triggers:
- "compare models" - "compare models"
- "model shootout" - "model shootout"
@ -69,12 +70,12 @@ If C: ask for the path. Verify it exists. Use as positional argument.
## Step 2: Choose providers ## Step 2: Choose providers
```bash ```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). 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: 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." - **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:** - **Options:**
- A) All authed providers. Completeness: 10/10. - 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). - 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. - A) Enable judge (adds ~$0.05). Completeness: 10/10.
- B) Skip judge — speed/cost/tokens only. Completeness: 7/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] * gstack-model-benchmark <skill-or-prompt-file> [options]
* *
* 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 * --prompt "<text>" Inline prompt instead of a file
* --workdir <path> Working dir passed to each CLI (default: cwd) * --workdir <path> Working dir passed to each CLI (default: cwd)
* --timeout-ms <n> Per-provider timeout (default: 300000) * --timeout-ms <n> Per-provider timeout (default: 300000)
@ -15,27 +15,29 @@
* --skip-unavailable Skip providers that fail available() check * --skip-unavailable Skip providers that fail available() check
* (default: include them with unavailable marker) * (default: include them with unavailable marker)
* --judge Run Anthropic SDK judge on outputs for quality score * --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 * --dry-run Validate flags + resolve auth, don't invoke providers
* *
* Examples: * Examples:
* gstack-model-benchmark --prompt "Write a haiku about databases" --models claude,gpt * 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 ./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 '../lib/conductor-env-shim';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; 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 { ClaudeAdapter } from '../test/helpers/providers/claude';
import { GptAdapter } from '../test/helpers/providers/gpt'; import { GptAdapter } from '../test/helpers/providers/gpt';
import { GeminiAdapter } from '../test/helpers/providers/gemini'; 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(), claude: () => new ClaudeAdapter(),
gpt: () => new GptAdapter(), gpt: () => new GptAdapter(),
gemini: () => new GeminiAdapter(), gemini: () => new GeminiAdapter(),
grok: () => new GrokAdapter(),
}; };
type OutputFormat = 'table' | 'json' | 'markdown'; type OutputFormat = 'table' | 'json' | 'markdown';
@ -76,13 +78,13 @@ function positionalArgs(args: string[]): string[] {
return positional; return positional;
} }
function parseProviders(s: string | undefined): Array<'claude' | 'gpt' | 'gemini'> { function parseProviders(s: string | undefined): ProviderName[] {
if (!s) return ['claude']; 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)) { 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 { 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']; return seen.size ? Array.from(seen) : ['claude'];
@ -129,11 +131,16 @@ async function main(): Promise<void> {
const report = await runBenchmark(input); const report = await runBenchmark(input);
if (doJudge) { if (doJudge) {
try { // Grok-or-skip: judge requires Anthropic; Grok-only machines skip cleanly
const { judgeEntries } = await import('../test/helpers/benchmark-judge'); if (!process.env.ANTHROPIC_API_KEY) {
await judgeEntries(report); console.error('WARN: judge skipped — ANTHROPIC_API_KEY not set (Grok-or-skip judge).');
} catch (err) { } else {
console.error(`WARN: judge unavailable: ${(err as Error).message}`); 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: { async function dryRunReport(opts: {
prompt: string; prompt: string;
providers: Array<'claude' | 'gpt' | 'gemini'>; providers: ProviderName[];
workdir: string; workdir: string;
timeoutMs: number; timeoutMs: number;
output: OutputFormat; output: OutputFormat;

View File

@ -30,6 +30,11 @@ modify files.
The generated external invocation name is `gstack-claude`. 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 ## 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 openclaw from './openclaw';
import hermes from './hermes'; import hermes from './hermes';
import gbrain from './gbrain'; import gbrain from './gbrain';
import grokBuild from './grok-build';
/** All registered host configs. Add new hosts here. */ /** 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. */ /** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries( 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 // 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. 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 ## Step 5: Verify connection
After the user pastes the instructions into the other agent, wait a moment then check: 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. 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 ## Step 5: Verify connection
After the user pastes the instructions into the other agent, wait a moment then check: 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 Overlay Selection ────────────────────────────────
// --model is explicit. We do NOT auto-detect from host (host ≠ model). // --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'; import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
const MODEL_ARG = process.argv.find(a => a.startsWith('--model')); const MODEL_ARG = process.argv.find(a => a.startsWith('--model'));
const MODEL_EXPLICIT = !!MODEL_ARG;
const MODEL_ARG_VAL: Model = (() => { const MODEL_ARG_VAL: Model = (() => {
if (!MODEL_ARG) return 'claude'; if (!MODEL_ARG) return 'claude';
const val = MODEL_ARG.includes('=') ? MODEL_ARG.split('=')[1] : process.argv[process.argv.indexOf(MODEL_ARG) + 1]; 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 preambleTier = tierMatch ? parseInt(tierMatch[1], 10) : undefined;
const interactiveMatch = tmplContent.match(/^interactive:\s*(true|false)\s*$/m); const interactiveMatch = tmplContent.match(/^interactive:\s*(true|false)\s*$/m);
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined; 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 { return {
skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host], 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": { "benchmark-models": {
"lead": "Cross-model benchmark for gstack skills.", "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\"." "voice_line": "Voice triggers (speech-to-text aliases): \"compare models\", \"model shootout\", \"which model is best\"."
}, },
"browse": { "browse": {

View File

@ -99,14 +99,24 @@ export function generateSnapshotFlags(_ctx: TemplateContext): string {
return lines.join('\n'); 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 { export function generateBrowseSetup(ctx: TemplateContext): string {
const globalBrowse = resolveDistBinary(ctx.paths.browseDir, 'browse');
return `## SETUP (run this check BEFORE any browse command) return `## SETUP (run this check BEFORE any browse command)
\`\`\`bash \`\`\`bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) _ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
B="" B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" ] && B="$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" [ -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 if [ -x "$B" ]; then
echo "READY: $B" echo "READY: $B"
else else

View File

@ -1,5 +1,6 @@
import type { TemplateContext } from './types'; import type { TemplateContext } from './types';
import { AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS } from './constants'; import { AI_SLOP_BLACKLIST, OPENAI_HARD_REJECTIONS, OPENAI_LITMUS_CHECKS } from './constants';
import { resolveDistBinary } from './browse';
export function generateDesignReviewLite(ctx: TemplateContext): string { export function generateDesignReviewLite(ctx: TemplateContext): string {
const litmusList = OPENAI_LITMUS_CHECKS.map((item, i) => `${i + 1}. ${item}`).join(' '); 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 { 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) return `## DESIGN SETUP (run this check BEFORE any design mockup command)
\`\`\`bash \`\`\`bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) _ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
D="" D=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" [ -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 if [ -x "$D" ]; then
echo "DESIGN_READY: $D" echo "DESIGN_READY: $D"
else else
@ -800,7 +803,7 @@ else
fi fi
B="" B=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" ] && B="$_ROOT/${ctx.paths.localSkillRoot}/browse/dist/browse" [ -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 if [ -x "$B" ]; then
echo "BROWSE_READY: $B" echo "BROWSE_READY: $B"
else else
@ -831,13 +834,14 @@ data, not project files. They persist across branches, conversations, and worksp
} }
export function generateDesignMockup(ctx: TemplateContext): string { export function generateDesignMockup(ctx: TemplateContext): string {
const globalDesign = resolveDistBinary(ctx.paths.designDir, 'design');
return `## Visual Design Exploration return `## Visual Design Exploration
\`\`\`bash \`\`\`bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) _ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
D="" D=""
[ -n "$_ROOT" ] && [ -x "$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" ] && D="$_ROOT/${ctx.paths.localSkillRoot}/design/dist/design" [ -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" [ -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 { generateTasksSectionEmit, generateTasksSectionAggregate } from './tasks-section';
import { SECTION, SECTION_INDEX } from './sections'; import { SECTION, SECTION_INDEX } from './sections';
import { generateRedactTaxonomyTable, generateRedactInvocationBlock } from './redact-doc'; import { generateRedactTaxonomyTable, generateRedactInvocationBlock } from './redact-doc';
import { generateSpecSpawn, generateSpecExecuteFlag } from './spec-spawn';
export const RESOLVERS: Record<string, ResolverValue> = { export const RESOLVERS: Record<string, ResolverValue> = {
SLUG_EVAL: generateSlugEval, SLUG_EVAL: generateSlugEval,
@ -100,6 +101,8 @@ export const RESOLVERS: Record<string, ResolverValue> = {
MAKE_PDF_SETUP: generateMakePdfSetup, MAKE_PDF_SETUP: generateMakePdfSetup,
TASKS_SECTION_EMIT: generateTasksSectionEmit, TASKS_SECTION_EMIT: generateTasksSectionEmit,
TASKS_SECTION_AGGREGATE: generateTasksSectionAggregate, TASKS_SECTION_AGGREGATE: generateTasksSectionAggregate,
SPEC_SPAWN: generateSpecSpawn,
SPEC_EXECUTE_FLAG: generateSpecExecuteFlag,
SECTION, SECTION,
SECTION_INDEX, SECTION_INDEX,
}; };

View File

@ -1,4 +1,5 @@
import type { TemplateContext } from './types'; import type { TemplateContext } from './types';
import { resolveDistBinary } from './browse';
/** /**
* {{MAKE_PDF_SETUP}} emits the shell preamble that resolves $P to the * {{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): * Resolution order (matches src/browseClient.ts::resolveBrowseBin):
* 1. Local skill root: $_ROOT/{localSkillRoot}/make-pdf/dist/pdf * 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 * 3. Env override (MAKE_PDF_BIN) for contributor dev builds
*/ */
export function generateMakePdfSetup(ctx: TemplateContext): string { 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) return `## MAKE-PDF SETUP (run this check BEFORE any make-pdf command)
\`\`\`bash \`\`\`bash
@ -19,7 +21,7 @@ _ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
P="" P=""
[ -n "$MAKE_PDF_BIN" ] && [ -x "$MAKE_PDF_BIN" ] && P="$MAKE_PDF_BIN" [ -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" ] && [ -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 if [ -x "$P" ]; then
echo "MAKE_PDF_READY: $P" echo "MAKE_PDF_READY: $P"
alias _p_="$P" # shellcheck alias helper (not exported) 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_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist" GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/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" FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
OPENCODE_SKILLS="$HOME/.config/opencode/skills" OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack" OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
GROK_SKILLS="$HOME/.grok/skills"
GROK_GSTACK="$GROK_SKILLS/gstack"
IS_WINDOWS=0 IS_WINDOWS=0
case "$(uname -s)" in case "$(uname -s)" in
@ -85,7 +87,7 @@ NO_TEAM_MODE=0
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in 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 ;; --host=*) HOST="${1#--host=}"; shift ;;
--local) LOCAL_INSTALL=1; shift ;; --local) LOCAL_INSTALL=1; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;; --prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
@ -100,8 +102,11 @@ while [ $# -gt 0 ]; do
esac esac
done done
# Alias: --host grok → grok-build
if [ "$HOST" = "grok" ]; then HOST="grok-build"; fi
case "$HOST" in case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;; claude|codex|kiro|factory|opencode|grok-build|auto) ;;
openclaw) openclaw)
echo "" echo ""
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code" 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 "GBrain setup and brain skills ship from the GBrain repo."
echo "" echo ""
exit 0 ;; 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 esac
# ─── Resolve skill prefix preference ───────────────────────── # ─── Resolve skill prefix preference ─────────────────────────
@ -200,14 +205,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0 INSTALL_KIRO=0
INSTALL_FACTORY=0 INSTALL_FACTORY=0
INSTALL_OPENCODE=0 INSTALL_OPENCODE=0
INSTALL_GROK=0
if [ "$HOST" = "auto" ]; then if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1 command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1 command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1 command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1 command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=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 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 INSTALL_CLAUDE=1
fi fi
elif [ "$HOST" = "claude" ]; then elif [ "$HOST" = "claude" ]; then
@ -220,6 +227,8 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1 INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1 INSTALL_OPENCODE=1
elif [ "$HOST" = "grok-build" ]; then
INSTALL_GROK=1
fi fi
migrate_direct_codex_install() { migrate_direct_codex_install() {
@ -475,6 +484,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
) )
fi 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 # 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then if ! ensure_playwright_browser; then
echo "Installing Playwright Chromium..." echo "Installing Playwright Chromium..."
@ -972,6 +991,212 @@ link_opencode_skill_dirs() {
fi 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) # 4. Install for Claude (default)
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")" SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$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" echo " opencode skills: $OPENCODE_SKILLS"
fi 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. # 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, # 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. # 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 **Scope honesty:** This skill's MCP registration step (5a) uses
`claude mcp add` and targets Claude Code specifically. Other local hosts `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 (Cursor, Codex CLI, **Grok Build**, etc.) will still get the gbrain CLI on
register `gbrain serve` in their own MCP config manually after setup. 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 **Audience:** local-Mac users. openclaw/hermes agents typically run in cloud
docker containers with their own gbrain; "sharing" a brain between them and 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)" for gbrain? (recommended yes)"
The registration form depends on the path picked in Step 2: 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 **Scope honesty:** This skill's MCP registration step (5a) uses
`claude mcp add` and targets Claude Code specifically. Other local hosts `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 (Cursor, Codex CLI, **Grok Build**, etc.) will still get the gbrain CLI on
register `gbrain serve` in their own MCP config manually after setup. 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 **Audience:** local-Mac users. openclaw/hermes agents typically run in cloud
docker containers with their own gbrain; "sharing" a brain between them and 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)" for gbrain? (recommended yes)"
The registration form depends on the path picked in Step 2: 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-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.** | | `--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). | | `--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`). | | `--no-execute` | — | File issue only; do NOT spawn agent (alias: `--file-only`). |
| `--file-only` | — | Same as `--no-execute`. | | `--file-only` | — | Same as `--no-execute`. |
| `--plan-file <path>` | inferred from harness | Load the spec into the specified plan file instead of inferring. | | `--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 in-progress changes will be visible to the agent. Cancel with Ctrl+C if not
desired." Then fall back to current dir (still spawn). desired." Then fall back to current dir (still spawn).
If A and worktree created: spawn `claude -p` with the spec piped via stdin: {{SPEC_SPAWN}}
```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"
```
Update archive frontmatter with `spec_worktree_path: $SPAWN_PATH` and Update archive frontmatter with `spec_worktree_path: $SPAWN_PATH` and
`spec_executed: true` (atomic re-write). `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 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 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 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` guidance in CLAUDE.md / AGENTS.md so the coding agent knows when to prefer
search over Grep. `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 **Architecture (post-codex review):** This skill uses gbrain v0.20.0+'s
**native code surfaces** (`gbrain sources add`, `gbrain sync --strategy code`, **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 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 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 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` guidance in CLAUDE.md / AGENTS.md so the coding agent knows when to prefer
search over Grep. `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 **Architecture (post-codex review):** This skill uses gbrain v0.20.0+'s
**native code surfaces** (`gbrain sources add`, `gbrain sync --strategy code`, **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_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist" GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/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) _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 [ -n "$_UPD" ] && echo "$_UPD" || true
mkdir -p ~/.gstack/sessions mkdir -p ~/.gstack/sessions

View File

@ -21,6 +21,7 @@ GSTACK_ROOT="$HOME/.factory/skills/gstack"
GSTACK_BIN="$GSTACK_ROOT/bin" GSTACK_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist" GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
GSTACK_DESIGN="$GSTACK_ROOT/design/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) _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 [ -n "$_UPD" ] && echo "$_UPD" || true
mkdir -p ~/.gstack/sessions 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. * 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 { ClaudeAdapter } from './providers/claude';
import { GptAdapter } from './providers/gpt'; import { GptAdapter } from './providers/gpt';
import { GeminiAdapter } from './providers/gemini'; import { GeminiAdapter } from './providers/gemini';
import { GrokAdapter } from './providers/grok';
export type ProviderName = 'claude' | 'gpt' | 'gemini' | 'grok';
export interface BenchmarkInput { export interface BenchmarkInput {
prompt: string; prompt: string;
workdir: string; workdir: string;
timeoutMs?: number; timeoutMs?: number;
/** Adapter names to run (e.g., ['claude', 'gpt', 'gemini']). */ /** Adapter names to run (e.g., ['claude', 'gpt', 'gemini', 'grok']). */
providers: Array<'claude' | 'gpt' | 'gemini'>; providers: ProviderName[];
/** Optional per-provider model overrides. */ /** 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. */ /** If true, skip providers whose available() returns !ok. If false, include them with error. */
skipUnavailable?: boolean; skipUnavailable?: boolean;
} }
export interface BenchmarkEntry { export interface BenchmarkEntry {
provider: string; provider: string;
family: 'claude' | 'gpt' | 'gemini'; family: Family;
available: boolean; available: boolean;
unavailable_reason?: string; unavailable_reason?: string;
result?: RunResult; result?: RunResult;
@ -44,10 +47,11 @@ export interface BenchmarkReport {
entries: BenchmarkEntry[]; entries: BenchmarkEntry[];
} }
const ADAPTERS: Record<'claude' | 'gpt' | 'gemini', () => ProviderAdapter> = { const ADAPTERS: Record<ProviderName, () => ProviderAdapter> = {
claude: () => new ClaudeAdapter(), claude: () => new ClaudeAdapter(),
gpt: () => new GptAdapter(), gpt: () => new GptAdapter(),
gemini: () => new GeminiAdapter(), gemini: () => new GeminiAdapter(),
grok: () => new GrokAdapter(),
}; };
export async function runBenchmark(input: BenchmarkInput): Promise<BenchmarkReport> { export async function runBenchmark(input: BenchmarkInput): Promise<BenchmarkReport> {

View File

@ -32,6 +32,10 @@ export const PRICING: Record<string, ModelPricing> = {
// Google // Google
'gemini-2.5-pro': { input_per_mtok: 1.25, output_per_mtok: 5.00, as_of: '2026-04' }, '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' }, '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>(); 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; reason?: string;
} }
export type Family = 'claude' | 'gpt' | 'gemini'; export type Family = 'claude' | 'gpt' | 'gemini' | 'grok';
export interface ProviderAdapter { export interface ProviderAdapter {
/** Stable name used in output tables and config (e.g., 'claude', 'gpt', 'gemini'). */ /** Stable name used in output tables and config (e.g., 'claude', 'gpt', 'gemini'). */

View File

@ -22,16 +22,18 @@ import {
slate, slate,
cursor, cursor,
openclaw, openclaw,
grokBuild,
} from '../hosts/index'; } from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types'; import { HOST_PATHS } from '../scripts/resolvers/types';
import { resolveDistBinary } from '../scripts/resolvers/browse';
const ROOT = path.resolve(import.meta.dir, '..'); const ROOT = path.resolve(import.meta.dir, '..');
// ─── hosts/index.ts ───────────────────────────────────────── // ─── hosts/index.ts ─────────────────────────────────────────
describe('hosts/index.ts', () => { describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 10 hosts', () => { test('ALL_HOST_CONFIGS has 11 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(10); expect(ALL_HOST_CONFIGS.length).toBe(11);
}); });
test('ALL_HOST_NAMES matches config names', () => { 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'); const current = fs.readFileSync(path.join(ROOT, '.factory', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden); 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 ───────────────────── // ─── Individual host config correctness ─────────────────────
@ -535,4 +543,43 @@ describe('host config correctness', () => {
expect(config.runtimeRoot.globalSymlinks).toContain('ETHOS.md'); 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',
);
});
}); });