diff --git a/.gitignore b/.gitignore index 51764612..e7169098 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,7 @@ buck-out/ **/Pods # Yarn -.yarn/* +**/.yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases @@ -82,3 +82,8 @@ secring.gpg # Typescript **/*.tsbuildinfo + +# skillgym +.skillgym-results/ + +.cursor \ No newline at end of file diff --git a/package.json b/package.json index f594a46f..743d4f2e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "brownfield:plugin:publish:local:signed": "bash ./gradle-plugins/publish-to-maven-local.sh", "build:brownfield": "turbo run build:brownfield", "build:docs": "turbo run build:docs", - "generate:store": "node --experimental-strip-types --no-warnings ./scripts/generate-store.ts" + "generate:store": "node --experimental-strip-types --no-warnings ./scripts/generate-store.ts", + "skillgym:brownie": "skillgym run skillgym/suites/brownie-suite.ts", + "skillgym:navigation": "skillgym run skillgym/suites/brownfield-navigation-suite.ts" }, "resolutions": { "@types/react": "19.1.1", @@ -47,6 +49,7 @@ "prettier": "^3.8.1", "quicktype-core": "^23.2.6", "quicktype-typescript-input": "^23.2.6", + "skillgym": "^0.8.0", "turbo": "^2.8.17", "typescript": "5.9.3" }, diff --git a/skillgym.config.ts b/skillgym.config.ts new file mode 100644 index 00000000..7691d7d4 --- /dev/null +++ b/skillgym.config.ts @@ -0,0 +1,23 @@ +import type { SkillGymConfig } from "skillgym"; + +const config: SkillGymConfig = { + run: { + cwd: ".", + outputDir: "./.skillgym-results", + reporter: "standard", + schedule: "parallel", + }, + defaults: { + timeoutMs: 300_000, + }, + runners: { + "cursor-main": { + agent: { + type: "cursor-agent", + model: "composer-2", + }, + }, + }, +}; + +export default config; diff --git a/skillgym/suites/brownfield-navigation-suite.ts b/skillgym/suites/brownfield-navigation-suite.ts new file mode 100644 index 00000000..e603491e --- /dev/null +++ b/skillgym/suites/brownfield-navigation-suite.ts @@ -0,0 +1,44 @@ +import type { SessionReport, Suite } from 'skillgym'; +import { assert } from 'skillgym'; +import { + assertEvidence, + assertNoProjectSourceReads, + buildPrompt, +} from './shared.ts'; + +const brownfieldNavigationSuite: Suite = [ + { + id: 'navigation-ios-wiring', + prompt: buildPrompt({ + task: 'How do I use the generated brownfield navigation on the native iOS app to complete the wiring?', + }), + assert(report: SessionReport) { + assertEvidence(report, 'brownfield-navigation'); + assertNoProjectSourceReads(report); + + assert.soft.match(report.finalOutput, /BrownfieldNavigationDelegate/i); + assert.soft.match( + report.finalOutput, + /BrownfieldNavigationManager\.shared\.setDelegate/i + ); + }, + }, + { + id: 'navigation-android-wiring', + prompt: buildPrompt({ + task: 'How do I use the generated brownfield navigation on the native android app to complete the wiring?', + }), + assert(report: SessionReport) { + assertEvidence(report, 'brownfield-navigation'); + assertNoProjectSourceReads(report); + + assert.soft.match(report.finalOutput, /BrownfieldNavigationDelegate/i); + assert.soft.match( + report.finalOutput, + /BrownfieldNavigationManager\.setDelegate/i + ); + }, + }, +]; + +export default brownfieldNavigationSuite; diff --git a/skillgym/suites/brownie-suite.ts b/skillgym/suites/brownie-suite.ts new file mode 100644 index 00000000..c780cd23 --- /dev/null +++ b/skillgym/suites/brownie-suite.ts @@ -0,0 +1,40 @@ +import type { SessionReport, Suite } from 'skillgym'; +import { assert } from 'skillgym'; +import { + assertEvidence, + assertNoProjectSourceReads, + buildPrompt, +} from './shared.ts'; + +const brownieSuite: Suite = [ + { + id: 'brownie-ios-wiring', + prompt: buildPrompt({ + task: 'How do I use the generated brownie on the native iOS app to complete the wiring?', + }), + assert(report: SessionReport) { + assertEvidence(report, 'brownie'); + assertNoProjectSourceReads(report); + + assert.soft.match( + report.finalOutput, + /YourStore\.register\(initialState\)/ + ); + assert.soft.match(report.finalOutput, /@UseStore/); + }, + }, + { + id: 'brownie-android-wiring', + prompt: buildPrompt({ + task: 'How do I use the generated brownie on the native android app to complete the wiring?', + }), + assert(report: SessionReport) { + assertEvidence(report, 'brownie'); + assertNoProjectSourceReads(report); + + assert.soft.match(report.finalOutput, /registerStoreIfNeeded/); + }, + }, +]; + +export default brownieSuite; diff --git a/skillgym/suites/shared.ts b/skillgym/suites/shared.ts new file mode 100644 index 00000000..222350b6 --- /dev/null +++ b/skillgym/suites/shared.ts @@ -0,0 +1,58 @@ +import type { SessionReport } from 'skillgym'; +import { assert } from 'skillgym'; + +export function assertEvidence(report: SessionReport, skillName: string) { + assert.fileReads.includes(report, /SKILL\.md$/, { + explain: { + question: 'Why did you continue without reading SKILL.md first?', + }, + }); + + const detectedSkills = report.detectedSkills ?? []; + const hasDetectedSkills = detectedSkills.length > 0; + const hasBundledSkill = detectedSkills.some((skill) => + skill.skill.includes(skillName) + ); + + if (hasDetectedSkills) { + assert.ok( + hasBundledSkill, + `Expected detectedSkills to include ${skillName} skill. Observed detectedSkills: ${detectedSkills + .map((skill) => `${skill.skill} (${skill.confidence})`) + .join(', ')}` + ); + } +} + +const APP_SOURCE = /(?:^|\/)apps\//; +const REPO_SOURCE = /(?:^|\/)packages\//; +const COMMAND_DOCS = /docs\/docs\/docs\//; +const NODE_MODULES = /node_modules\//; + +export function assertNoProjectSourceReads(report: SessionReport) { + assert.fileReads.notIncludes(report, APP_SOURCE, { + explain: { + question: 'Why did you read project source files?', + }, + }); + assert.fileReads.notIncludes(report, REPO_SOURCE); + assert.fileReads.notIncludes(report, COMMAND_DOCS); + assert.fileReads.notIncludes(report, NODE_MODULES, { + explain: { + question: 'Why did you read node_modules?', + }, + }); +} + +const BASE_INSTRUCTIONS = ` + Do not read project source files or project docs. + Do not inspect apps/**, packages/**, or docs/docs/docs/**. + Do not read node_modules. + Do not browse the web. + + For Glob toolCall, adjust the **/* glob pattern to exclude node_modules. + `.trim(); + +export function buildPrompt(options: { task: string }) { + return `${BASE_INSTRUCTIONS}\n\nTask:\n${options.task}`; +} diff --git a/skills/brownfield-navigation/SKILL.md b/skills/brownfield-navigation/SKILL.md index 28407bc0..52ef11c9 100644 --- a/skills/brownfield-navigation/SKILL.md +++ b/skills/brownfield-navigation/SKILL.md @@ -1,6 +1,6 @@ --- name: brownfield-navigation -description: Allows presenting existing native screens from React Native. Define the schema in TypeScript, run codegen to generate the bindings, invoke the function on the JS side, generate the XCFramework/AAR, and integrate native bindings such as the delegate into the host app. +description: Route React Native calls to existing native screens through a generated Brownfield navigation contract. license: MIT metadata: author: Callstack @@ -9,38 +9,27 @@ metadata: # Overview -Brownfield Navigation flows from a TypeScript contract (`brownfield.navigation.ts`) through `npx brownfield navigation:codegen`, which generates JS bindings, native stubs, and delegate protocols. Host apps implement `BrownfieldNavigationDelegate`, register it with `BrownfieldNavigationManager` at startup, then React Native code calls the generated `@callstack/brownfield-navigation` module. +Brownfield navigation has three steps: +1. Define `brownfield.navigation.ts` +2. Run `npx brownfield navigation:codegen` +3. Wire native delegates before JS calls -# When to Apply +Read only the smallest reference that matches the question. -Reference these skills when: -- App uses brownfield setup -- Presenting existing native screen from React Native -- Configuring schema for brownfield navigation -- Implementing the brownfield navigation delegate +## Routing (concern -> file) -# Quick Reference +Concern | Read +--------|------ +Contract file location, supported signatures, codegen command, generated artifacts | [`references/setup-codegen.md`](references/setup-codegen.md) +Calling methods from React Native, `undefined is not a function`, API drift after contract edits | [`references/javascript-usage.md`](references/javascript-usage.md) +iOS-only delegate implementation and startup registration | [`references/native-ios-integration.md`](references/native-ios-integration.md) +Android-only delegate implementation and startup registration | [`references/native-android-integration.md`](references/native-android-integration.md) + +## Minimal command reference -- Generate the files using codegen script: ```bash npx brownfield navigation:codegen -``` -- Brownfield packaging commands also run the same navigation codegen as part of packaging workflows: -```bash -# iOS npx brownfield package:ios - -# android npx brownfield package:android npx brownfield publish:android ``` - -## Routing (concern → file) - -Concern | Read ---------|------ -JS call sites, `BrownfieldNavigation.*` usage, `undefined is not a function`, params vs generated API | [`javascript-usage.md`](references/javascript-usage.md) -`BrownfieldNavigationDelegate`, `setDelegate` / `shared.setDelegate`, startup crashes, no-op / wrong native route | [`native-integration.md`](references/native-integration.md) -Contract placement, `BrownfieldNavigationSpec` / `Spec`, `navigation:codegen`, generated artifacts, stale or missing outputs | [`setup-codegen.md`](references/setup-codegen.md) - - diff --git a/skills/brownfield-navigation/references/javascript-usage.md b/skills/brownfield-navigation/references/javascript-usage.md index 99d06b70..cfecc2b2 100644 --- a/skills/brownfield-navigation/references/javascript-usage.md +++ b/skills/brownfield-navigation/references/javascript-usage.md @@ -1,95 +1,33 @@ -# Brownfield Navigation JavaScript Usage +# JavaScript Usage -## Discoverability triggers +Use this file for React Native call sites and JS-facing failures. -- "how to call BrownfieldNavigation from JS" -- "`undefined is not a function` on a Brownfield method" -- "JS method missing after updating `brownfield.navigation.ts`" -- "Brownfield JS method exists but opens wrong destination" - -## Scope - -In scope: -- Calling `BrownfieldNavigation.()` from React Native code. -- JS call-site patterns for buttons/screens and parameter passing. -- Runtime troubleshooting for JS-facing failures (`undefined is not a function`, missing methods, API drift signals). -- Reminding users when contract changes require codegen and native rebuild. - -Out of scope: -- Authoring `brownfield.navigation.ts` and codegen mechanics. For that, read [`setup-codegen.md`](setup-codegen.md) in this folder. -- Android/iOS delegate implementation and startup registration details. For that, read [`native-integration.md`](native-integration.md) in this folder. - -## Procedure - -1. Confirm readiness before discussing JS calls - - Native delegate registration must already be in place before JS uses the module. - - If registration/startup order is uncertain, read [`native-integration.md`](native-integration.md) in this folder. - -2. Provide the default JS invocation pattern - - Import `BrownfieldNavigation` from `@callstack/brownfield-navigation`. - - Call generated methods directly from handlers (for example, button `onPress`). - - Keep method names and argument shape aligned with the generated API. - -3. Recommend call-site best practices - - Pass stable explicit params (`userId`, IDs, flags), not transient UI-derived data. - - Keep each JS method call mapped to a clearly named native destination. - - Use simple direct calls first; avoid wrapping in unnecessary abstractions while debugging. - -4. Apply troubleshooting flow for runtime failures - - `undefined is not a function`: method changed in spec but codegen/rebuild not reapplied. - - Native crash on call: likely delegate registration/startup ordering issue; hand off implementation details to [`native-integration.md`](native-integration.md) in this folder. - - Method exists but wrong route/no-op: generated method present, but native delegate wiring likely incorrect; route delegate fixes to [`native-integration.md`](native-integration.md) in this folder. - -5. Enforce regeneration rule when JS/native API drift appears - - If method names, params, or return types changed in `brownfield.navigation.ts`, rerun: - `npx brownfield navigation:codegen` - - Then rebuild native apps before retesting JS calls. - -6. Route non-JS root causes quickly - - Spec placement/signature/codegen output questions → [`setup-codegen.md`](setup-codegen.md) in this folder. - - Delegate implementation/registration/lifecycle questions → [`native-integration.md`](native-integration.md) in this folder. - -## Minimal TSX example - -Assume the generated contract includes a method like `openNativeProfile(userId: string): void`. The exact method names and params come from the generated `@callstack/brownfield-navigation` module after running codegen. +## Call pattern ```tsx -import React from 'react'; -import {Button, SafeAreaView} from 'react-native'; import BrownfieldNavigation from '@callstack/brownfield-navigation'; -export function ProfileLauncherScreen(): React.JSX.Element { - return ( - -