This commit is contained in:
Andriy 2026-07-14 18:26:06 -07:00 committed by GitHub
commit 0f8ecea6da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 242 additions and 22 deletions

2
.gitignore vendored
View File

@ -23,6 +23,7 @@ bin/gstack-global-discover*
.hermes/
.gbrain/
.gbrain-source
.lingma/
.context/
extension/.auth.json
# xterm assets are vendored from npm at build time; not source-of-truth.
@ -44,3 +45,4 @@ docs/throughput-*.json
# gbrain local source-staging dir (capability checks, source clones) — runtime artifact
.sources/
.playwright-browsers

View File

@ -101,7 +101,7 @@ These are conversational skills. Your OpenClaw agent runs them directly via chat
### Other AI Agents
gstack works on 10 AI coding agents, not just Claude. Setup auto-detects which
gstack works on 11 AI coding agents, not just Claude. Setup auto-detects which
agents you have installed:
```bash
@ -121,6 +121,15 @@ 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-*/` |
| Qoder | `--host qoder` | `~/.lingma/skills/gstack-*/` |
You can also install skills to a custom directory with `--dir` (useful for
project-level skill installations):
```bash
# Install Qoder skills to a project-local directory
./setup --host qoder --dir ./my-project/.lingma/skills
```
**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.
@ -360,6 +369,7 @@ rm -rf ~/.codex/skills/gstack* 2>/dev/null
rm -rf ~/.factory/skills/gstack* 2>/dev/null
rm -rf ~/.kiro/skills/gstack* 2>/dev/null
rm -rf ~/.openclaw/skills/gstack* 2>/dev/null
rm -rf ~/.lingma/skills/gstack* 2>/dev/null
# 6. Remove temp files
rm -f /tmp/gstack-* 2>/dev/null

View File

@ -1,7 +1,7 @@
# Adding a New Host to gstack
gstack uses a declarative host config system. Each supported AI coding agent
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw) is defined
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Qoder) is defined
as a typed TypeScript config object. Adding a new host means creating one file
and re-exporting it. Zero code changes to the generator, setup, or tooling.
@ -17,6 +17,7 @@ hosts/
├── slate.ts # Slate (Random Labs)
├── cursor.ts # Cursor
├── openclaw.ts # OpenClaw (hybrid: config + adapter)
├── qoder.ts # Qoder (通义灵码)
└── index.ts # Registry: imports all, derives Host type
```
@ -141,6 +142,28 @@ freshness check passes, codex skill excluded.
Add install instructions for the new host in the appropriate section.
### 7. (Optional) Support project-level installation with `--dir`
If your host supports installing skills to a project-local directory (e.g. for
team-wide or repo-specific skill installations), extend the setup script to
support the `--dir` flag. Add your host to the `case` statement that handles
`CUSTOM_DIR` overrides in the `setup` script:
```bash
case "$HOST" in
myhost)
MYHOST_SKILLS="$CUSTOM_DIR"
MYHOST_GSTACK="$MYHOST_SKILLS/gstack"
;;
esac
```
Users can then install skills to a custom directory:
```bash
./setup --host myhost --dir ./my-project/.myhost/skills
```
## Config field reference
See `scripts/host-config.ts` for the full `HostConfig` interface with JSDoc

View File

@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';
import gbrain from './gbrain';
import qoder from './qoder';
/** 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, qoder];
/** 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, qoder };

48
hosts/qoder.ts Normal file
View File

@ -0,0 +1,48 @@
import type { HostConfig } from '../scripts/host-config';
const qoder: HostConfig = {
name: 'qoder',
displayName: 'Qoder',
cliCommand: 'qoder',
cliAliases: [],
globalRoot: '.lingma/skills/gstack',
localSkillRoot: '.lingma/skills/gstack',
hostSubdir: '.lingma',
usesEnvVars: true,
frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: null,
},
generation: {
generateMetadata: false,
skipSkills: ['codex'],
},
pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '~/.lingma/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.lingma/skills/gstack' },
{ from: '.claude/skills', to: '.lingma/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 qoder;

162
setup
View File

@ -18,12 +18,19 @@ INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
# Use custom Playwright browsers path if manually installed (avoids macOS cache permission issues)
if [ -d "$SOURCE_GSTACK_DIR/.playwright-browsers" ]; then
export PLAYWRIGHT_BROWSERS_PATH="$SOURCE_GSTACK_DIR/.playwright-browsers"
fi
CODEX_SKILLS="$HOME/.codex/skills"
CODEX_GSTACK="$CODEX_SKILLS/gstack"
FACTORY_SKILLS="$HOME/.factory/skills"
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
QODER_SKILLS="$HOME/.lingma/skills"
QODER_GSTACK="$QODER_SKILLS/gstack"
IS_WINDOWS=0
case "$(uname -s)" in
@ -82,12 +89,15 @@ SKILL_PREFIX=1
SKILL_PREFIX_FLAG=0
TEAM_MODE=0
NO_TEAM_MODE=0
CUSTOM_DIR=""
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, qoder, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host=*) HOST="${1#--host=}"; shift ;;
--local) LOCAL_INSTALL=1; shift ;;
--dir) [ -z "$2" ] && echo "Missing value for --dir (expected a directory path)" >&2 && exit 1; CUSTOM_DIR="$2"; shift 2 ;;
--dir=*) CUSTOM_DIR="${1#--dir=}"; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
--no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;;
--team) TEAM_MODE=1; shift ;;
@ -100,8 +110,13 @@ while [ $# -gt 0 ]; do
esac
done
# Expand ~ in --dir argument (bash doesn't expand it inside quoted strings)
if [ -n "$CUSTOM_DIR" ] && [ "${CUSTOM_DIR#\~}" != "$CUSTOM_DIR" ]; then
CUSTOM_DIR="${HOME}${CUSTOM_DIR#\~}"
fi
case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;;
claude|codex|kiro|factory|opencode|qoder|auto) ;;
openclaw)
echo ""
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
@ -136,7 +151,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, qoder, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
esac
# ─── Resolve skill prefix preference ─────────────────────────
@ -200,14 +215,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
INSTALL_OPENCODE=0
INSTALL_QODER=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 qoder >/dev/null 2>&1 && INSTALL_QODER=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_QODER" -eq 0 ]; then
INSTALL_CLAUDE=1
fi
elif [ "$HOST" = "claude" ]; then
@ -220,6 +237,20 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1
elif [ "$HOST" = "qoder" ]; then
INSTALL_QODER=1
fi
# ─── Apply custom directory override ──────────────────────────
# When --dir is specified, override the target skills directory
# for the selected host (supports project-level installations).
if [ -n "$CUSTOM_DIR" ]; then
case "$HOST" in
qoder)
QODER_SKILLS="$CUSTOM_DIR"
QODER_GSTACK="$QODER_SKILLS/gstack"
;;
esac
fi
migrate_direct_codex_install() {
@ -255,12 +286,12 @@ ensure_playwright_browser() {
# (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
(
cd "$SOURCE_GSTACK_DIR"
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch({ channel: 'chromium' }); await b.close(); })()" 2>/dev/null
)
else
(
cd "$SOURCE_GSTACK_DIR"
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch({ channel: "chromium" }); await browser.close();'
) >/dev/null 2>&1
fi
}
@ -477,11 +508,17 @@ fi
# 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then
echo "Installing Playwright Chromium..."
(
cd "$SOURCE_GSTACK_DIR"
bunx playwright install chromium
)
# Skip download if browser already exists in custom path (avoids macOS cache hang)
if [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ] && [ -d "$PLAYWRIGHT_BROWSERS_PATH/chromium-1208" ]; then
echo "Playwright Chromium already installed at $PLAYWRIGHT_BROWSERS_PATH"
else
echo "Installing Playwright Chromium..."
(
cd "$SOURCE_GSTACK_DIR"
# Use timeout to avoid hanging on macOS cache issues (timeout returns 124 on macOS)
timeout 120 bunx playwright install chromium 2>/dev/null || echo " Playwright install timed out or failed — browser-dependent skills may not work"
)
fi
if [ "$IS_WINDOWS" -eq 1 ]; then
# On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
@ -506,13 +543,15 @@ fi
if ! ensure_playwright_browser; then
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "gstack setup failed: Playwright Chromium could not be launched via Node.js" >&2
echo "Warning: Playwright Chromium could not be launched via Node.js" >&2
echo " This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2
echo " Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2
echo " Browser-dependent skills (/browse, /qa) will not work until fixed." >&2
else
echo "gstack setup failed: Playwright Chromium could not be launched" >&2
echo "Warning: Playwright Chromium could not be launched" >&2
echo " Browser-dependent skills (/browse, /qa) will not work until fixed." >&2
echo " To fix: 'bunx playwright install chromium' or set GSTACK_SKIP_BROWSER_CHECK=1" >&2
fi
exit 1
fi
# 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux).
@ -908,6 +947,59 @@ create_opencode_runtime_root() {
fi
}
create_qoder_runtime_root() {
local gstack_dir="$1"
local qoder_gstack="$2"
local qoder_dir="$gstack_dir/.lingma/skills"
if [ -L "$qoder_gstack" ]; then
rm -f "$qoder_gstack"
elif [ -d "$qoder_gstack" ] && [ "$qoder_gstack" != "$gstack_dir" ]; then
rm -rf "$qoder_gstack"
fi
mkdir -p "$qoder_gstack" "$qoder_gstack/browse" "$qoder_gstack/design" "$qoder_gstack/gstack-upgrade" "$qoder_gstack/review" "$qoder_gstack/qa" "$qoder_gstack/plan-devex-review"
if [ -f "$qoder_dir/gstack/SKILL.md" ]; then
_link_or_copy "$qoder_dir/gstack/SKILL.md" "$qoder_gstack/SKILL.md"
fi
if [ -d "$gstack_dir/bin" ]; then
_link_or_copy "$gstack_dir/bin" "$qoder_gstack/bin"
fi
if [ -d "$gstack_dir/browse/dist" ]; then
_link_or_copy "$gstack_dir/browse/dist" "$qoder_gstack/browse/dist"
fi
if [ -d "$gstack_dir/browse/bin" ]; then
_link_or_copy "$gstack_dir/browse/bin" "$qoder_gstack/browse/bin"
fi
if [ -d "$gstack_dir/design/dist" ]; then
_link_or_copy "$gstack_dir/design/dist" "$qoder_gstack/design/dist"
fi
if [ -f "$qoder_dir/gstack-upgrade/SKILL.md" ]; then
_link_or_copy "$qoder_dir/gstack-upgrade/SKILL.md" "$qoder_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" "$qoder_gstack/review/$f"
fi
done
if [ -d "$gstack_dir/review/specialists" ]; then
_link_or_copy "$gstack_dir/review/specialists" "$qoder_gstack/review/specialists"
fi
if [ -d "$gstack_dir/qa/templates" ]; then
_link_or_copy "$gstack_dir/qa/templates" "$qoder_gstack/qa/templates"
fi
if [ -d "$gstack_dir/qa/references" ]; then
_link_or_copy "$gstack_dir/qa/references" "$qoder_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" "$qoder_gstack/plan-devex-review/dx-hall-of-fame.md"
fi
if [ -f "$gstack_dir/ETHOS.md" ]; then
_link_or_copy "$gstack_dir/ETHOS.md" "$qoder_gstack/ETHOS.md"
fi
}
link_factory_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
@ -972,6 +1064,38 @@ link_opencode_skill_dirs() {
fi
}
link_qoder_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local qoder_dir="$gstack_dir/.lingma/skills"
local linked=()
if [ ! -d "$qoder_dir" ]; then
echo " Generating .lingma/ skill docs..."
( cd "$gstack_dir" && bun run gen:skill-docs --host qoder )
fi
if [ ! -d "$qoder_dir" ]; then
echo " warning: .lingma/skills/ generation failed — run 'bun run gen:skill-docs --host qoder' manually" >&2
return 1
fi
for skill_dir in "$qoder_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")")"
@ -1193,6 +1317,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
echo " opencode skills: $OPENCODE_SKILLS"
fi
# 6d. Install for Qoder
if [ "$INSTALL_QODER" -eq 1 ]; then
mkdir -p "$QODER_SKILLS"
create_qoder_runtime_root "$SOURCE_GSTACK_DIR" "$QODER_GSTACK"
link_qoder_skill_dirs "$SOURCE_GSTACK_DIR" "$QODER_SKILLS"
echo "gstack ready (qoder)."
echo " browse: $BROWSE_BIN"
echo " qoder skills: $QODER_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.

View File

@ -2372,9 +2372,9 @@ describe('setup script validation', () => {
expect(claudeSection).toContain('link_claude_root_skill_alias "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"');
});
test('setup supports --host auto|claude|codex|kiro|opencode', () => {
test('setup supports --host auto|claude|codex|kiro|factory|opencode|qoder', () => {
expect(setupContent).toContain('--host');
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|auto');
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|qoder|auto');
});
test('auto mode detects claude, codex, kiro, and opencode binaries', () => {

View File

@ -22,6 +22,7 @@ import {
slate,
cursor,
openclaw,
qoder,
} from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types';
@ -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', () => {
@ -53,6 +54,7 @@ describe('hosts/index.ts', () => {
expect(slate.name).toBe('slate');
expect(cursor.name).toBe('cursor');
expect(openclaw.name).toBe('openclaw');
expect(qoder.name).toBe('qoder');
});
test('getHostConfig returns correct config', () => {