Skip to content

Commit d2906f8

Browse files
authored
feat: add skill gym for brownie and navigation agent skills (#308)
* feat: add and setup skillgym * feat: add template app for skillgym * chore: add scripts * fix: prettier * refactor: update skills and suites
1 parent c153378 commit d2906f8

20 files changed

Lines changed: 438 additions & 691 deletions

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ buck-out/
6464
**/Pods
6565

6666
# Yarn
67-
.yarn/*
67+
**/.yarn/*
6868
!.yarn/patches
6969
!.yarn/plugins
7070
!.yarn/releases
@@ -82,3 +82,8 @@ secring.gpg
8282

8383
# Typescript
8484
**/*.tsbuildinfo
85+
86+
# skillgym
87+
.skillgym-results/
88+
89+
.cursor

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"brownfield:plugin:publish:local:signed": "bash ./gradle-plugins/publish-to-maven-local.sh",
2020
"build:brownfield": "turbo run build:brownfield",
2121
"build:docs": "turbo run build:docs",
22-
"generate:store": "node --experimental-strip-types --no-warnings ./scripts/generate-store.ts"
22+
"generate:store": "node --experimental-strip-types --no-warnings ./scripts/generate-store.ts",
23+
"skillgym:brownie": "skillgym run skillgym/suites/brownie-suite.ts",
24+
"skillgym:navigation": "skillgym run skillgym/suites/brownfield-navigation-suite.ts"
2325
},
2426
"resolutions": {
2527
"@types/react": "19.1.1",
@@ -48,6 +50,7 @@
4850
"prettier": "^3.8.1",
4951
"quicktype-core": "^23.2.6",
5052
"quicktype-typescript-input": "^23.2.6",
53+
"skillgym": "^0.8.0",
5154
"turbo": "^2.8.17",
5255
"typescript": "5.9.3"
5356
},

skillgym.config.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { SkillGymConfig } from "skillgym";
2+
3+
const config: SkillGymConfig = {
4+
run: {
5+
cwd: ".",
6+
outputDir: "./.skillgym-results",
7+
reporter: "standard",
8+
schedule: "parallel",
9+
},
10+
defaults: {
11+
timeoutMs: 300_000,
12+
},
13+
runners: {
14+
"cursor-main": {
15+
agent: {
16+
type: "cursor-agent",
17+
model: "composer-2",
18+
},
19+
},
20+
},
21+
};
22+
23+
export default config;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { SessionReport, Suite } from 'skillgym';
2+
import { assert } from 'skillgym';
3+
import {
4+
assertEvidence,
5+
assertNoProjectSourceReads,
6+
buildPrompt,
7+
} from './shared.ts';
8+
9+
const brownfieldNavigationSuite: Suite = [
10+
{
11+
id: 'navigation-ios-wiring',
12+
prompt: buildPrompt({
13+
task: 'How do I use the generated brownfield navigation on the native iOS app to complete the wiring?',
14+
}),
15+
assert(report: SessionReport) {
16+
assertEvidence(report, 'brownfield-navigation');
17+
assertNoProjectSourceReads(report);
18+
19+
assert.soft.match(report.finalOutput, /BrownfieldNavigationDelegate/i);
20+
assert.soft.match(
21+
report.finalOutput,
22+
/BrownfieldNavigationManager\.shared\.setDelegate/i
23+
);
24+
},
25+
},
26+
{
27+
id: 'navigation-android-wiring',
28+
prompt: buildPrompt({
29+
task: 'How do I use the generated brownfield navigation on the native android app to complete the wiring?',
30+
}),
31+
assert(report: SessionReport) {
32+
assertEvidence(report, 'brownfield-navigation');
33+
assertNoProjectSourceReads(report);
34+
35+
assert.soft.match(report.finalOutput, /BrownfieldNavigationDelegate/i);
36+
assert.soft.match(
37+
report.finalOutput,
38+
/BrownfieldNavigationManager\.setDelegate/i
39+
);
40+
},
41+
},
42+
];
43+
44+
export default brownfieldNavigationSuite;

