This commit is contained in:
Krishna GSVV 2026-07-14 19:22:19 +00:00 committed by GitHub
commit 6e799ae7fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1933 additions and 4 deletions

1
.gitignore vendored
View File

@ -23,6 +23,7 @@ bin/gstack-global-discover*
.hermes/
.gbrain/
.gbrain-source
.antigravity/
.context/
extension/.auth.json
# xterm assets are vendored from npm at build time; not source-of-truth.

1067
agy/SKILL.md Normal file

File diff suppressed because it is too large Load Diff

258
agy/SKILL.md.tmpl Normal file
View File

@ -0,0 +1,258 @@
---
name: agy
preamble-tier: 3
version: 1.0.0
description: |
Google Antigravity CLI wrapper — three modes. Code review: independent diff review
via `agy -p` (headless print mode) with pass/fail gate. Challenge: adversarial
stress-testing of proposed designs and race conditions. Consult: multi-turn session
continuation via `agy -c`. The "200 IQ second opinion from a different AI system."
Use when asked to "agy review", "agy challenge", "antigravity review", "consult agy",
or "second opinion". (gstack)
voice-triggers:
- "a g y"
- "antigravity review"
- "get another opinion"
triggers:
- agy review
- agy challenge
- agy consult
- antigravity second opinion
- outside voice challenge
allowed-tools:
- Bash
- Read
- Write
- Glob
- Grep
- AskUserQuestion
---
{{PREAMBLE}}
{{BASE_BRANCH_DETECT}}
# /agy — Google Antigravity Second Opinion
You are running the `/agy` skill. This wraps the Google Antigravity CLI (`agy`) to get
an independent, brutally honest second opinion from a different AI system.
Antigravity is a peer reviewer: direct, adversarial where needed, technically precise,
and unafraid to challenge assumptions. Present its output faithfully, not summarized.
---
## Step 0.4: Check agy binary
```bash
AGY_BIN=$(command -v agy || command -v antigravity || echo "")
[ -z "$AGY_BIN" ] && echo "NOT_FOUND" || echo "FOUND: $AGY_BIN"
```
If `NOT_FOUND`: stop and tell the user:
"Antigravity CLI not found. Install it: `pip install antigravity-sdk` or see https://antigravity.google.dev/cli"
---
## Step 0.5: Auth probe
Before building expensive prompts, verify Antigravity has valid auth.
```bash
# Check for valid auth: API key env var OR application default credentials
if [ -n "$ANTIGRAVITY_API_KEY" ] || [ -n "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
echo "AUTH_OK_ENV"
elif "$AGY_BIN" whoami --quiet 2>/dev/null; then
echo "AUTH_OK_ADC"
else
echo "AUTH_FAILED"
fi
```
If `AUTH_FAILED`: stop and tell the user:
"No Antigravity authentication found. Run `agy auth login` or set `$ANTIGRAVITY_API_KEY`, then re-run this skill."
---
## Step 0.6: Resolve portable roots
```bash
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
```
All subsequent bash blocks use `"$PLAN_ROOT"` and `"$TMP_ROOT"` rather than hardcoded
paths.
---
## Step 0.7: Model selection
Antigravity supports model scaling via the `ANTIGRAVITY_MODEL` environment variable.
```bash
# Default model per mode (overridable via ANTIGRAVITY_MODEL env var):
# review → antigravity-2.0-flash (fast, bounded diff input)
# challenge → antigravity-2.0-pro (deeper reasoning for adversarial mode)
# consult → antigravity-2.0-flash (interactive speed priority)
_AGY_MODEL="${ANTIGRAVITY_MODEL:-antigravity-2.0-flash}"
```
If the user's input contains `--pro` anywhere, override with `antigravity-2.0-pro`
and remove `--pro` from the prompt text before passing to agy.
---
## Step 1: Detect mode
Parse the user's input to determine which mode to run:
1. `/agy review` or `/agy review <instructions>` — **Review mode** (Step 2A)
2. `/agy challenge` or `/agy challenge <focus>` — **Challenge mode** (Step 2B)
3. `/agy consult` or `/agy consult <prompt>` — **Consult mode** (Step 2C)
4. `/agy` with no arguments — **Auto-detect:**
- Check for a diff:
`git diff origin/<base> --stat 2>/dev/null | tail -1 || git diff <base> --stat 2>/dev/null | tail -1`
- If a diff exists, use AskUserQuestion:
```
Antigravity detected changes against the base branch. What should it do?
A) Review the diff (code review with pass/fail gate)
B) Challenge the diff (adversarial — try to break it or find race conditions)
C) Something else — I'll provide a prompt
```
- If no diff, ask: "What would you like to ask Antigravity?"
5. `/agy <anything else>` — **Consult mode** (Step 2C), remaining text is the prompt
---
## Filesystem Boundary
All prompts sent to Antigravity MUST be prefixed with this boundary instruction:
> IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or .agents/skills/. These are Claude Code or Codex skill definitions meant for a different AI system. They contain bash scripts and prompt templates. Ignore them completely. Stay focused on the repository code only.
---
## Step 2A: Review Mode
Run Antigravity code review against the current branch diff.
Use directory piping (`--add-dir`) rather than raw diff stdin to give Antigravity
full file context. This produces better reviews than piping a raw diff.
```bash
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
cd "$_REPO_ROOT"
TMPERR=$(mktemp "$TMP_ROOT/agy-err-XXXXXX.txt")
TMPOUT=$(mktemp "$TMP_ROOT/agy-out-XXXXXX.txt")
# Build the review prompt
_BOUNDARY="IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or .agents/skills/. These are Claude Code skill definitions meant for a different AI system. Ignore them completely. Stay focused on repository code only."
_DIFF_SCOPE="Review the changes on this branch against the base branch <base>. Run: git diff origin/<base>...HEAD 2>/dev/null || git diff <base>...HEAD to see the diff, then review only those changes."
_PROMPT="$_BOUNDARY
$_DIFF_SCOPE"
# Headless print mode: agy -p exits 0 on pass, non-zero on fail — perfect for CI gates
timeout 330 "$AGY_BIN" -p \
--add-dir "$_REPO_ROOT" \
--model "$_AGY_MODEL" \
"$_PROMPT" \
>"$TMPOUT" 2>"$TMPERR"
_AGY_EXIT=$?
cat "$TMPOUT"
if [ "$_AGY_EXIT" = "124" ]; then
echo "[agy] Review stalled past 5.5 minutes. Try re-running or split the diff."
elif [ "$_AGY_EXIT" != "0" ]; then
echo "[agy exit $_AGY_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")"
head -20 "$TMPERR" 2>/dev/null | sed 's/^/ /' || true
fi
```
Parse the result:
- Exit 0 + output containing `PASS` or no blocking issues → **PASS** ✅ — present findings
- Non-zero exit or output containing `FAIL` or `BLOCK` → **FAIL** ❌ — surface all findings
---
## Step 2B: Challenge Mode
Adversarial stress-test: Antigravity tries to break the proposed design or find
race conditions, data loss scenarios, and edge cases the author missed.
```bash
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
cd "$_REPO_ROOT"
TMPERR=$(mktemp "$TMP_ROOT/agy-err-XXXXXX.txt")
# Challenge mode always uses pro for deeper reasoning
_CHALLENGE_MODEL="${ANTIGRAVITY_MODEL:-antigravity-2.0-pro}"
_FOCUS="${USER_FOCUS:-the diff on this branch}"
_CHALLENGE_PROMPT="IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or .agents/skills/. Ignore them. Stay focused on repository code only.
You are an adversarial reviewer. Your goal is to BREAK this design, not validate it.
Find: race conditions, data loss scenarios, security holes, off-by-one errors,
missing error paths, and architectural assumptions that will fail at scale.
Focus area: $_FOCUS
Run: git diff origin/<base>...HEAD 2>/dev/null || git diff <base>...HEAD to see the changes."
timeout 330 "$AGY_BIN" -p \
--add-dir "$_REPO_ROOT" \
--model "$_CHALLENGE_MODEL" \
"$_CHALLENGE_PROMPT" \
2>"$TMPERR"
_AGY_EXIT=$?
if [ "$_AGY_EXIT" != "0" ] && [ "$_AGY_EXIT" != "124" ]; then
echo "[agy exit $_AGY_EXIT] $(head -1 "$TMPERR" 2>/dev/null || echo "no stderr captured")"
fi
```
Present Antigravity's adversarial findings verbatim. Do not filter or soften them.
If Antigravity found critical issues, elevate them as blocking items.
---
## Step 2C: Consult Mode
Multi-turn consultation with session continuity.
```bash
_CONSULT_PROMPT="${USER_PROMPT}"
# -c flag resumes the last session (or starts a new one)
timeout 330 "$AGY_BIN" -c \
--model "$_AGY_MODEL" \
"$_CONSULT_PROMPT"
_AGY_EXIT=$?
if [ "$_AGY_EXIT" = "124" ]; then
echo "[agy] Consult session stalled. Try a shorter prompt or add --model antigravity-2.0-flash."
fi
```
Present Antigravity's response verbatim. For follow-up questions, re-invoke
`/agy consult <follow-up>` to continue the session with `-c`.
---
## Step 3: Report
After any mode completes, summarize:
1. Which mode ran and what model was used
2. Key findings (verbatim from Antigravity where possible)
3. Pass/Fail verdict for Review and Challenge modes
4. Any suggested follow-up actions
For Review and Challenge modes, if findings are blocking:
- List each as a numbered `❌ BLOCKING` item
- Do NOT auto-fix — present and let the user decide
For Consult mode:
- Present the response as-is
- Offer to continue: "Run `/agy consult <follow-up>` to continue this session."

