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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ bin/gstack-global-discover
.slate/
.cursor/
.openclaw/
.pi/
!.pi/settings.json
.context/
extension/.auth.json
.gstack-worktrees/
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ These are conversational skills. Your OpenClaw agent runs them directly via chat

### Other AI Agents

gstack works on 8 AI coding agents, not just Claude. Setup auto-detects which
gstack works on 9 AI coding agents, not just Claude. Setup auto-detects which
agents you have installed:

```bash
Expand All @@ -128,6 +128,7 @@ Or target a specific agent with `./setup --host <name>`:
| Factory Droid | `--host factory` | `~/.factory/skills/gstack-*/` |
| Slate | `--host slate` | `~/.slate/skills/gstack-*/` |
| Kiro | `--host kiro` | `~/.kiro/skills/gstack-*/` |
| Pi | `--host pi` | `~/.pi/agent/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
26 changes: 26 additions & 0 deletions bin/gstack-uninstall
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
# ~/.codex/skills/gstack* — Codex skill install + per-skill symlinks
# ~/.factory/skills/gstack* — Factory Droid skill install + per-skill symlinks
# ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks
# ~/.pi/agent/skills/gstack* — Pi skill install + per-skill copies
# ~/.gstack/ — global state (config, analytics, sessions, projects,
# repos, installation-id, browse error logs)
# .claude/skills/gstack* — project-local skill install (--local installs)
# .gstack/ — per-project browse state (in current git repo)
# .gstack-worktrees/ — per-project test worktrees (in current git repo)
# .pi/skills/gstack* — Pi project-local sidecar (in current git repo)
# .agents/skills/gstack* — Codex/Gemini/Cursor sidecar (in current git repo)
# Running browse daemons — stopped via SIGTERM before cleanup
#
Expand Down Expand Up @@ -66,13 +68,15 @@ if [ "$FORCE" -eq 0 ]; then
[ -d "$HOME/.codex/skills" ] && echo " ~/.codex/skills/gstack*"
[ -d "$HOME/.factory/skills" ] && echo " ~/.factory/skills/gstack*"
[ -d "$HOME/.kiro/skills" ] && echo " ~/.kiro/skills/gstack*"
[ -d "$HOME/.pi/agent/skills" ] && echo " ~/.pi/agent/skills/gstack*"
[ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR"

if [ -n "$_GIT_ROOT" ]; then
[ -d "$_GIT_ROOT/.claude/skills/gstack" ] && echo " $_GIT_ROOT/.claude/skills/gstack (project-local)"
[ -d "$_GIT_ROOT/.gstack" ] && echo " $_GIT_ROOT/.gstack/ (browse state + reports)"
[ -d "$_GIT_ROOT/.gstack-worktrees" ] && echo " $_GIT_ROOT/.gstack-worktrees/"
[ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*"
[ -d "$_GIT_ROOT/.pi/skills" ] && echo " $_GIT_ROOT/.pi/skills/gstack*"
fi

# Preview running daemons
Expand Down Expand Up @@ -191,6 +195,16 @@ if [ -d "$KIRO_SKILLS" ]; then
done
fi

# ─── Remove Pi skills ───────────────────────────────────────
PI_SKILLS="$HOME/.pi/agent/skills"
if [ -d "$PI_SKILLS" ]; then
for _ITEM in "$PI_SKILLS"/gstack*; do
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
rm -rf "$_ITEM"
REMOVED+=("pi/$(basename "$_ITEM")")
done
fi

# ─── Remove per-project .agents/ sidecar ─────────────────────
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.agents/skills" ]; then
for _ITEM in "$_GIT_ROOT/.agents/skills"/gstack*; do
Expand All @@ -215,6 +229,18 @@ if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.factory/skills" ]; then
rmdir "$_GIT_ROOT/.factory" 2>/dev/null || true
fi

# ─── Remove per-project .pi/ sidecar ─────────────────────────
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.pi/skills" ]; then
for _ITEM in "$_GIT_ROOT/.pi/skills"/gstack*; do
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
rm -rf "$_ITEM"
REMOVED+=("pi-local/$(basename "$_ITEM")")
done

rmdir "$_GIT_ROOT/.pi/skills" 2>/dev/null || true
# Don't rmdir .pi/ itself — Pi may have other files there (settings.json, etc.)
fi

# ─── Remove per-project state ───────────────────────────────
if [ -n "$_GIT_ROOT" ]; then
if [ -d "$_GIT_ROOT/.gstack" ]; then
Expand Down
12 changes: 12 additions & 0 deletions gstack-upgrade/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,18 @@ elif [ -d ".claude/skills/gstack/.git" ]; then
elif [ -d ".agents/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".agents/skills/gstack"
elif [ -d ".pi/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".pi/skills/gstack"
elif [ -d ".claude/skills/gstack" ]; then
INSTALL_TYPE="vendored"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d "$HOME/.claude/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.claude/skills/gstack"
elif [ -d "$HOME/.pi/agent/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.pi/agent/skills/gstack"
else
echo "ERROR: gstack not found"
exit 1
Expand Down Expand Up @@ -150,6 +156,12 @@ if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
fi
elif [ -n "$_ROOT" ] && [ -d "$_ROOT/.pi/skills/gstack" ]; then
_RESOLVED_LOCAL=$(cd "$_ROOT/.pi/skills/gstack" && pwd -P)
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.pi/skills/gstack"
fi
fi
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
```
Expand Down
12 changes: 12 additions & 0 deletions gstack-upgrade/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,18 @@ elif [ -d ".claude/skills/gstack/.git" ]; then
elif [ -d ".agents/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".agents/skills/gstack"
elif [ -d ".pi/skills/gstack/.git" ]; then
INSTALL_TYPE="local-git"
INSTALL_DIR=".pi/skills/gstack"
elif [ -d ".claude/skills/gstack" ]; then
INSTALL_TYPE="vendored"
INSTALL_DIR=".claude/skills/gstack"
elif [ -d "$HOME/.claude/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.claude/skills/gstack"
elif [ -d "$HOME/.pi/agent/skills/gstack" ]; then
INSTALL_TYPE="vendored-global"
INSTALL_DIR="$HOME/.pi/agent/skills/gstack"
else
echo "ERROR: gstack not found"
exit 1
Expand Down Expand Up @@ -152,6 +158,12 @@ if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
fi
elif [ -n "$_ROOT" ] && [ -d "$_ROOT/.pi/skills/gstack" ]; then
_RESOLVED_LOCAL=$(cd "$_ROOT/.pi/skills/gstack" && pwd -P)
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.pi/skills/gstack"
fi
fi
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
```
Expand Down
5 changes: 3 additions & 2 deletions hosts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import opencode from './opencode';
import slate from './slate';
import cursor from './cursor';
import openclaw from './openclaw';
import pi from './pi';

/** All registered host configs. Add new hosts here. */
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw];
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, pi];

/** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
Expand Down Expand Up @@ -63,4 +64,4 @@ export function getExternalHosts(): HostConfig[] {
}

// Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw };
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, pi };
73 changes: 73 additions & 0 deletions hosts/pi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { HostConfig } from '../scripts/host-config';

const pi: HostConfig = {
name: 'pi',
displayName: 'Pi',
cliCommand: 'pi',
cliAliases: [],

// Pi uses ~/.pi/agent/skills/ for global, .pi/skills/ for project-local
globalRoot: '.pi/agent/skills/gstack',
localSkillRoot: '.pi/skills/gstack',
hostSubdir: '.pi',
usesEnvVars: true,

frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: 1024, // Agent Skills spec: max 1024 chars
prefixName: true, // Pi validates name: must match parent directory (gstack-review, etc.)
},

generation: {
generateMetadata: false,
skipSkills: ['codex'], // Codex skill wraps `codex exec` — Pi invokes codex directly
},

pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '~/.pi/agent/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.pi/skills/gstack' },
{ from: '.claude/skills/review', to: '.pi/skills/gstack/review' },
{ from: '.claude/skills', to: '.pi/skills' },
// Boundary instructions: tell Codex to also avoid .pi/ dirs
{ from: 'under ~/.claude/, ~/.agents/, .pi/skills/, or agents/. These are Claude Code skill definitions', to: 'under ~/.claude/, ~/.agents/, ~/.pi/, .pi/skills/, or agents/. These are Pi / Claude Code skill definitions' },
// Self-identity rewrites: Pi is the host, not Claude Code
{ from: 'this Claude Code window', to: 'this Pi window' },
{ from: 'using Claude Code as a force multiplier', to: 'using Pi as a force multiplier' },
{ from: "via Claude Code's Agent tool", to: 'via Pi tools' },
{ from: 'with Claude Code. The engineering barrier', to: 'with Pi. The engineering barrier' },
{ from: '\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n', to: '\n' },
{ from: 'Claude Code: N sessions', to: 'Pi: N sessions' },
],

// Pi tool names are lowercase; remap Claude-style references
toolRewrites: {
'use the Bash tool': 'use the bash tool',
'use the Read tool': 'use the read tool',
'use the Write tool': 'use the write tool',
'use the Edit tool': 'use the edit tool',
'use the Agent tool': 'dispatch a subagent',
'use the Grep tool': 'use bash with grep',
'use the Glob tool': 'use bash with find',
'AskUserQuestion': 'ask_user_question',
},

suppressedResolvers: [], // Pi uses Claude Opus — same capabilities, nothing to suppress

runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'],
globalFiles: {
'review': ['checklist.md', 'TODOS-format.md'],
},
},

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

coAuthorTrailer: 'Co-Authored-By: Claude Opus 4.6 via Pi <noreply@anthropic.com>',
learningsMode: 'full', // Same model as Claude — full cross-project learnings
};

export default pi;
8 changes: 6 additions & 2 deletions scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,12 @@ function transformFrontmatter(content: string, host: Host): string {
}

// Build frontmatter with allowed fields
// Pi requires name: to match the parent directory name (e.g., gstack-review)
const outputName = (fm.prefixName && name && name !== 'gstack' && !name.startsWith('gstack-'))
? `gstack-${name}`
: name;
const indentedDesc = description.split('\n').map(l => ` ${l}`).join('\n');
let newFm = `---\nname: ${name}\ndescription: |\n${indentedDesc}\n`;
let newFm = `---\nname: ${outputName}\ndescription: |\n${indentedDesc}\n`;

// Add extra fields (host-wide)
if (fm.extraFields) {
Expand Down Expand Up @@ -649,7 +653,7 @@ if (!DRY_RUN) {
const configPath = path.join(process.env.HOME || '', '.gstack', 'config.yaml');
if (fs.existsSync(configPath)) {
const config = fs.readFileSync(configPath, 'utf-8');
if (/^skill_prefix:\s*true/m.test(config)) {
if (/^skill_prefix:\s*true/m.test(config) && HOST !== 'pi') {
console.log('\nNote: skill_prefix is true. Run gstack-relink to re-apply name: patches.');
}
}
Expand Down
2 changes: 2 additions & 0 deletions scripts/host-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface HostConfig {
renameFields?: Record<string, string>;
/** Conditionally add fields based on template frontmatter values. */
conditionalFields?: Array<{ if: Record<string, unknown>; add: Record<string, unknown> }>;
/** Prefix name: field with 'gstack-' to match output directory name. Pi requires name == dirname. */
prefixName?: boolean;
};

// --- Generation ---
Expand Down
Loading