skillgym/suites/brownie-suite.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { SessionReport, Suite } from 'skillgym';
2+
import { assert } from 'skillgym';
3+
import {
4+
assertEvidence,
5+
assertNoProjectSourceReads,
6+
buildPrompt,
7+
} from './shared.ts';
8+
9+
const brownieSuite: Suite = [
10+
{
11+
id: 'brownie-ios-wiring',
12+
prompt: buildPrompt({
13+
task: 'How do I use the generated brownie on the native iOS app to complete the wiring?',
14+
}),
15+
assert(report: SessionReport) {
16+
assertEvidence(report, 'brownie');
17+
assertNoProjectSourceReads(report);
18+
19+
assert.soft.match(
20+
report.finalOutput,
21+
/YourStore\.register\(initialState\)/
22+
);
23+
assert.soft.match(report.finalOutput, /@UseStore/);
24+
},
25+
},
26+
{
27+
id: 'brownie-android-wiring',
28+
prompt: buildPrompt({
29+
task: 'How do I use the generated brownie on the native android app to complete the wiring?',
30+
}),
31+
assert(report: SessionReport) {
32+
assertEvidence(report, 'brownie');
33+
assertNoProjectSourceReads(report);
34+
35+
assert.soft.match(report.finalOutput, /registerStoreIfNeeded/);
36+
},
37+
},
38+
];
39+
40+
export default brownieSuite;

skillgym/suites/shared.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { SessionReport } from 'skillgym';
2+
import { assert } from 'skillgym';
3+
4+
export function assertEvidence(report: SessionReport, skillName: string) {
5+
assert.fileReads.includes(report, /SKILL\.md$/, {
6+
explain: {
7+
question: 'Why did you continue without reading SKILL.md first?',
8+
},
9+
});
10+
11+
const detectedSkills = report.detectedSkills ?? [];
12+
const hasDetectedSkills = detectedSkills.length > 0;
13+
const hasBundledSkill = detectedSkills.some((skill) =>
14+
skill.skill.includes(skillName)
15+
);
16+
17+
if (hasDetectedSkills) {
18+
assert.ok(
19+
hasBundledSkill,
20+
`Expected detectedSkills to include ${skillName} skill. Observed detectedSkills: ${detectedSkills
21+
.map((skill) => `${skill.skill} (${skill.confidence})`)
22+
.join(', ')}`
23+
);
24+
}
25+
}
26+
27+
const APP_SOURCE = /(?:^|\/)apps\//;
28+
const REPO_SOURCE = /(?:^|\/)packages\//;
29+
const COMMAND_DOCS = /docs\/docs\/docs\//;
30+
const NODE_MODULES = /node_modules\//;
31+
32+
export function assertNoProjectSourceReads(report: SessionReport) {
33+
assert.fileReads.notIncludes(report, APP_SOURCE, {
34+
explain: {
35+
question: 'Why did you read project source files?',
36+
},
37+
});
38+
assert.fileReads.notIncludes(report, REPO_SOURCE);
39+
assert.fileReads.notIncludes(report, COMMAND_DOCS);
40+
assert.fileReads.notIncludes(report, NODE_MODULES, {
41+
explain: {
42+
question: 'Why did you read node_modules?',
43+
},
44+
});
45+
}
46+
47+
const BASE_INSTRUCTIONS = `
48+
Do not read project source files or project docs.
49+
Do not inspect apps/**, packages/**, or docs/docs/docs/**.
50+
Do not read node_modules.
51+
Do not browse the web.
52+
53+
For Glob toolCall, adjust the **/* glob pattern to exclude node_modules.
54+
`.trim();
55+
56+
export function buildPrompt(options: { task: string }) {
57+
return `${BASE_INSTRUCTIONS}\n\nTask:\n${options.task}`;
58+
}
Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: brownfield-navigation
3-
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.
3+
description: Route React Native calls to existing native screens through a generated Brownfield navigation contract.
44
license: MIT
55
metadata:
66
author: Callstack
@@ -9,38 +9,27 @@ metadata:
99