View File

@ -10,6 +10,7 @@ Conventions:
## Skills
- [/agy](agy/SKILL.md): Google Antigravity CLI wrapper — three modes.
- [/autoplan](autoplan/SKILL.md): Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles.
- [/benchmark](benchmark/SKILL.md): Performance regression detection using the browse daemon.
- [/benchmark-models](benchmark-models/SKILL.md): Cross-model benchmark for gstack skills.

140
hosts/antigravity.ts Normal file
View File

@ -0,0 +1,140 @@
import type { HostConfig } from '../scripts/host-config';
/**
* Google Antigravity 2.0 host config.
*
* Supports:
* - Antigravity CLI (`agy` / `antigravity`)
* - Antigravity Desktop IDE (multi-agent subagent worktrees)
* - Antigravity Python SDK (cron/headless background agents)
*
* Path layout:
* Global install : ~/.antigravity/skills/gstack
* Repo-local : .antigravity/skills/gstack
* Host subdir : .antigravity
*
* NOTE on .agents/ collision: codex already owns hostSubdir='.agents'. Using
* '.antigravity' keeps validateAllConfigs() happy and is the idiomatic pattern
* for every host after claude/codex (see openclaw, hermes, gbrain, etc.).
*
* NOTE on 'gemini' alias: the `gemini` binary name is owned by @google/gemini-cli
* (a separate product). It must NOT be registered here to avoid CLI resolution
* collisions. Only 'agy' is a safe, unambiguous alias.
*
* NOTE on repo-local discovery: Antigravity Desktop IDE reads `.antigravity/skills/`
* for repo-local skills by default (configurable via ANTIGRAVITY_SKILL_ROOT env var).
* If a project team uses `.agents/skills/` exclusively, document a symlink:
* ln -s .antigravity/skills/gstack .agents/skills/gstack
* or set ANTIGRAVITY_SKILL_ROOT=.agents/skills in the project's .env.
* This is noted in the generated skill AGENTS.md preamble via the adapter.
*/
const antigravity: HostConfig = {
name: 'antigravity',
displayName: 'Google Antigravity',
cliCommand: 'antigravity',
cliAliases: ['agy'],
globalRoot: '.antigravity/skills/gstack',
localSkillRoot: '.antigravity/skills/gstack',
hostSubdir: '.antigravity',
usesEnvVars: true,
frontmatter: {
mode: 'allowlist',
// Keep only fields consumed by Antigravity's routing table.
// 'version' + 'author' populate the IDE skill card metadata.
// 'tools' lets the Desktop IDE pre-authorize tool calls per skill.
// Keeping this list short prevents context-window bloat when the
// multi-agent orchestrator loads all skill manifests simultaneously.
keepFields: ['name', 'description', 'version', 'author'],
renameFields: { 'allowed-tools': 'tools' },
// 1024-char limit prevents any single skill description from dominating
// the routing table context; 'truncate' avoids hard build failures on
// edge cases while still surfacing an actionable warning in CI logs.
descriptionLimit: 1024,
descriptionLimitBehavior: 'truncate',
},
generation: {
generateMetadata: false,
// 'codex' skill is a Claude wrapper around the codex exec binary — meaningless on Antigravity.
// 'agy' skill is a Claude wrapper around the agy exec binary — skip self-referential loop.
skipSkills: ['codex', 'agy'],
},
pathRewrites: [
// Global root rewrites — must be ordered longest-match first
{ from: '~/.claude/skills/gstack', to: '~/.antigravity/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.antigravity/skills/gstack' },
{ from: '.claude/skills/review', to: '.antigravity/skills/gstack/review' },
{ from: '.claude/skills', to: '.antigravity/skills' },
// Codex sidecar path (.agents is used by codex, not Antigravity)
{ from: '.agents/skills/gstack', to: '.antigravity/skills/gstack' },
{ from: '.agents/skills', to: '.antigravity/skills' },
// Config file rewrites
{ from: 'CLAUDE.md', to: 'AGENTS.md' },
// Home dir path segment rewrites
{ from: '~/.claude/', to: '~/.antigravity/' },
{ from: '.claude/', to: '.antigravity/' },
],
toolRewrites: {
// Bash / shell execution
'use the Bash tool': 'use the execute_terminal_command tool',
'use the Write tool': 'use the apply_file_modification tool',
'use the Read tool': 'use the read_file_content tool',
'use the Edit tool': 'use the apply_file_modification tool',
'use the Agent tool': 'spawn a subagent task',
'use the Grep tool': 'search for',
'use the Glob tool': 'find files matching',
// Bare noun references (appear in prose after "the X tool already ran" etc.)
'the Bash tool': 'the execute_terminal_command tool',
'the Read tool': 'the read_file_content tool',
'the Write tool': 'the apply_file_modification tool',
'the Edit tool': 'the apply_file_modification tool',
},
// Suppress Claude-specific cross-invocation resolvers that would generate
// "run `codex …`" or "run `claude …`" instructions inside Antigravity sessions.
suppressedResolvers: [
'DESIGN_OUTSIDE_VOICES', // design.ts — would emit "run codex" instructions
'ADVERSARIAL_STEP', // review.ts — would emit "run codex" instructions
'CODEX_SECOND_OPINION', // review.ts — Codex can't be invoked from Antigravity
'CODEX_PLAN_REVIEW', // review.ts — same
'REVIEW_ARMY', // review-army.ts — Claude orchestration, not applicable
'GBRAIN_CONTEXT_LOAD', // gbrain not integrated with Antigravity SDK
'GBRAIN_SAVE_RESULTS', // same
],
runtimeRoot: {
globalSymlinks: [
'bin',
'browse/dist',
'browse/bin',
'design/dist',
'gstack-upgrade',
'ETHOS.md',
'review/specialists',
'qa/templates',
'qa/references',
],
globalFiles: {
'review': ['checklist.md', 'design-checklist.md', 'TODOS-format.md'],
},
},
install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},
coAuthorTrailer: 'Co-Authored-By: Google Antigravity <antigravity-agent@google.com>',
learningsMode: 'basic',
boundaryInstruction: 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or .agents/skills/. These are Claude Code or Codex skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Stay focused on the repository code only.',
// Adapter handles AskUserQuestion → request_user_input with headless-mode
// conditional (ANTIGRAVITY_HEADLESS=1 → write to TODOS.md instead of blocking).
adapter: 'scripts/host-adapters/antigravity-adapter.ts',
};
export default antigravity;

View File

@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';
import gbrain from './gbrain';
import antigravity from './antigravity';
/** 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, antigravity];
/** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
@ -65,4 +66,4 @@ export function getExternalHosts(): HostConfig[] {
}
// Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain };
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, antigravity };

View File

@ -16,6 +16,7 @@
"build:diagram-render": "cd lib/diagram-render && bun install && bun run scripts/build.ts",
"gen:skill-docs": "bun run scripts/gen-skill-docs.ts",
"gen:skill-docs:user": "bun run scripts/gen-skill-docs.ts --respect-detection",
"setup:antigravity": "bun run scripts/gen-skill-docs.ts --host antigravity && bun run scripts/setup-antigravity.ts",
"dev": "bun run browse/src/cli.ts",
"server": "bun run browse/src/server.ts",
"test": "bun test browse/test/ test/ make-pdf/test/ --ignore 'test/skill-e2e-*.test.ts' --ignore test/skill-llm-eval.test.ts --ignore test/skill-routing-e2e.test.ts --ignore test/codex-e2e.test.ts --ignore test/gemini-e2e.test.ts && (bun run slop:diff 2>/dev/null || true)",

View File

@ -655,6 +655,17 @@ function applyHostRewrites(content: string, hostConfig: HostConfig): string {
result = result.replaceAll(from, to);
}
}
if (hostConfig.adapter) {
const adapterPath = path.resolve(ROOT, hostConfig.adapter);
try {
const adapter = require(adapterPath);
if (adapter && adapter.transform) {
result = adapter.transform(result, hostConfig);
}
} catch (e) {
console.warn(`Failed to load adapter ${hostConfig.adapter}:`, e);
}
}
return result;
}

View File

@ -0,0 +1,109 @@
/**
* Google Antigravity host adapter post-processing content transformer.
*
* Runs AFTER generic frontmatter/path/tool rewrites from the config system.
* Handles semantic transformations that string-replace can't cover cleanly:
*
* 1. AskUserQuestion request_user_input with headless-mode safety guard
* 2. Agent/subagent spawning Antigravity subagent task patterns
* 3. Browse binary patterns ($B antigravity exec pattern)
* 4. Preamble binary path references map or strip
*
* Headless-mode semantics:
* Antigravity frequently runs skills in background Git worktrees via Desktop
* subagents or Python SDK cron jobs. Skills that call AskUserQuestion will
* deadlock in a non-TTY context. The adapter wraps every AskUserQuestion
* reference with a conditional: if ANTIGRAVITY_HEADLESS=1, write a structured
* note to TODOS.md instead of blocking on stdin.
*
* Interface: transform(content, config) transformed content
*/
import type { HostConfig } from '../host-config';
/** Headless-mode fallback instruction injected alongside every interactive prompt reference. */
const HEADLESS_GUARD_PROSE =
'(If running headlessly — `ANTIGRAVITY_HEADLESS=1` or non-TTY — write a structured note ' +
'to `TODOS.md` with the question and context instead of blocking on stdin.)';
/**
* Transform generated SKILL.md content for Antigravity compatibility.
* Called after all generic rewrites (paths, tools, frontmatter) have been applied.
*/
export function transform(content: string, _config: HostConfig): string {
let result = content;
// 1. ── AskUserQuestion → request_user_input with headless guard ──────────
//
// Match only the tool-call instruction patterns (capitalized function-name style),
// not conversational prose like "you may ask the user a question". This prevents
// mutating normal explanatory text.
//
// Patterns targeted:
// "Use AskUserQuestion to …"
// "use AskUserQuestion to …"
// "call AskUserQuestion"
// bare "AskUserQuestion" inside a code fence or instruction block
//
// We append the headless guard note only once per replacement to avoid repetition.
result = result.replaceAll(
'Use AskUserQuestion',
`Use request_user_input ${HEADLESS_GUARD_PROSE}`
);
result = result.replaceAll(
'use AskUserQuestion',
`use request_user_input ${HEADLESS_GUARD_PROSE}`
);
result = result.replaceAll(
'call AskUserQuestion',
`call request_user_input ${HEADLESS_GUARD_PROSE}`
);
// Bare noun reference fallback (after above patterns are consumed)
result = result.replaceAll('AskUserQuestion', 'request_user_input');
// 2. ── Agent/subagent spawning → Antigravity patterns ───────────────────
//
// "the Agent tool" was already handled by toolRewrites ("use the Agent tool" →
// "spawn a subagent task"), but bare noun patterns need direct replacement here.
result = result.replaceAll('the Agent tool', 'the subagent task system');
result = result.replaceAll('Agent tool', 'subagent task system');
// Codex-style parameter naming that leaks into skill prose
result = result.replaceAll('subagent_type', 'task type');
// 3. ── Browse binary patterns ────────────────────────────────────────────
//
// $B is the gstack browse binary shorthand. In Antigravity, wrap in exec call.
// Only rewrite inside code fences to avoid mangling prose.
result = result.replace(/`\$B /g, '`exec $B ');
// 4. ── Preamble binary path references ──────────────────────────────────
//
// Global bin paths like ~/.antigravity/skills/gstack/bin/gstack-X are already
// rewritten by pathRewrites. Keep them as-is (the exec model works the same).
// Nothing to strip or remap here.
// 5. ── TODOS.md headless fallback instruction (structured note format) ───
//
// When a skill explicitly instructs writing to a file on headless runs, ensure
// the instruction references a valid, linting-compliant markdown format:
//
// ## TODO: [Skill Name] — [ISO timestamp]
// **Question:** <question text>
// **Context:** <context>
// **Expected response:** <what the skill is waiting for>
//
// This is injected as a comment in the section that mentions TODOS.md if the
// pattern doesn't already exist in the content (idempotency guard).
const TODOS_FORMAT_HINT =
'<!-- Antigravity headless format:\n' +
' ## TODO: [Skill] — [ISO timestamp]\n' +
' **Question:** <text>\n' +
' **Context:** <context>\n' +
' **Expected response:** <what to provide when unblocking>\n' +
'-->';
if (result.includes('TODOS.md') && !result.includes('Antigravity headless format')) {
result = result.replace('TODOS.md', `TODOS.md\n${TODOS_FORMAT_HINT}`);
}
return result;
}

View File

@ -3,6 +3,11 @@
"catalog_mode": "trim",
"note": "Routing / voice-trigger prose extracted from SKILL.md frontmatter descriptions during catalog trim. Loaded on demand when routing guidance is needed.",
"skills": {
"agy": {
"lead": "Google Antigravity CLI wrapper — three modes.",
"routing": "Code review: independent diff review\nvia `agy -p` (headless print mode) with pass/fail gate. Challenge: adversarial\nstress-testing of proposed designs and race conditions. Consult: multi-turn session\ncontinuation via `agy -c`. The \"200 IQ second opinion from a different AI system.\"\nUse when asked to \"agy review\", \"agy challenge\", \"antigravity review\", \"consult agy\",\nor \"second opinion\".",
"voice_line": "Voice triggers (speech-to-text aliases): \"a g y\", \"antigravity review\", \"get another opinion\"."
},
"autoplan": {
"lead": "Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles.",
"routing": "Surfaces\ntaste decisions (close approaches, borderline scope, codex disagreements) at a final\napproval gate. One command, fully reviewed plan out.\nUse when asked to \"auto review\", \"autoplan\", \"run all reviews\", \"review this plan\nautomatically\", or \"make the decisions for me\".\nProactively suggest when the user has a plan file and wants to run the full review\ngauntlet without answering 15-30 intermediate questions.",

View File

@ -0,0 +1,335 @@
#!/usr/bin/env bun
/**
* setup-antigravity.ts
*
* Automated post-generation setup for the Google Antigravity host.
*
* What it does:
* 1. Resolves the repo-local generated skills directory:
* <repo>/.antigravity/skills/gstack
* 2. Ensures the global Antigravity skills parent directory exists:
* ~/.agents/skills/
* 3. Creates a platform-appropriate link at ~/.agents/skills/gstack:
* Windows NTFS junction (no admin/Developer Mode required)
* macOS/Linux directory symlink
* 4. Is fully idempotent: safe to run repeatedly; skips if already correct.
* 5. Detects and cleans up broken or stale links before re-creating.
*
* Why ~/.agents/skills/ (not ~/.antigravity/skills/):
* Antigravity v2 Desktop IDE and CLI discover repo-local and global AI skills
* under ~/.agents/skills/. The .antigravity/ directory is the VS Code app shell,
* not the skill discovery root. This script bridges the two.
*
* Usage:
* bun run scripts/setup-antigravity.ts
* bun run setup:antigravity (via package.json)
*
* The setup:antigravity script in package.json first runs gen:skill-docs --host
* antigravity so the source directory always exists before linking.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import antigravityConfig from '../hosts/antigravity';
// ─── Constants ───────────────────────────────────────────────────────────────
const IS_WINDOWS = process.platform === 'win32';
/** Root of the gstack repository (parent of this script's directory). */
const REPO_ROOT = path.resolve(import.meta.dir, '..');
/** Source: the full generated Antigravity skills directory inside the repo.
* This is the PARENT that contains gstack/, gstack-ship/, gstack-review/, etc.
* We link this entire directory into ~/.agents/skills/gstack so Antigravity
* can discover all gstack-* skills in one place.
*/
const SOURCE_DIR = path.join(REPO_ROOT, '.antigravity', 'skills');
/** Target parent: the global Antigravity/agents skills directory. */
const TARGET_PARENT = path.join(os.homedir(), '.agents', 'skills');
/** Target link path: where Antigravity will discover gstack skills. */
const TARGET_LINK = path.join(TARGET_PARENT, 'gstack');
// ─── Logging helpers ─────────────────────────────────────────────────────────
function log(msg: string): void {
process.stdout.write(msg + '\n');
}
function warn(msg: string): void {
process.stderr.write('[warn] ' + msg + '\n');
}
function die(msg: string): never {
process.stderr.write('[error] ' + msg + '\n');
process.exit(1);
}
// ─── Utility: resolve a link's final real target ────────────────────────────
/**
* Return the real path a symlink/junction points at, or null if the link is
* broken (target doesn't exist on disk) or the path doesn't exist at all.
*/
function readLinkTarget(linkPath: string): string | null {
try {
const target = fs.readlinkSync(linkPath);
// Resolve relative targets against the link's directory
const resolved = path.isAbsolute(target)
? target
: path.resolve(path.dirname(linkPath), target);
return fs.existsSync(resolved) ? resolved : null;
} catch {
return null;
}
}
/**
* Return true if linkPath is a symlink or NTFS junction.
* On Windows, junctions report isSymbolicLink() === true via lstat.
*/
function isLink(linkPath: string): boolean {
try {
return fs.lstatSync(linkPath).isSymbolicLink();
} catch {
return false;
}
}
/**
* Return true if linkPath exists as a real directory (not a link).
*/
function isRealDir(linkPath: string): boolean {
try {
const stat = fs.lstatSync(linkPath);
return stat.isDirectory() && !stat.isSymbolicLink();
} catch {
return false;
}
}
// ─── Step 1: Verify source exists ────────────────────────────────────────────
function assertSourceExists(): void {
if (!fs.existsSync(SOURCE_DIR)) {
die(
`Source directory not found: ${SOURCE_DIR}\n` +
`Run 'bun run gen:skill-docs --host antigravity' first, or use:\n` +
` bun run setup:antigravity`
);
}
log(`✓ Source: ${SOURCE_DIR}`);
}
// ─── Step 2: Ensure target parent directory exists ───────────────────────────
function ensureTargetParent(): void {
if (!fs.existsSync(TARGET_PARENT)) {
fs.mkdirSync(TARGET_PARENT, { recursive: true });
log(`✓ Created: ${TARGET_PARENT}`);
} else {
log(`✓ Parent exists: ${TARGET_PARENT}`);
}
}
// ─── Step 3: Check existing link/directory state ─────────────────────────────
type LinkState =
| { kind: 'none' } // TARGET_LINK does not exist
| { kind: 'correct'; target: string } // already points at SOURCE_DIR
| { kind: 'stale'; target: string } // link exists but points elsewhere
| { kind: 'broken' } // link exists but target is missing
| { kind: 'real-dir' } // real directory (not a link) — back up
| { kind: 'real-file' }; // real file (not a link) — back up
function inspectTarget(): LinkState {
if (!fs.existsSync(TARGET_LINK) && !isLink(TARGET_LINK)) {
return { kind: 'none' };
}
if (isLink(TARGET_LINK)) {
const resolvedTarget = readLinkTarget(TARGET_LINK);
if (resolvedTarget === null) {
return { kind: 'broken' };
}
// Normalize both paths for comparison (handles Windows drive-letter case, trailing slashes)
const normalizedTarget = path.normalize(resolvedTarget);
const normalizedSource = path.normalize(SOURCE_DIR);
if (normalizedTarget === normalizedSource) {
return { kind: 'correct', target: resolvedTarget };
}
return { kind: 'stale', target: resolvedTarget };
}
if (isRealDir(TARGET_LINK)) {
return { kind: 'real-dir' };
}
// Exists but is neither link nor directory (shouldn't happen, but handle gracefully)
return { kind: 'real-file' };
}
// ─── Step 4: Remove existing stale/broken link or back up real directory ────
function removeOrBackup(state: LinkState): void {
if (state.kind === 'broken' || state.kind === 'stale') {
log(` Removing ${state.kind} link: ${TARGET_LINK}`);
if (state.kind === 'stale') {
log(` (was pointing at: ${(state as { kind: 'stale'; target: string }).target})`);
}
// Use unlinkSync for junctions/symlinks (rmSync throws EFAULT on Windows junctions)
fs.unlinkSync(TARGET_LINK);
return;
}
if (state.kind === 'real-dir' || state.kind === 'real-file') {
const backupPath = TARGET_LINK + '.backup-' + Date.now();
warn(
`${TARGET_LINK} is a ${state.kind === 'real-dir' ? 'real directory' : 'real file'} (not a link).\n` +
` Backing it up to: ${backupPath}\n` +
` If this was intentional, merge it back manually after setup.`
);
fs.renameSync(TARGET_LINK, backupPath);
return;
}
}
// ─── Step 5: Create the link ─────────────────────────────────────────────────
function createLink(): void {
// Dynamically select link type based on the operating system
const linkType = IS_WINDOWS ? 'junction' : 'dir';
try {
fs.symlinkSync(SOURCE_DIR, TARGET_LINK, linkType);
log(`✓ Successfully bridged across ${process.platform} using a '${linkType}' link!`);
log(` ${TARGET_LINK}${SOURCE_DIR}`);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
die(
`Failed to create ${linkType}.\n` +
` ${msg}\n\n` +
`Manual fallback:\n` +
(IS_WINDOWS
? ` mklink /J "${TARGET_LINK}" "${SOURCE_DIR}"`
: ` ln -s "${SOURCE_DIR}" "${TARGET_LINK}"`)
);
}
}
// ─── Step 6: Verify the link resolves correctly ──────────────────────────────
function verifyLink(): void {
const state = inspectTarget();
if (state.kind !== 'correct') {
die(
`Link verification failed after creation (state: ${state.kind}).\n` +
` Expected: ${TARGET_LINK}${SOURCE_DIR}\n` +
` Please report this as a bug in scripts/setup-antigravity.ts.`
);
}
log(`✓ Verified: ${TARGET_LINK}${SOURCE_DIR}`);
}
// ─── Step 7: Print discovery hint ───────────────────────────────────────────
function printDiscoveryHint(): void {
log('');
log('─'.repeat(60));
log(' Antigravity skill bridge ready.');
log(` Skills are discoverable at: ${TARGET_LINK}`);
log('');
if (IS_WINDOWS) {
log(' Windows note: an NTFS junction was created. No admin required.');
log(' To verify: dir /AL "' + path.dirname(TARGET_LINK) + '"');
} else {
log(' To verify: ls -la "' + path.dirname(TARGET_LINK) + '"');
}
log('');
log(' To refresh skills after a git pull:');
log(' bun run setup:antigravity');
log('─'.repeat(60));
}
// ─── Main ────────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
log('');
log('Setting up Antigravity skill bridge...');
log('');
// 1. Verify source
assertSourceExists();
// 1.5. Populate runtime assets
populateRuntimeAssets();
// 2. Ensure ~/.agents/skills/ exists
ensureTargetParent();
// 3. Inspect existing state
const state = inspectTarget();
if (state.kind === 'correct') {
log(`✓ Already linked correctly: ${TARGET_LINK}`);
log(` (${IS_WINDOWS ? 'junction' : 'symlink'}${SOURCE_DIR})`);
printDiscoveryHint();
return; // Idempotent: nothing to do
}
// 4. Remove stale/broken link or back up real directory
if (state.kind !== 'none') {
removeOrBackup(state);
}
// 5. Create the link
createLink();
// 6. Verify
verifyLink();
// 7. Done
printDiscoveryHint();
}
main().catch((err) => die(`Unhandled error: ${err}`));
// ─── Step 7: Populate runtime assets ──────────────────────────────────────────
function populateRuntimeAssets(): void {
const runtime = antigravityConfig.runtimeRoot;
if (!runtime) return;
function linkOrCopy(src: string, dst: string) {
if (!fs.existsSync(src)) return;
if (fs.existsSync(dst)) return;
fs.mkdirSync(path.dirname(dst), { recursive: true });
if (isRealDir(src)) {
fs.symlinkSync(src, dst, IS_WINDOWS ? 'junction' : 'dir');
} else {
if (IS_WINDOWS) {
fs.copyFileSync(src, dst);
} else {
fs.symlinkSync(src, dst, 'file');
}
}
}
if (runtime.globalSymlinks) {
for (const asset of runtime.globalSymlinks) {
linkOrCopy(path.join(REPO_ROOT, asset), path.join(SOURCE_DIR, asset));
}
}
if (runtime.globalFiles) {
for (const [dir, files] of Object.entries(runtime.globalFiles)) {
for (const file of files) {
linkOrCopy(path.join(REPO_ROOT, dir, file), path.join(SOURCE_DIR, dir, file));
}
}
}
}

View File

@ -30,8 +30,8 @@ const ROOT = path.resolve(import.meta.dir, '..');
// ─── hosts/index.ts ─────────────────────────────────────────
describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 10 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(10);
test('ALL_HOST_CONFIGS has 11 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(11);
});
test('ALL_HOST_NAMES matches config names', () => {