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/
|
.opencode/
|
||||||
.slate/
|
.slate/
|
||||||
.cursor/
|
.cursor/
|
||||||
|
.grok/
|
||||||
.openclaw/
|
.openclaw/
|
||||||
.hermes/
|
.hermes/
|
||||||
.gbrain/
|
.gbrain/
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
* 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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 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 };
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
`
|
`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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'). */
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue