mirror of https://github.com/garrytan/gstack.git
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:
parent
11de390be1
commit
13d2f7dcdf
|
|
@ -19,6 +19,7 @@ bin/gstack-global-discover*
|
|||
.opencode/
|
||||
.slate/
|
||||
.cursor/
|
||||
.grok/
|
||||
.openclaw/
|
||||
.hermes/
|
||||
.gbrain/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
\`\`\`
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
`
|
||||
: '';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
244
setup
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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'). */
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue