mirror of https://github.com/garrytan/gstack.git
Merge 4b4fa93b51 into 7c9df1c568
This commit is contained in:
commit
6e799ae7fb
|
|
@ -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.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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."
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue