Skip to content
Merged
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ buck-out/
**/Pods

# Yarn
.yarn/*
**/.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
Expand All @@ -82,3 +82,8 @@ secring.gpg

# Typescript
**/*.tsbuildinfo

# skillgym
.skillgym-results/

.cursor
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
23 changes: 23 additions & 0 deletions skillgym.config.ts
Original file line number Diff line number Diff line change
@@ -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;
44 changes: 44 additions & 0 deletions skillgym/suites/brownfield-navigation-suite.ts
Original file line number Diff line number Diff line change
@@ -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;
40 changes: 40 additions & 0 deletions skillgym/suites/brownie-suite.ts
Original file line number Diff line number Diff line change
@@ -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;
58 changes: 58 additions & 0 deletions skillgym/suites/shared.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
41 changes: 15 additions & 26 deletions skills/brownfield-navigation/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)


108 changes: 23 additions & 85 deletions skills/brownfield-navigation/references/javascript-usage.md
Original file line number Diff line number Diff line change
@@ -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.<method>()` 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 (
<SafeAreaView>
<Button
title="Open native profile"
onPress={() => {
BrownfieldNavigation.openNativeProfile('user-123');
}}
/>
</SafeAreaView>
);
}
BrownfieldNavigation.openNativeProfile('user-123');
```

Portable takeaways:
- Import the generated module from `@callstack/brownfield-navigation`.
- Call the generated method directly from a user action such as `onPress`.
- If this method is missing or throws `undefined is not a function`, treat it as a codegen/rebuild drift signal first.
## Checklist

- Call generated methods directly from user actions first
- Keep params explicit and stable
- Keep method names aligned with generated API

## Fast triage

- `undefined is not a function`:
- Usually codegen/rebuild drift after contract edits
- Method exists but opens wrong screen:
- Usually delegate wiring issue (see native docs)
- Crash on first call:
- Often delegate registration order issue (see native docs)

## Quick reference
## Recovery order

- Import: `import BrownfieldNavigation from '@callstack/brownfield-navigation'`
- Typical calls:
- `BrownfieldNavigation.navigateToSettings()`
- `BrownfieldNavigation.navigateToReferrals('user-123')`
- Regenerate on contract change: `npx brownfield navigation:codegen`
- Retest order:
1. Confirm contract shape
2. Regenerate
3. Rebuild native apps
4. Retest JS call sites
- Error cues this skill should handle first:
- `undefined is not a function` on a Brownfield method
- JS method missing after updating `brownfield.navigation.ts`
- JS call parameters appear out of sync with generated API
1. Confirm contract shape
2. Run `npx brownfield navigation:codegen`
3. Rebuild iOS/Android
4. Retest JS call
Loading
Loading