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
15 changes: 14 additions & 1 deletion scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ const HOST_ARG_VAL: HostArg = (() => {
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 === 'factory' || val === 'droid') return 'factory';
if (val === 'gemini') return 'gemini';
if (val === 'claude') return 'claude';
if (val === 'all') return 'all';
throw new Error(`Unknown host: ${val}. Use claude, codex, factory, droid, agents, or all.`);
throw new Error(`Unknown host: ${val}. Use claude, codex, factory, gemini, droid, agents, or all.`);
})();

// For single-host mode, HOST is the host. For --host all, it's set per iteration below.
Expand Down Expand Up @@ -242,6 +243,7 @@ interface ExternalHostConfig {
const EXTERNAL_HOST_CONFIG: Record<string, ExternalHostConfig> = {
codex: { hostSubdir: '.agents', generateMetadata: true, descriptionLimit: 1024 },
factory: { hostSubdir: '.factory', generateMetadata: false },
gemini: { hostSubdir: '.agents', generateMetadata: true, descriptionLimit: 1024 },
};

// ─── Template Processing ────────────────────────────────────
Expand Down Expand Up @@ -300,6 +302,11 @@ function processExternalHost(
result = result.replace(/\.claude\/skills\/review/g, `${config.hostSubdir}/skills/gstack/review`);
result = result.replace(/\.claude\/skills/g, `${config.hostSubdir}/skills`);

// Replace CLAUDE.md with host-specific config file
if (ctx.paths.configFile !== 'CLAUDE.md') {
result = result.replace(/CLAUDE\.md/g, ctx.paths.configFile);
}

// Factory-only: translate Claude Code tool names to generic phrasing
if (host === 'factory') {
result = result.replace(/use the Bash tool/g, 'run this command');
Expand Down Expand Up @@ -375,6 +382,7 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
outputPath = result.outputPath;
symlinkLoop = result.symlinkLoop;
}
}

// Prepend generated header (after frontmatter)
const header = GENERATED_HEADER.replace('{{SOURCE}}', path.basename(tmplPath));
Expand Down Expand Up @@ -407,6 +415,11 @@ for (const currentHost of hostsToRun) {
const tokenBudget: Array<{ skill: string; lines: number; tokens: number }> = [];

for (const tmplPath of findTemplates()) {
// Skip /codex skill for external hosts (self-referential)
if (HOST === 'codex' || HOST === 'gemini' || HOST === 'factory') {
const dir = path.basename(path.dirname(tmplPath));
if (dir === 'codex') continue;
}
// Skip /codex skill for non-Claude hosts (it's a Claude wrapper around codex exec)
if (currentHost !== 'claude') {
const dir = path.basename(path.dirname(tmplPath));
Expand Down
25 changes: 6 additions & 19 deletions scripts/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@
* Each resolver takes a TemplateContext and returns the replacement string.
*/

import type { TemplateContext, ResolverFn } from './types';
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, generateDesignSetup, generateDesignMockup, generateDesignShotgunLoop } from './design';
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, generateScopeDrift } from './review';
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow } from './utility';
import { generateLearningsSearch, generateLearningsLog } from './learnings';
import { generateConfidenceCalibration } from './confidence';
import { generateInvokeSkill } from './composition';
import { generateReviewArmy } from './review-army';
import { generateReviewDashboard, generatePlanFileReviewReport, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec } from './review';
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateConfigFile } from './utility';

