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
309 changes: 309 additions & 0 deletions GEMINI.md

Large diffs are not rendered by default.

309 changes: 309 additions & 0 deletions GEMINI.md.tmpl

Large diffs are not rendered by default.

Binary file modified bin/gstack-global-discover
Binary file not shown.
2 changes: 1 addition & 1 deletion browse/bin/find-browse
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ if test -x "$DIR/find-browse"; then
fi
# Fallback: basic discovery with priority chain
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
for MARKER in .codex .agents .claude; do
for MARKER in .gemini .codex .agents .claude; do
if [ -n "$ROOT" ] && test -x "$ROOT/$MARKER/skills/gstack/browse/dist/browse"; then
echo "$ROOT/$MARKER/skills/gstack/browse/dist/browse"
exit 0
Expand Down
2 changes: 1 addition & 1 deletion browse/src/find-browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function getGitRoot(): string | null {
export function locateBinary(): string | null {
const root = getGitRoot();
const home = homedir();
const markers = ['.codex', '.agents', '.claude'];
const markers = ['.gemini', '.codex', '.agents', '.claude'];

// Workspace-local takes priority (for development)
if (root) {
Expand Down
19 changes: 9 additions & 10 deletions browse/test/find-browse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,23 @@ describe('locateBinary', () => {
}
});

test('priority chain checks .codex, .agents, .claude markers', () => {
test('priority chain checks .gemini, .codex, .agents, .claude markers', () => {
// Verify the source code implements the correct priority order.
// We read the function source to confirm the markers array order.
const src = require('fs').readFileSync(require('path').join(__dirname, '../src/find-browse.ts'), 'utf-8');
// The markers array should list .codex first, then .agents, then .claude
// The markers array should list .gemini first, then .codex, then .agents, then .claude
const markersMatch = src.match(/const markers = \[([^\]]+)\]/);
expect(markersMatch).not.toBeNull();
const markers = markersMatch![1];
const markers = JSON.parse(`[${markersMatch![1]}]`);
const geminiIdx = markers.indexOf('.gemini');
const codexIdx = markers.indexOf('.codex');
const agentsIdx = markers.indexOf('.agents');
const claudeIdx = markers.indexOf('.claude');
// All three must be present
expect(codexIdx).toBeGreaterThanOrEqual(0);
expect(agentsIdx).toBeGreaterThanOrEqual(0);
expect(claudeIdx).toBeGreaterThanOrEqual(0);
// .codex before .agents before .claude
expect(codexIdx).toBeLessThan(agentsIdx);
expect(agentsIdx).toBeLessThan(claudeIdx);
// All four must be present
expect(geminiIdx).toBe(0);
expect(codexIdx).toBe(1);
expect(agentsIdx).toBe(2);
expect(claudeIdx).toBe(3);
});

test('function signature accepts no arguments', () => {
Expand Down
14 changes: 10 additions & 4 deletions scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ const HOST: Host = (() => {
if (!HOST_ARG) return 'claude';
const val = HOST_ARG.includes('=') ? HOST_ARG.split('=')[1] : process.argv[process.argv.indexOf(HOST_ARG) + 1];
if (val === 'codex' || val === 'agents') return 'codex';
if (val === 'gemini') return 'gemini';

if (val === 'claude') return 'claude';
throw new Error(`Unknown host: ${val}. Use claude, codex, or agents.`);
throw new Error(`Unknown host: ${val}. Use claude, codex, gemini, or agents.`);
})();

// HostPaths, HOST_PATHS, and TemplateContext imported from ./resolvers/types (line 7-8)
Expand Down Expand Up @@ -2237,7 +2239,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
let outputDir: string | null = null;

// For codex host, route output to .agents/skills/{codexSkillName}/SKILL.md
if (host === 'codex') {
if (host === 'codex' || host === 'gemini') {
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
outputDir = path.join(ROOT, '.agents', 'skills', codexName);
fs.mkdirSync(outputDir, { recursive: true });
Expand Down Expand Up @@ -2274,7 +2276,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
}

// For codex host: transform frontmatter and replace Claude-specific paths
if (host === 'codex') {
if (host === 'codex' || host === 'gemini') {
// Extract hook safety prose BEFORE transforming frontmatter (which strips hooks)
const safetyProse = extractHookSafetyProse(tmplContent);

Expand All @@ -2292,6 +2294,10 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
content = content.replace(/\.claude\/skills\/gstack/g, ctx.paths.localSkillRoot);
content = content.replace(/\.claude\/skills\/review/g, '.agents/skills/gstack/review');
content = content.replace(/\.claude\/skills/g, '.agents/skills');
if (ctx.paths.configFile !== 'CLAUDE.md') {
content = content.replace(/CLAUDE\.md/g, ctx.paths.configFile);
}


if (outputDir) {
const codexName = codexSkillName(skillDir === '.' ? '' : skillDir);
Expand Down Expand Up @@ -2327,7 +2333,7 @@ const tokenBudget: Array<{ skill: string; lines: number; tokens: number }> = [];

for (const tmplPath of findTemplates()) {
// Skip /codex skill for codex host (self-referential — it's a Claude wrapper around codex exec)
if (HOST === 'codex') {
if (HOST === 'codex' || HOST === 'gemini') {
const dir = path.basename(path.dirname(tmplPath));
if (dir === 'codex') continue;
}
Expand Down
10 changes: 2 additions & 8 deletions scripts/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
/**
* RESOLVERS record — maps {{PLACEHOLDER}} names to generator functions.
* Each resolver takes a TemplateContext and returns the replacement string.
*/

import type { TemplateContext } from './types';

// Domain modules
import { generatePreamble } from './preamble';
import { generateTestFailureTriage } from './preamble';
import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup } from './browse';
import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch } from './design';
import { generateTestBootstrap, generateTestCoverageAuditPlan, generateTestCoverageAuditShip, generateTestCoverageAuditReview } from './testing';
import { generateReviewDashboard, generatePlanFileReviewReport, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec } from './review';
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer } from './utility';

import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateConfigFile } from './utility';
export const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
SLUG_EVAL: generateSlugEval,
SLUG_SETUP: generateSlugSetup,
Expand Down Expand Up @@ -45,4 +38,5 @@ export const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
PLAN_COMPLETION_AUDIT_REVIEW: generatePlanCompletionAuditReview,
PLAN_VERIFICATION_EXEC: generatePlanVerificationExec,
CO_AUTHOR_TRAILER: generateCoAuthorTrailer,
CONFIG_FILE: generateConfigFile,
};
16 changes: 12 additions & 4 deletions scripts/resolvers/preamble.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import type { TemplateContext } from './types';

function generatePreambleBash(ctx: TemplateContext): string {
const runtimeRoot = ctx.host === 'codex'
? `_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
let runtimeRoot = '';
if (ctx.host === 'codex') {
runtimeRoot = `_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
GSTACK_ROOT="$HOME/.codex/skills/gstack"
[ -n "$_ROOT" ] && [ -d "$_ROOT/.agents/skills/gstack" ] && GSTACK_ROOT="$_ROOT/.agents/skills/gstack"
GSTACK_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
`
: '';
`;
} else if (ctx.host === 'gemini') {
runtimeRoot = `_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
GSTACK_ROOT="$HOME/.gemini/skills/gstack"
[ -n "$_ROOT" ] && [ -d "$_ROOT/.agents/skills/gstack" ] && GSTACK_ROOT="$_ROOT/.agents/skills/gstack"
GSTACK_BIN="$GSTACK_ROOT/bin"
GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"
`;
}

return `## Preamble (run first)

Expand Down
30 changes: 15 additions & 15 deletions scripts/resolvers/testing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TemplateContext } from './types';

export function generateTestBootstrap(_ctx: TemplateContext): string {
export function generateTestBootstrap(ctx: TemplateContext): string {
return `## Test Framework Bootstrap

**Detect existing test framework and project runtime:**
Expand Down Expand Up @@ -129,9 +129,9 @@ Write TESTING.md with:
- Test layers: Unit tests (what, where, when), Integration tests, Smoke tests, E2E tests
- Conventions: file naming, assertion style, setup/teardown patterns

### B7. Update CLAUDE.md
### B7. Update ${ctx.paths.configFile}

First check: If CLAUDE.md already has a \`## Testing\` section → skip. Don't duplicate.
First check: If ${ctx.paths.configFile} already has a \`## Testing\` section → skip. Don't duplicate.

Append a \`## Testing\` section:
- Run command and test directory
Expand All @@ -150,7 +150,7 @@ Append a \`## Testing\` section:
git status --porcelain
\`\`\`

Only commit if there are changes. Stage all bootstrap files (config, test directory, TESTING.md, CLAUDE.md, .github/workflows/test.yml if created):
Only commit if there are changes. Stage all bootstrap files (config, test directory, TESTING.md, ${ctx.paths.configFile}, .github/workflows/test.yml if created):
\`git commit -m "chore: bootstrap test framework ({framework name})"\`

---`;
Expand Down Expand Up @@ -179,7 +179,7 @@ Only commit if there are changes. Stage all bootstrap files (config, test direct

type CoverageAuditMode = 'plan' | 'ship' | 'review';

function generateTestCoverageAuditInner(mode: CoverageAuditMode): string {
function generateTestCoverageAuditInner(mode: CoverageAuditMode, ctx: TemplateContext): string {
const sections: string[] = [];

// ── Intro (mode-specific) ──
Expand All @@ -197,8 +197,8 @@ function generateTestCoverageAuditInner(mode: CoverageAuditMode): string {

Before analyzing coverage, detect the project's test framework:

1. **Read CLAUDE.md** — look for a \`## Testing\` section with test command and framework name. If found, use that as the authoritative source.
2. **If CLAUDE.md has no testing section, auto-detect:**
1. **Read ${ctx.paths.configFile}** — look for a \`## Testing\` section with test command and framework name. If found, use that as the authoritative source.
2. **If ${ctx.paths.configFile} has no testing section, auto-detect:**

\`\`\`bash
setopt +o nomatch 2>/dev/null || true # zsh compat
Expand Down Expand Up @@ -460,7 +460,7 @@ Coverage line: \`Test Coverage Audit: N new code paths. M covered (X%). K tests

**7. Coverage gate:**

Before proceeding, check CLAUDE.md for a \`## Test Coverage\` section with \`Minimum:\` and \`Target:\` fields. If found, use those percentages. Otherwise use defaults: Minimum = 60%, Target = 80%.
Before proceeding, check ${ctx.paths.configFile} for a \`## Test Coverage\` section with \`Minimum:\` and \`Target:\` fields. If found, use those percentages. Otherwise use defaults: Minimum = 60%, Target = 80%.

Using the coverage percentage from the diagram in substep 4 (the \`COVERAGE: X/Y (Z%)\` line):

Expand Down Expand Up @@ -543,7 +543,7 @@ If no test framework detected → include gaps as INFORMATIONAL findings only, n

### Coverage Warning

After producing the coverage diagram, check the coverage percentage. Read CLAUDE.md for a \`## Test Coverage\` section with a \`Minimum:\` field. If not found, use default: 60%.
After producing the coverage diagram, check the coverage percentage. Read ${ctx.paths.configFile} for a \`## Test Coverage\` section with a \`Minimum:\` field. If not found, use default: 60%.

If coverage is below the minimum threshold, output a prominent warning **before** the regular review findings:

Expand All @@ -560,14 +560,14 @@ If coverage percentage cannot be determined, skip the warning silently.`);
return sections.join('\n');
}

export function generateTestCoverageAuditPlan(_ctx: TemplateContext): string {
return generateTestCoverageAuditInner('plan');
export function generateTestCoverageAuditPlan(ctx: TemplateContext): string {
return generateTestCoverageAuditInner('plan', ctx);
}

export function generateTestCoverageAuditShip(_ctx: TemplateContext): string {
return generateTestCoverageAuditInner('ship');
export function generateTestCoverageAuditShip(ctx: TemplateContext): string {
return generateTestCoverageAuditInner('ship', ctx);
}

export function generateTestCoverageAuditReview(_ctx: TemplateContext): string {
return generateTestCoverageAuditInner('review');
export function generateTestCoverageAuditReview(ctx: TemplateContext): string {
return generateTestCoverageAuditInner('review', ctx);
}
12 changes: 11 additions & 1 deletion scripts/resolvers/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export type Host = 'claude' | 'codex';
export type Host = 'claude' | 'codex' | 'gemini';

export interface HostPaths {
skillRoot: string;
localSkillRoot: string;
binDir: string;
browseDir: string;
configFile: string;
}

export const HOST_PATHS: Record<Host, HostPaths> = {
Expand All @@ -13,12 +14,21 @@ export const HOST_PATHS: Record<Host, HostPaths> = {
localSkillRoot: '.claude/skills/gstack',
binDir: '~/.claude/skills/gstack/bin',
browseDir: '~/.claude/skills/gstack/browse/dist',
configFile: 'CLAUDE.md',
},
codex: {
skillRoot: '$GSTACK_ROOT',
localSkillRoot: '.agents/skills/gstack',
binDir: '$GSTACK_BIN',
browseDir: '$GSTACK_BROWSE',
configFile: 'CLAUDE.md',
},
gemini: {
skillRoot: '$GSTACK_ROOT',
localSkillRoot: '.agents/skills/gstack',
binDir: '$GSTACK_BIN',
browseDir: '$GSTACK_BROWSE',
configFile: 'GEMINI.md',
},
};

Expand Down
11 changes: 7 additions & 4 deletions scripts/resolvers/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ branch name wherever the instructions say "the base branch" or \`<default>\`.
---`;
}

export function generateDeployBootstrap(_ctx: TemplateContext): string {
export function generateDeployBootstrap(ctx: TemplateContext): string {
const CFG = ctx.paths.configFile;
return `\`\`\`bash
# Check for persisted deploy config in CLAUDE.md
DEPLOY_CONFIG=$(grep -A 20 "## Deploy Configuration" CLAUDE.md 2>/dev/null || echo "NO_CONFIG")
# Check for persisted deploy config in ${CFG}
DEPLOY_CONFIG=$(grep -A 20 "## Deploy Configuration" ${CFG} 2>/dev/null || echo "NO_CONFIG")
echo "$DEPLOY_CONFIG"

# If config exists, parse it
Expand All @@ -78,7 +79,7 @@ for f in $(find .github/workflows -maxdepth 1 \\( -name '*.yml' -o -name '*.yaml
done
\`\`\`

If \`PERSISTED_PLATFORM\` and \`PERSISTED_URL\` were found in CLAUDE.md, use them directly
If \`PERSISTED_PLATFORM\` and \`PERSISTED_URL\` were found in ${CFG}, use them directly
and skip manual detection. If no persisted config exists, use the auto-detected platform
to guide deploy verification. If nothing is detected, ask the user via AskUserQuestion
in the decision tree below.
Expand Down Expand Up @@ -371,4 +372,6 @@ export function generateCoAuthorTrailer(ctx: TemplateContext): string {
return 'Co-Authored-By: OpenAI Codex <noreply@openai.com>';
}
return 'Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>';
export function generateConfigFile(ctx: TemplateContext): string {
return ctx.paths.configFile;
}
Loading