Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bin/gstack-global-discover*
.openclaw/
.hermes/
.gbrain/
.agy/
.gbrain-source
.context/
extension/.auth.json
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Or target a specific agent with `./setup --host <name>`:
| Kiro | `--host kiro` | `~/.kiro/skills/gstack-*/` |
| Hermes | `--host hermes` | `~/.hermes/skills/gstack-*/` |
| GBrain (mod) | `--host gbrain` | `~/.gbrain/skills/gstack-*/` |
| Antigravity | `--host agy` | `~/.gemini/config/skills/gstack-*/` |

**Want to add support for another agent?** See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md).
It's one TypeScript config file, zero code changes.
Expand Down
48 changes: 48 additions & 0 deletions hosts/agy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { HostConfig } from '../scripts/host-config';

const agy: HostConfig = {
name: 'agy',
displayName: 'Antigravity',
cliCommand: 'agy',
cliAliases: ['antigravity'],

globalRoot: '.gemini/config/skills/gstack',
localSkillRoot: '.agents/skills/gstack',
hostSubdir: '.agy',
usesEnvVars: true,

frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: null,
},

generation: {
generateMetadata: false,
skipSkills: ['codex'],
},

pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '~/.gemini/config/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.agents/skills/gstack' },
{ from: '.claude/skills', to: '.agents/skills' },
],

suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'],

runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'design/dist', 'gstack-upgrade', 'ETHOS.md', 'review/specialists', 'qa/templates', 'qa/references', 'plan-devex-review/dx-hall-of-fame.md'],
globalFiles: {
'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'],
},
},

install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},

learningsMode: 'basic',
};

export default agy;
5 changes: 3 additions & 2 deletions hosts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';
import gbrain from './gbrain';
import agy from './agy';

/** 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, agy];

/** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
Expand Down Expand Up @@ -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, agy };
127 changes: 123 additions & 4 deletions setup
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ FACTORY_SKILLS="$HOME/.factory/skills"
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
AGY_SKILLS="$HOME/.gemini/config/skills"
AGY_GSTACK="$AGY_SKILLS/gstack"

IS_WINDOWS=0
case "$(uname -s)" in
Expand Down Expand Up @@ -85,7 +87,7 @@ NO_TEAM_MODE=0
PLAN_TUNE_HOOKS_MODE="" # "" = resolve from env/config/prompt; "yes"/"no" = explicit
while [ $# -gt 0 ]; do
case "$1" in
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, agy, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host=*) HOST="${1#--host=}"; shift ;;
--local) LOCAL_INSTALL=1; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
Expand All @@ -101,7 +103,7 @@ while [ $# -gt 0 ]; do
done

case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;;
claude|codex|kiro|factory|opencode|agy|auto) ;;
openclaw)
echo ""
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
Expand Down Expand Up @@ -136,7 +138,7 @@ case "$HOST" in
echo "GBrain setup and brain skills ship from the GBrain repo."
echo ""
exit 0 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, agy, or auto)" >&2; exit 1 ;;
esac

# ─── Resolve skill prefix preference ─────────────────────────
Expand Down Expand Up @@ -200,14 +202,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
INSTALL_OPENCODE=0
INSTALL_AGY=0
if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
command -v agy >/dev/null 2>&1 && INSTALL_AGY=1
# If none found, default to claude
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_AGY" -eq 0 ]; then
INSTALL_CLAUDE=1
fi
elif [ "$HOST" = "claude" ]; then
Expand All @@ -220,6 +224,8 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1
elif [ "$HOST" = "agy" ]; then
INSTALL_AGY=1
fi

migrate_direct_codex_install() {
Expand Down Expand Up @@ -475,6 +481,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
)
fi

# 1e. Generate .agy/ Antigravity skill docs
if [ "$INSTALL_AGY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
log "Generating .agy/ skill docs..."
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host agy
)
fi

# 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then
echo "Installing Playwright Chromium..."
Expand Down Expand Up @@ -972,6 +988,99 @@ link_opencode_skill_dirs() {
fi
}

create_agy_runtime_root() {
local gstack_dir="$1"
local agy_gstack="$2"
local agy_dir="$gstack_dir/.agy/skills"

if [ -L "$agy_gstack" ]; then
rm -f "$agy_gstack"
elif [ -d "$agy_gstack" ] && [ "$agy_gstack" != "$gstack_dir" ]; then
rm -rf "$agy_gstack"
fi

mkdir -p "$agy_gstack" "$agy_gstack/browse" "$agy_gstack/design" "$agy_gstack/gstack-upgrade" "$agy_gstack/review" "$agy_gstack/qa" "$agy_gstack/plan-devex-review"

if [ -f "$agy_dir/gstack/SKILL.md" ]; then
_link_or_copy "$agy_dir/gstack/SKILL.md" "$agy_gstack/SKILL.md"
fi
if [ -d "$gstack_dir/bin" ]; then
_link_or_copy "$gstack_dir/bin" "$agy_gstack/bin"
fi
if [ -d "$gstack_dir/browse/dist" ]; then
_link_or_copy "$gstack_dir/browse/dist" "$agy_gstack/browse/dist"
fi
if [ -d "$gstack_dir/browse/bin" ]; then
_link_or_copy "$gstack_dir/browse/bin" "$agy_gstack/browse/bin"
fi
if [ -d "$gstack_dir/design/dist" ]; then
_link_or_copy "$gstack_dir/design/dist" "$agy_gstack/design/dist"
fi
if [ -f "$agy_dir/gstack-upgrade/SKILL.md" ]; then
_link_or_copy "$agy_dir/gstack-upgrade/SKILL.md" "$agy_gstack/gstack-upgrade/SKILL.md"
fi
for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
if [ -f "$gstack_dir/review/$f" ]; then
_link_or_copy "$gstack_dir/review/$f" "$agy_gstack/review/$f"
fi
done
if [ -d "$gstack_dir/review/specialists" ]; then
_link_or_copy "$gstack_dir/review/specialists" "$agy_gstack/review/specialists"
fi
if [ -d "$gstack_dir/qa/templates" ]; then
_link_or_copy "$gstack_dir/qa/templates" "$agy_gstack/qa/templates"
fi
if [ -d "$gstack_dir/qa/references" ]; then
_link_or_copy "$gstack_dir/qa/references" "$agy_gstack/qa/references"
fi
if [ -f "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" ]; then
_link_or_copy "$gstack_dir/plan-devex-review/dx-hall-of-fame.md" "$agy_gstack/plan-devex-review/dx-hall-of-fame.md"
fi
if [ -f "$gstack_dir/ETHOS.md" ]; then
_link_or_copy "$gstack_dir/ETHOS.md" "$agy_gstack/ETHOS.md"
fi
}

link_agy_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local agy_dir="$gstack_dir/.agy/skills"
local linked=()

if [ ! -d "$agy_dir" ]; then
echo " Generating .agy/ skill docs..."
( cd "$gstack_dir" && bun run gen:skill-docs --host agy )
fi

if [ ! -d "$agy_dir" ]; then
echo " warning: .agy/skills/ generation failed — run 'bun run gen:skill-docs --host agy' manually" >&2
return 1
fi

# Verify at least one SKILL.md exists before reporting success (KiDDarn safeguard)
local skill_count
skill_count=$(find "$agy_dir" -name "SKILL.md" | wc -l | tr -d ' ')
if [ "$skill_count" -eq 0 ]; then
echo " warning: no SKILL.md files found in .agy/skills/ — generation may have failed" >&2
return 1
fi

for skill_dir in "$agy_dir"/gstack*/; do
if [ -f "$skill_dir/SKILL.md" ]; then
skill_name="$(basename "$skill_dir")"
[ "$skill_name" = "gstack" ] && continue
target="$skills_dir/$skill_name"
if [ -L "$target" ] || [ ! -e "$target" ]; then
_link_or_copy "$skill_dir" "$target"
linked+=("$skill_name")
fi
fi
done
if [ ${#linked[@]} -gt 0 ]; then
echo " linked skills: ${linked[*]}"
fi
}

# 4. Install for Claude (default)
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
Expand Down Expand Up @@ -1193,6 +1302,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
echo " opencode skills: $OPENCODE_SKILLS"
fi

# 6d. Install for Antigravity (agy)
if [ "$INSTALL_AGY" -eq 1 ]; then
mkdir -p "$AGY_SKILLS"
create_agy_runtime_root "$SOURCE_GSTACK_DIR" "$AGY_GSTACK"
link_agy_skill_dirs "$SOURCE_GSTACK_DIR" "$AGY_SKILLS"
echo "gstack ready (agy)."
echo " browse: $BROWSE_BIN"
echo " agy skills: $AGY_SKILLS"
fi

# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs.
Expand Down
34 changes: 32 additions & 2 deletions test/host-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
slate,
cursor,
openclaw,
agy,
} from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types';

Expand All @@ -30,8 +31,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', () => {
Expand All @@ -53,6 +54,7 @@ describe('hosts/index.ts', () => {
expect(slate.name).toBe('slate');
expect(cursor.name).toBe('cursor');
expect(openclaw.name).toBe('openclaw');
expect(agy.name).toBe('agy');
});

test('getHostConfig returns correct config', () => {
Expand Down Expand Up @@ -536,3 +538,31 @@ describe('host config correctness', () => {
}
});
});

// ─── agy specific tests ─────────────────────────────────────

describe('agy specific config', () => {
test('agy has correct global root path', () => {
expect(agy.globalRoot).toBe('.gemini/config/skills/gstack');
});

test('agy has correct local skill root', () => {
expect(agy.localSkillRoot).toBe('.agents/skills/gstack');
});

test('agy runtime root includes all expected sidecars', () => {
expect(agy.runtimeRoot.globalSymlinks).toContain('bin');
expect(agy.runtimeRoot.globalSymlinks).toContain('browse/dist');
expect(agy.runtimeRoot.globalSymlinks).toContain('ETHOS.md');
});

test('agy skill generation produces at least one SKILL.md', () => {
const agySkillDir = path.join(ROOT, '.agy', 'skills');
if (fs.existsSync(agySkillDir)) {
const skills = fs.readdirSync(agySkillDir, { withFileTypes: true });
const hasSkillMd = skills.some(d => d.isDirectory() &&
fs.existsSync(path.join(agySkillDir, d.name, 'SKILL.md')));
expect(hasSkillMd).toBe(true);
}
});
});