1010
# Overview
1111

12-
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.
12+
Brownfield navigation has three steps:
13+
1. Define `brownfield.navigation.ts`
14+
2. Run `npx brownfield navigation:codegen`
15+
3. Wire native delegates before JS calls
1316

14-
# When to Apply
17+
Read only the smallest reference that matches the question.
1518

16-
Reference these skills when:
17-
- App uses brownfield setup
18-
- Presenting existing native screen from React Native
19-
- Configuring schema for brownfield navigation
20-
- Implementing the brownfield navigation delegate
19+
## Routing (concern -> file)
2120

22-
# Quick Reference
21+
Concern | Read
22+
--------|------
23+
Contract file location, supported signatures, codegen command, generated artifacts | [`references/setup-codegen.md`](references/setup-codegen.md)
24+
Calling methods from React Native, `undefined is not a function`, API drift after contract edits | [`references/javascript-usage.md`](references/javascript-usage.md)
25+
iOS-only delegate implementation and startup registration | [`references/native-ios-integration.md`](references/native-ios-integration.md)
26+
Android-only delegate implementation and startup registration | [`references/native-android-integration.md`](references/native-android-integration.md)
27+
28+
## Minimal command reference
2329

24-
- Generate the files using codegen script:
2530
```bash
2631
npx brownfield navigation:codegen
27-
```
28-
- Brownfield packaging commands also run the same navigation codegen as part of packaging workflows:
29-
```bash
30-
# iOS
3132
npx brownfield package:ios
32-
33-
# android
3433
npx brownfield package:android
3534
npx brownfield publish:android
3635
```
37-
38-
## Routing (concern → file)
39-
40-
Concern | Read
41-
--------|------
42-
JS call sites, `BrownfieldNavigation.*` usage, `undefined is not a function`, params vs generated API | [`javascript-usage.md`](references/javascript-usage.md)
43-
`BrownfieldNavigationDelegate`, `setDelegate` / `shared.setDelegate`, startup crashes, no-op / wrong native route | [`native-integration.md`](references/native-integration.md)
44-
Contract placement, `BrownfieldNavigationSpec` / `Spec`, `navigation:codegen`, generated artifacts, stale or missing outputs | [`setup-codegen.md`](references/setup-codegen.md)
45-
46-
Lines changed: 23 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,33 @@
1-
# Brownfield Navigation JavaScript Usage
1+
# JavaScript Usage
22

3-
## Discoverability triggers
3+
Use this file for React Native call sites and JS-facing failures.
44

5-
- "how to call BrownfieldNavigation from JS"
6-
- "`undefined is not a function` on a Brownfield method"
7-
- "JS method missing after updating `brownfield.navigation.ts`"
8-
- "Brownfield JS method exists but opens wrong destination"
9-
10-
## Scope
11-
12-
In scope:
13-
- Calling `BrownfieldNavigation.<method>()` from React Native code.
14-
- JS call-site patterns for buttons/screens and parameter passing.
15-
- Runtime troubleshooting for JS-facing failures (`undefined is not a function`, missing methods, API drift signals).
16-
- Reminding users when contract changes require codegen and native rebuild.
17-
18-
Out of scope:
19-
- Authoring `brownfield.navigation.ts` and codegen mechanics. For that, read [`setup-codegen.md`](setup-codegen.md) in this folder.
20-
- Android/iOS delegate implementation and startup registration details. For that, read [`native-integration.md`](native-integration.md) in this folder.
21-
22-
## Procedure
23-
24-
1. Confirm readiness before discussing JS calls
25-
- Native delegate registration must already be in place before JS uses the module.
26-
- If registration/startup order is uncertain, read [`native-integration.md`](native-integration.md) in this folder.
27-
28-
2. Provide the default JS invocation pattern
29-
- Import `BrownfieldNavigation` from `@callstack/brownfield-navigation`.
30-
- Call generated methods directly from handlers (for example, button `onPress`).
31-
- Keep method names and argument shape aligned with the generated API.
32-
33-
3. Recommend call-site best practices
34-
- Pass stable explicit params (`userId`, IDs, flags), not transient UI-derived data.
35-
- Keep each JS method call mapped to a clearly named native destination.
36-
- Use simple direct calls first; avoid wrapping in unnecessary abstractions while debugging.
37-
38-
4. Apply troubleshooting flow for runtime failures
39-
- `undefined is not a function`: method changed in spec but codegen/rebuild not reapplied.
40-
- Native crash on call: likely delegate registration/startup ordering issue; hand off implementation details to [`native-integration.md`](native-integration.md) in this folder.
41-
- 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.
42-
43-
5. Enforce regeneration rule when JS/native API drift appears
44-
- If method names, params, or return types changed in `brownfield.navigation.ts`, rerun:
45-
`npx brownfield navigation:codegen`
46-
- Then rebuild native apps before retesting JS calls.
47-
48-
6. Route non-JS root causes quickly
49-
- Spec placement/signature/codegen output questions → [`setup-codegen.md`](setup-codegen.md) in this folder.
50-
- Delegate implementation/registration/lifecycle questions → [`native-integration.md`](native-integration.md) in this folder.
51-
52-
## Minimal TSX example
53-
54-
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.
5+
## Call pattern
556

567
```tsx
57-
import React from 'react';
58-
import {Button, SafeAreaView} from 'react-native';
598
import BrownfieldNavigation from '@callstack/brownfield-navigation';
609

61-
export function ProfileLauncherScreen(): React.JSX.Element {
62-
return (
63-
<SafeAreaView>
64-
<Button
65-
title="Open native profile"
66-
onPress={() => {
67-
BrownfieldNavigation.openNativeProfile('user-123');
68-
}}
69-
/>
70-
</SafeAreaView>
71-
);
72-
}
10+
BrownfieldNavigation.openNativeProfile('user-123');
7311
```
7412

75-
Portable takeaways:
76-
- Import the generated module from `@callstack/brownfield-navigation`.
77-
- Call the generated method directly from a user action such as `onPress`.
78-
- If this method is missing or throws `undefined is not a function`, treat it as a codegen/rebuild drift signal first.
13+
## Checklist
14+
15+
- Call generated methods directly from user actions first
16+
- Keep params explicit and stable
17+
- Keep method names aligned with generated API
18+
19+
## Fast triage
20+
21+
- `undefined is not a function`:
22+
- Usually codegen/rebuild drift after contract edits
23+
- Method exists but opens wrong screen:
24+
- Usually delegate wiring issue (see native docs)
25+
- Crash on first call:
26+
- Often delegate registration order issue (see native docs)
7927

80-
## Quick reference
28+
## Recovery order
8129

82-
- Import: `import BrownfieldNavigation from '@callstack/brownfield-navigation'`
83-
- Typical calls:
84-
- `BrownfieldNavigation.navigateToSettings()`
85-
- `BrownfieldNavigation.navigateToReferrals('user-123')`
86-
- Regenerate on contract change: `npx brownfield navigation:codegen`
87-
- Retest order:
88-
1. Confirm contract shape
89-
2. Regenerate
90-
3. Rebuild native apps
91-
4. Retest JS call sites
92-
- Error cues this skill should handle first:
93-
- `undefined is not a function` on a Brownfield method
94-
- JS method missing after updating `brownfield.navigation.ts`
95-
- JS call parameters appear out of sync with generated API
30+
1. Confirm contract shape
31+
2. Run `npx brownfield navigation:codegen`
32+
3. Rebuild iOS/Android
33+
4. Retest JS call

0 commit comments

Comments
 (0)