export const RESOLVERS: Record<string, ResolverFn> = {
export const RESOLVERS: Record<string, (ctx: TemplateContext) => string> = {
SLUG_EVAL: generateSlugEval,
SLUG_SETUP: generateSlugSetup,
COMMAND_REFERENCE: generateCommandReference,
Expand All @@ -40,23 +36,14 @@ export const RESOLVERS: Record<string, ResolverFn> = {
TEST_FAILURE_TRIAGE: generateTestFailureTriage,
SPEC_REVIEW_LOOP: generateSpecReviewLoop,
DESIGN_SKETCH: generateDesignSketch,
DESIGN_SETUP: generateDesignSetup,
DESIGN_MOCKUP: generateDesignMockup,
DESIGN_SHOTGUN_LOOP: generateDesignShotgunLoop,
BENEFITS_FROM: generateBenefitsFrom,
CODEX_SECOND_OPINION: generateCodexSecondOpinion,
ADVERSARIAL_STEP: generateAdversarialStep,
SCOPE_DRIFT: generateScopeDrift,
DEPLOY_BOOTSTRAP: generateDeployBootstrap,
CODEX_PLAN_REVIEW: generateCodexPlanReview,
PLAN_COMPLETION_AUDIT_SHIP: generatePlanCompletionAuditShip,
PLAN_COMPLETION_AUDIT_REVIEW: generatePlanCompletionAuditReview,
PLAN_VERIFICATION_EXEC: generatePlanVerificationExec,
CO_AUTHOR_TRAILER: generateCoAuthorTrailer,
LEARNINGS_SEARCH: generateLearningsSearch,
LEARNINGS_LOG: generateLearningsLog,
CONFIDENCE_CALIBRATION: generateConfidenceCalibration,
INVOKE_SKILL: generateInvokeSkill,
CHANGELOG_WORKFLOW: generateChangelogWorkflow,
REVIEW_ARMY: generateReviewArmy,
CONFIG_FILE: generateConfigFile,
};
2 changes: 1 addition & 1 deletion scripts/resolvers/preamble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { TemplateContext } from './types';
*/

function generatePreambleBash(ctx: TemplateContext): string {
const hostConfigDir: Record<string, string> = { codex: '.codex', factory: '.factory' };
const hostConfigDir: Record<string, string> = { codex: '.codex', factory: '.factory', gemini: '.gemini' };
const runtimeRoot = (ctx.host !== 'claude')
? `_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
GSTACK_ROOT="$HOME/${hostConfigDir[ctx.host]}/skills/gstack"
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);
}
14 changes: 13 additions & 1 deletion scripts/resolvers/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
export type Host = 'claude' | 'codex' | 'factory';
export type Host = 'claude' | 'codex' | 'factory' | 'gemini';

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

export const HOST_PATHS: Record<Host, HostPaths> = {
Expand All @@ -15,20 +16,31 @@ export const HOST_PATHS: Record<Host, HostPaths> = {
binDir: '~/.claude/skills/gstack/bin',
browseDir: '~/.claude/skills/gstack/browse/dist',
designDir: '~/.claude/skills/gstack/design/dist',
configFile: 'CLAUDE.md',
},
codex: {
skillRoot: '$GSTACK_ROOT',
localSkillRoot: '.agents/skills/gstack',
binDir: '$GSTACK_BIN',
browseDir: '$GSTACK_BROWSE',
designDir: '$GSTACK_DESIGN',
configFile: 'CLAUDE.md',
},
factory: {
skillRoot: '$GSTACK_ROOT',
localSkillRoot: '.factory/skills/gstack',
binDir: '$GSTACK_BIN',
browseDir: '$GSTACK_BROWSE',
designDir: '$GSTACK_DESIGN',
configFile: 'CLAUDE.md',
},
gemini: {
skillRoot: '$GSTACK_ROOT',
localSkillRoot: '.agents/skills/gstack',
binDir: '$GSTACK_BIN',
browseDir: '$GSTACK_BROWSE',
designDir: '$GSTACK_DESIGN',
configFile: 'GEMINI.md',
},
};

Expand Down
12 changes: 8 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 @@ -375,6 +376,9 @@ export function generateCoAuthorTrailer(ctx: TemplateContext): string {
}
return 'Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>';
}
export function generateConfigFile(ctx: TemplateContext): string {
return ctx.paths.configFile;
}

export function generateChangelogWorkflow(_ctx: TemplateContext): string {
return `## CHANGELOG (auto-generate)
Expand Down
Loading