agentic-team 0.0.0-stage → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -2
  3. package/bin/build-template.js +208 -0
  4. package/bin/index.js +1139 -0
  5. package/package.json +41 -4
  6. package/template/.claude-plugin/plugin.json +10 -0
  7. package/template/.orchestra/README.md +401 -0
  8. package/template/.orchestra/blueprints/README.md +87 -0
  9. package/template/.orchestra/blueprints/api-only.md +46 -0
  10. package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
  11. package/template/.orchestra/blueprints/saas-starter.md +69 -0
  12. package/template/.orchestra/config.yml +277 -0
  13. package/template/.orchestra/roles/orchestrator.md +105 -0
  14. package/template/.orchestra/roles/product-manager.md +438 -0
  15. package/template/.orchestra/scripts/compact-diff.awk +45 -0
  16. package/template/.orchestra/stacks/README.md +81 -0
  17. package/template/.orchestra/stacks/_lib/common.sh +3095 -0
  18. package/template/.orchestra/stacks/_repo/setup.sh +1109 -0
  19. package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
  20. package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
  21. package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
  22. package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
  23. package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
  24. package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
  25. package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
  26. package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
  27. package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
  28. package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
  29. package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
  30. package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
  31. package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
  32. package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
  33. package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
  34. package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
  35. package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
  36. package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
  37. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
  38. package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
  39. package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
  40. package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
  41. package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
  42. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
  43. package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
  44. package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
  45. package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
  46. package/template/.orchestra/trackers/github.sh +467 -0
  47. package/template/CLAUDE.md +54 -0
  48. package/template/agents/architect.md +83 -0
  49. package/template/agents/engineer-high.md +17 -0
  50. package/template/agents/engineer-low.md +17 -0
  51. package/template/agents/engineer-max.md +17 -0
  52. package/template/agents/engineer-medium.md +17 -0
  53. package/template/agents/engineer-xhigh.md +17 -0
  54. package/template/agents/lead.md +870 -0
  55. package/template/agents/milestone-reviewer.md +63 -0
  56. package/template/agents/orchestrator.md +105 -0
  57. package/template/agents/product-manager.md +438 -0
  58. package/template/agents/reviewer.md +301 -0
  59. package/template/agents/scaffolder.md +180 -0
  60. package/template/agents/tracker.md +202 -0
  61. package/template/commands/blueprint.md +23 -0
  62. package/template/commands/create-role.md +64 -0
  63. package/template/commands/export.md +45 -0
  64. package/template/commands/help.md +55 -0
  65. package/template/commands/hotfix.md +12 -0
  66. package/template/commands/orchestrator.md +12 -0
  67. package/template/commands/pm.md +18 -0
  68. package/template/commands/rewind.md +61 -0
  69. package/template/commands/setup.md +14 -0
  70. package/template/commands/start.md +29 -0
  71. package/template/commands/status.md +11 -0
  72. package/template/commands/sync.md +73 -0
  73. package/template/commands/verifier.md +53 -0
  74. package/template/hooks/audit-context.sh +153 -0
  75. package/template/hooks/enforce-role.sh +175 -0
  76. package/template/hooks/protect-system-files.sh +123 -0
  77. package/template/hooks/review-rounds.sh +172 -0
  78. package/template/rules/acceptance-check.orchestra.md +11 -0
  79. package/template/rules/analytics-kpi.orchestra.md +56 -0
  80. package/template/rules/code-standards.orchestra.md +34 -0
  81. package/template/rules/comments.orchestra.md +77 -0
  82. package/template/rules/commit-format.orchestra.md +20 -0
  83. package/template/rules/dependency-upgrade.orchestra.md +112 -0
  84. package/template/rules/design-parity.orchestra.md +170 -0
  85. package/template/rules/observability.orchestra.md +118 -0
  86. package/template/rules/performance.orchestra.md +186 -0
  87. package/template/rules/phase-limits.orchestra.md +3 -0
  88. package/template/rules/pull-request.orchestra.md +213 -0
  89. package/template/rules/role-boundaries.orchestra.md +117 -0
  90. package/template/rules/security.orchestra.md +106 -0
  91. package/template/rules/stuck-detection.orchestra.md +25 -0
  92. package/template/rules/testing-standards.orchestra.md +214 -0
  93. package/template/rules/tracker-authority.orchestra.md +190 -0
  94. package/template/rules/verification-gate.orchestra.md +56 -0
  95. package/template/skills/accessibility/SKILL.md +31 -0
  96. package/template/skills/android-testing/SKILL.md +112 -0
  97. package/template/skills/architecture/SKILL.md +175 -0
  98. package/template/skills/auth-setup/SKILL.md +105 -0
  99. package/template/skills/best-practices/SKILL.md +29 -0
  100. package/template/skills/code-optimizer/SKILL.md +27 -0
  101. package/template/skills/core-web-vitals/SKILL.md +32 -0
  102. package/template/skills/crud-api/SKILL.md +32 -0
  103. package/template/skills/debug/SKILL.md +28 -0
  104. package/template/skills/deployment/SKILL.md +104 -0
  105. package/template/skills/error-handling/SKILL.md +154 -0
  106. package/template/skills/frontend-design/SKILL.md +30 -0
  107. package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
  108. package/template/skills/ios-testing/SKILL.md +100 -0
  109. package/template/skills/mobile-testing/SKILL.md +102 -0
  110. package/template/skills/observability/SKILL.md +91 -0
  111. package/template/skills/project-structure/SKILL.md +114 -0
  112. package/template/skills/push-notifications/SKILL.md +146 -0
  113. package/template/skills/react-best-practices/SKILL.md +31 -0
  114. package/template/skills/realtime/SKILL.md +146 -0
  115. package/template/skills/review/SKILL.md +82 -0
  116. package/template/skills/testing/SKILL.md +44 -0
  117. package/template/skills/web-quality-audit/SKILL.md +47 -0
  118. package/template/skills/web-testing/SKILL.md +120 -0
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env bash
2
+ # Expo / React Native.
3
+ #
4
+ # No styling framework and no UI library. Tokens are a typed module and
5
+ # components are built from React Native primitives, so the kit owes nothing
6
+ # to a third party's release schedule.
7
+ set -euo pipefail
8
+
9
+ DIR="${1:?usage: setup.sh <target-dir>}"
10
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../_lib" && pwd)"
11
+ # shellcheck source=../_lib/common.sh
12
+ source "$LIB/common.sh"
13
+
14
+ # --no-agents-md: create-expo-app writes its own CLAUDE.md and
15
+ # .claude/settings.json, which would collide with the stack's pack.
16
+ log "scaffolding expo into $DIR"
17
+ # --yes and </dev/null together: inside an existing git repository the CLI
18
+ # asks whether to skip `git init`, and with a pipe for stdin it waits for an
19
+ # answer that never comes. A create tool must never be able to block a
20
+ # scaffold — closing stdin turns a missed prompt into an error instead of a
21
+ # hang.
22
+ npx --yes create-expo-app@latest "$DIR" \
23
+ --yes --template blank-typescript --no-agents-md --no-install </dev/null
24
+
25
+ assert_scaffolded "$DIR"
26
+ cd "$DIR"
27
+
28
+ setup_yarn
29
+ yarn_install
30
+
31
+ setup_typescript
32
+ setup_biome
33
+ setup_node_pin
34
+ setup_typecheck_script
35
+
36
+ # Native modules — expo install keeps them on SDK-compatible versions.
37
+ expo_install @sentry/react-native posthog-react-native
38
+ # Test deps, in the shape today's packages actually want. Verified against a
39
+ # real scaffold — each of these was a failure before it was a comment:
40
+ # - jest is neither a dependency nor a peer of jest-expo, so without it
41
+ # `npm test` is "jest: command not found"
42
+ # - RNTL 14 peers on `test-renderer`, NOT the deprecated react-test-renderer.
43
+ # react-test-renderer@latest wants a react newer than the SDK pins and npm
44
+ # refuses the whole tree.
45
+ # - @react-native/jest-preset IS required, but only at the version matching
46
+ # the installed react-native. At @latest it drags in a react-dom newer than
47
+ # the SDK's react and the whole tree is rejected. npm's flat hoisting hid
48
+ # this; yarn does not hoist, so the preset must be declared.
49
+ expo_install --dev jest-expo
50
+ # - jest is pinned to the 29 line because jest-expo depends on jest 29
51
+ # packages throughout. jest@latest installs 30, and the resulting
52
+ # jest-runtime 30 / jest-mock 29 split fails every suite with
53
+ # "this._moduleMocker.clearMocksOnScope is not a function".
54
+ add_latest --dev "jest@29" test-renderer @testing-library/react-native
55
+ # @types/jest is on the SDK table (57 wants 29.5.14, not the registry's 30),
56
+ # so it goes through expo install like every other managed package.
57
+ expo_install --dev @types/jest
58
+
59
+ # Match the preset to the installed react-native rather than taking @latest.
60
+ RN_VERSION="$(node -e 'const d=require("./package.json").dependencies;process.stdout.write(String(d["react-native"]||"").replace(/[^0-9.]/g,""))')"
61
+ if [ -n "$RN_VERSION" ]; then
62
+ add_latest --dev "@react-native/jest-preset@${RN_VERSION}"
63
+ else
64
+ warn "could not read react-native version — install @react-native/jest-preset manually"
65
+ fi
66
+ pkg_set scripts.test="jest --passWithNoTests" scripts.test:watch="jest --watch"
67
+ pkg_set jest.preset="jest-expo"
68
+
69
+ log "writing design system skeleton"
70
+ mkdir -p src/design/helpers
71
+
72
+ cat > src/design/tokens.ts <<'TS'
73
+ /**
74
+ * Design tokens. Populated from the Claude Design source before any
75
+ * component is written. Colour is one axis of several — do not stop there.
76
+ *
77
+ * `as const` is deliberate: it makes every value a literal type, so a typo
78
+ * in a component is a compile error rather than a silent undefined.
79
+ */
80
+ export const tokens = {
81
+ spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
82
+ radius: { control: 8, surface: 12, pill: 999 },
83
+ fontSize: { caption: 12, body: 16, title: 20, display: 28 },
84
+ fontWeight: { regular: "400", medium: "500", bold: "700" },
85
+ duration: { fast: 120, normal: 240 },
86
+ // Minimum touch target on both platforms. Not negotiable.
87
+ hitTarget: 44,
88
+ } as const;
89
+
90
+ /** Raw palette. Components never reference these — they use the theme. */
91
+ export const palette = {
92
+ white: "#ffffff",
93
+ black: "#000000",
94
+ neutral50: "#f9fafb",
95
+ neutral900: "#111827",
96
+ brand500: "#2563eb",
97
+ danger500: "#dc2626",
98
+ } as const;
99
+ TS
100
+
101
+ cat > src/design/theme.ts <<'TS'
102
+ import { palette } from "./tokens";
103
+
104
+ /**
105
+ * Semantic layer. Components reference these names, never the palette, so
106
+ * a theme change stays a one-file change.
107
+ */
108
+ export type Theme = {
109
+ surface: string;
110
+ onSurface: string;
111
+ accent: string;
112
+ danger: string;
113
+ };
114
+
115
+ export const lightTheme: Theme = {
116
+ surface: palette.white,
117
+ onSurface: palette.neutral900,
118
+ accent: palette.brand500,
119
+ danger: palette.danger500,
120
+ };
121
+
122
+ export const darkTheme: Theme = {
123
+ surface: palette.neutral900,
124
+ onSurface: palette.neutral50,
125
+ accent: palette.brand500,
126
+ danger: palette.danger500,
127
+ };
128
+ TS
129
+
130
+ cat > src/design/ThemeProvider.tsx <<'TSX'
131
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
132
+ import { useColorScheme } from "react-native";
133
+ import { darkTheme, lightTheme, type Theme } from "./theme";
134
+
135
+ export type ThemeName = "light" | "dark" | "system";
136
+
137
+ const ThemeContext = createContext<Theme>(lightTheme);
138
+
139
+ /** Read the active theme. Components call this instead of importing colours. */
140
+ export const useTheme = (): Theme => useContext(ThemeContext);
141
+
142
+ export function ThemeProvider({
143
+ children,
144
+ name = "system",
145
+ }: {
146
+ children: ReactNode;
147
+ name?: ThemeName;
148
+ }) {
149
+ const system = useColorScheme();
150
+ const theme = useMemo(() => {
151
+ const resolved = name === "system" ? (system ?? "light") : name;
152
+ return resolved === "dark" ? darkTheme : lightTheme;
153
+ }, [name, system]);
154
+
155
+ return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
156
+ }
157
+ TSX
158
+
159
+ cat > src/design/types.ts <<'TS'
160
+ // Shared design-system types only. Nothing app-specific, no business logic.
161
+ export type Size = "sm" | "md" | "lg";
162
+ export type Variant = "primary" | "secondary" | "danger";
163
+ TS
164
+
165
+ cat > src/design/index.ts <<'TS'
166
+ export * from "./tokens";
167
+ export * from "./theme";
168
+ export * from "./types";
169
+ export { ThemeProvider, useTheme } from "./ThemeProvider";
170
+ TS
171
+
172
+ install_pre_commit_gate
173
+ # Jest here, not vitest — setup_coverage writes a vitest config, which would
174
+ # be dead weight in this app.
175
+ setup_integrations_dir
176
+
177
+ setup_coverage_jest
178
+ setup_mutation
179
+
180
+ # The two commands that see what typecheck and jest cannot: a native module
181
+ # off the SDK's compatibility table. This is the class of defect that passes
182
+ # every JS test and then fails on a device.
183
+ # Installed rather than run through npx: CI should not resolve a different
184
+ # version of the checker on every run. add_latest, not expo_install — the
185
+ # checker is not on the SDK table, and routing it through `expo install`
186
+ # makes the CLI re-parse its own argv and fail on "install" as a directory.
187
+ add_latest --dev expo-doctor
188
+ pkg_set scripts.doctor="expo-doctor && expo install --check"
189
+ # A real build, without EAS or a signing identity. `expo export` runs Metro
190
+ # over the whole graph exactly as a release bundle does, so an import that
191
+ # only resolves in dev, a missing asset, or a native module the SDK does not
192
+ # ship all fail here — none of which tsc, lint or jest can see. CI has no
193
+ # device, so this is the only thing standing between a green suite and an app
194
+ # that cannot start.
195
+ pkg_set scripts.build="expo export --platform all --output-dir .expo-export"
196
+ grep -q '^\.expo-export/' .gitignore 2>/dev/null || printf '\n# expo export (build output)\n.expo-export/\n' >> .gitignore
197
+
198
+ # Our own API, faked at the network layer. Mocking the fetch wrapper instead
199
+ # would leave the wrapper — the thing that breaks when the API changes —
200
+ # untested. msw ships react-native export conditions.
201
+ add_latest --dev msw
202
+
203
+ # Pin the application id before anything reads it.
204
+ #
205
+ # Left unset, `expo prebuild` derives it from the machine's username — the
206
+ # developer gets com.sulhadin.mobile and CI gets com.runner.mobile, for the
207
+ # same commit. Every id-addressed thing downstream then points at an app that
208
+ # does not exist on the device it is running against: maestro flows, push
209
+ # certificates, deep links, store uploads.
210
+ #
211
+ # com.example is deliberately not a plausible id. It has to be replaced with
212
+ # the real organisation identifier before any release, and it should be
213
+ # obvious that it has not been.
214
+ node -e '
215
+ const fs = require("fs");
216
+ const f = ["app.json", "app.config.json"].find((x) => fs.existsSync(x));
217
+ if (!f) process.exit(0);
218
+ const doc = JSON.parse(fs.readFileSync(f, "utf8"));
219
+ const expo = (doc.expo = doc.expo || {});
220
+ const id = `com.example.${(expo.slug || "app").replace(/[^a-z0-9]/gi, "")}`;
221
+ expo.android = { ...(expo.android || {}) };
222
+ expo.ios = { ...(expo.ios || {}) };
223
+ if (!expo.android.package) expo.android.package = id;
224
+ if (!expo.ios.bundleIdentifier) expo.ios.bundleIdentifier = id;
225
+ fs.writeFileSync(f, JSON.stringify(doc, null, 2) + "\n");
226
+ console.log(" [+] application id pinned to " + expo.android.package);
227
+ '
228
+ warn "the application id is a placeholder — set ios.bundleIdentifier and"
229
+ warn "android.package to your real organisation id before any release."
230
+
231
+ # Sentry's Xcode build phase runs sentry-cli to upload source maps, and with
232
+ # no organisation configured it does not warn — it fails the build with
233
+ # exit 65. A freshly scaffolded app therefore cannot build for iOS at all,
234
+ # while Android builds fine, so nothing catches it until someone opens Xcode.
235
+ #
236
+ # sentry-xcode.sh gates on a shell variable in the build phase's own
237
+ # environment: if [ "$SENTRY_DISABLE_AUTO_UPLOAD" != true ]
238
+ # It never sources a dotenv, and ios/.xcode.env is regenerated by every
239
+ # `expo prebuild`, so the variable has to be set where the build is invoked.
240
+ #
241
+ # Not a blanket disable: with real credentials present the upload still runs.
242
+ # Symbols remain required before release — see
243
+ # .claude/rules/observability.orchestra.md. This only stops a build that has
244
+ # no credentials from failing because of it.
245
+ SENTRY_GUARD='SENTRY_DISABLE_AUTO_UPLOAD=$([ -n "$SENTRY_AUTH_TOKEN" ] && echo false || echo true)'
246
+ # LANG for the same reason e2e-ios.sh sets it, and it belongs here too:
247
+ # `expo run:ios` shells out to CocoaPods, which reads the Podfile through
248
+ # ruby's default external encoding. With no UTF-8 locale that is US-ASCII and
249
+ # pod install dies inside String#unicode_normalize — after which xcodebuild
250
+ # reports "the sandbox is not in sync with the Podfile.lock", which names
251
+ # neither the cause nor the fix. The e2e path was guarded and the path a
252
+ # developer actually types was not.
253
+ IOS_LOCALE='LANG=${LANG:-en_US.UTF-8}'
254
+ pkg_set "scripts.ios=${IOS_LOCALE} ${SENTRY_GUARD} expo run:ios" \
255
+ "scripts.android=${SENTRY_GUARD} expo run:android"
256
+ log "sentry: symbol upload runs only when SENTRY_AUTH_TOKEN is present"
257
+
258
+ # The launch flow has to assert something only this app can render. Without a
259
+ # marker it can only assert "some element is visible", which React Native's
260
+ # red bundle-error screen satisfies — the one failure the check exists to
261
+ # catch.
262
+ #
263
+ # The root view owns the marker, so the assertion holds no matter what the
264
+ # screen becomes. setup_maestro refuses to write flows without it.
265
+ cat > App.tsx <<'TSX'
266
+ import { StatusBar } from "expo-status-bar";
267
+ import { StyleSheet, Text, View } from "react-native";
268
+
269
+ export default function App() {
270
+ // testID="app-root" is load-bearing: .maestro/launch.yaml asserts it to
271
+ // prove this app's tree actually mounted. Keep it on whatever the root
272
+ // becomes — a navigator, a provider tree — or the launch check weakens
273
+ // to "something is on screen", which a bundle-error screen satisfies.
274
+ return (
275
+ <View style={styles.container} testID="app-root">
276
+ <Text>Open up App.tsx to start working on your app!</Text>
277
+ <StatusBar style="auto" />
278
+ </View>
279
+ );
280
+ }
281
+
282
+ const styles = StyleSheet.create({
283
+ container: {
284
+ flex: 1,
285
+ backgroundColor: "#fff",
286
+ alignItems: "center",
287
+ justifyContent: "center",
288
+ },
289
+ });
290
+ TSX
291
+ log "App.tsx carries testID=app-root for the launch flow"
292
+
293
+ setup_maestro
294
+
295
+ # Expo's tsconfig sets an explicit types list, so jest globals are invisible
296
+ # to tsc until "jest" is added to it.
297
+ node -e "$JSONC_READ"'
298
+ const fs = require("fs");
299
+ const f = "tsconfig.json";
300
+ if (!fs.existsSync(f)) process.exit(0);
301
+ const c = readJsonc(f);
302
+ c.compilerOptions = { ...(c.compilerOptions || {}) };
303
+ c.compilerOptions.types = [...new Set([...(c.compilerOptions.types || []), "jest"])];
304
+ fs.writeFileSync(f, JSON.stringify(c, null, 2) + "\n");
305
+ '
306
+
307
+ # Not refresh_scaffold_deps: in an Expo app the SDK is the authority, not
308
+ # the registry. setup_typescript installed typescript@latest, which the
309
+ # SDK does not accept — this puts every managed package back on the table
310
+ # and fails the scaffold if any of them is still off it.
311
+ assert_expo_sdk_aligned
312
+
313
+ # Format last: every generated file — the design skeleton, tailwind config
314
+ # and the tsconfig edit above — lands after setup_biome ran, and an
315
+ # unformatted scaffold is rejected by the gate this script just installed.
316
+ yarn biome check --write . >/dev/null 2>&1 || true
317
+
318
+ # ── e2e, as a script rather than only as CI steps ─────────────────────────
319
+ # The workflow used to carry the build and the maestro run inline, which made
320
+ # it unrunnable anywhere else: nobody could reproduce a red e2e locally, and
321
+ # the orchestrator's own CI could only test a *copy* of those steps. A copy
322
+ # that drifts proves nothing. One script, called by both.
323
+ cat > e2e-ios.sh <<'E2E'
324
+ #!/usr/bin/env bash
325
+ # Build the app for a simulator and run the maestro flows against it.
326
+ # Called by .github/workflows/mobile-e2e.yml and runnable by hand.
327
+ set -euo pipefail
328
+ cd "$(dirname "$0")"
329
+
330
+ # A debug e2e build has no Sentry credentials and must not fail for want of
331
+ # them. Sentry's Xcode phase gates on this exact variable and kills the build
332
+ # with exit 65 otherwise.
333
+ export SENTRY_DISABLE_AUTO_UPLOAD="${SENTRY_DISABLE_AUTO_UPLOAD:-true}"
334
+
335
+ # CocoaPods reads the Podfile through ruby's default external encoding. With
336
+ # no locale set that is US-ASCII, and `pod install` dies inside
337
+ # String#unicode_normalize with "Unicode Normalization not appropriate for
338
+ # ASCII-8BIT" — then xcodebuild fails with the far less helpful "sandbox is
339
+ # not in sync with the Podfile.lock".
340
+ export LANG="${LANG:-en_US.UTF-8}"
341
+
342
+ # maestro is a JVM application, and nothing else in this script needs a JDK.
343
+ # Without one it dies on "Unable to locate a Java Runtime" after the build has
344
+ # already been paid for.
345
+ if [ -z "${JAVA_HOME:-}" ]; then
346
+ JAVA_HOME="$(/usr/libexec/java_home 2>/dev/null || true)"
347
+ [ -n "$JAVA_HOME" ] && export JAVA_HOME
348
+ fi
349
+ command -v java >/dev/null 2>&1 || [ -x "${JAVA_HOME:-}/bin/java" ] || {
350
+ echo "no JDK on PATH — maestro cannot run without one" >&2
351
+ exit 1
352
+ }
353
+
354
+ if ! command -v maestro >/dev/null 2>&1; then
355
+ if [ -x "$HOME/.maestro/bin/maestro" ]; then
356
+ PATH="$HOME/.maestro/bin:$PATH"; export PATH
357
+ else
358
+ echo "── installing maestro"
359
+ curl -fsSL https://get.maestro.mobile.dev | bash
360
+ PATH="$HOME/.maestro/bin:$PATH"; export PATH
361
+ fi
362
+ fi
363
+
364
+ echo "── native dependency check"
365
+ yarn doctor
366
+
367
+ # Two reasons this is not `run:ios --configuration Debug`, both reproduced on
368
+ # a real machine. A debug build loads its JS from Metro, so the command stays
369
+ # attached to the bundler and **never exits** — the job burns its timeout and
370
+ # maestro never runs. And a debug build with no dev server opens on the red
371
+ # error screen, which maestro's launch check happily passes.
372
+ #
373
+ # Release embeds the bundle; --no-bundler skips Metro entirely. Both flags are
374
+ # the CLI's own, read from `expo run:ios --help`. Simulator builds are
375
+ # unsigned, so no certificate is involved.
376
+ echo "── build and install on a simulator"
377
+ yarn expo run:ios --configuration Release --no-bundler
378
+
379
+ echo "── maestro"
380
+ maestro test .maestro
381
+ E2E
382
+ chmod +x e2e-ios.sh
383
+ log "e2e-ios.sh (build + maestro, runnable by hand and from CI)"
384
+
385
+ cat > e2e-android.sh <<'E2E'
386
+ #!/usr/bin/env bash
387
+ # Build the release APK, then drive it with maestro.
388
+ #
389
+ # Two phases, because the emulator is not ours to own: in CI the
390
+ # android-emulator-runner action boots one and runs a script inside it, so the
391
+ # build has to happen before that and the test inside it.
392
+ #
393
+ # ./e2e-android.sh build — doctor, prebuild, assembleRelease
394
+ # ./e2e-android.sh test — install on the connected device and run maestro
395
+ # ./e2e-android.sh — both, against whatever device is attached
396
+ set -euo pipefail
397
+ cd "$(dirname "$0")"
398
+
399
+ export SENTRY_DISABLE_AUTO_UPLOAD="${SENTRY_DISABLE_AUTO_UPLOAD:-true}"
400
+
401
+ APK="android/app/build/outputs/apk/release/app-release.apk"
402
+
403
+ find_jdk() {
404
+ [ -n "${JAVA_HOME:-}" ] && [ -x "$JAVA_HOME/bin/java" ] && return 0
405
+ for candidate in "$(/usr/libexec/java_home -v 21 2>/dev/null || true)" \
406
+ /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
407
+ /usr/lib/jvm/temurin-21-jdk-amd64 \
408
+ /usr/lib/jvm/java-21-openjdk-amd64; do
409
+ [ -n "$candidate" ] && [ -x "$candidate/bin/java" ] && { export JAVA_HOME="$candidate"; return 0; }
410
+ done
411
+ echo "no JDK 21 found — gradle and maestro both need one" >&2
412
+ exit 1
413
+ }
414
+
415
+ ensure_maestro() {
416
+ command -v maestro >/dev/null 2>&1 && return 0
417
+ if [ -x "$HOME/.maestro/bin/maestro" ]; then
418
+ PATH="$HOME/.maestro/bin:$PATH"; export PATH; return 0
419
+ fi
420
+ echo "── installing maestro"
421
+ curl -fsSL https://get.maestro.mobile.dev | bash
422
+ PATH="$HOME/.maestro/bin:$PATH"; export PATH
423
+ }
424
+
425
+ do_build() {
426
+ find_jdk
427
+ echo "── native dependency check"
428
+ yarn doctor
429
+
430
+ echo "── generate the native project"
431
+ yarn expo prebuild --platform android --no-install
432
+
433
+ # Release, not debug. A debug APK contains no JS bundle — it fetches one
434
+ # from Metro at launch — so on a machine with no dev server the app opens on
435
+ # the red "could not connect" screen. Maestro's launch check passes on that
436
+ # screen, which is worse than failing: the run goes green having proved
437
+ # nothing. Release embeds the bundle. Expo's template signs release with the
438
+ # debug key, so no keystore is needed.
439
+ echo "── build release APK"
440
+ (cd android && ./gradlew assembleRelease --no-daemon)
441
+ [ -f "$APK" ] || { echo "no APK at $APK after assembleRelease" >&2; exit 1; }
442
+ }
443
+
444
+ do_test() {
445
+ find_jdk
446
+ ensure_maestro
447
+ [ -f "$APK" ] || { echo "no APK at $APK — run './e2e-android.sh build' first" >&2; exit 1; }
448
+
449
+ echo "── install"
450
+ adb install -r "$APK"
451
+
452
+ echo "── maestro"
453
+ maestro test .maestro
454
+ }
455
+
456
+ case "${1:-all}" in
457
+ build) do_build ;;
458
+ test) do_test ;;
459
+ all) do_build; do_test ;;
460
+ *) echo "usage: $0 [build|test]" >&2; exit 2 ;;
461
+ esac
462
+ E2E
463
+ chmod +x e2e-android.sh
464
+ log "e2e-android.sh (build + maestro, two phases so CI can own the emulator)"
465
+
466
+ assert_verification_scripts
467
+ assert_scaffold_clean
468
+ log "mobile-expo ready in $DIR"
469
+ warn "e2e flows are in .maestro/ — fill in core-flow.yaml before trusting the suite."
470
+ warn "The maestro CLI installs separately: curl -fsSL https://get.maestro.mobile.dev | bash"
@@ -0,0 +1,141 @@
1
+ # Mobile — Expo / React Native
2
+
3
+ ## Who you are
4
+
5
+ You are two engineers at once, and both have to be satisfied:
6
+
7
+ - a **senior frontend engineer** — component API design, state, render cost
8
+ - a **senior mobile engineer** — platform conventions, touch targets, safe
9
+ areas, keyboard behavior, list performance, offline and slow networks
10
+
11
+ Web habits that do not survive on a phone: hover states, tiny tap targets,
12
+ unbounded lists, assuming a fast network, ignoring the notch. Catch these
13
+ in your own work before review does.
14
+
15
+ SOLID, KISS, YAGNI. No overengineering.
16
+
17
+ ## Structure
18
+
19
+ Mirrors the web stacks deliberately — one mental model across the repo.
20
+
21
+ ```
22
+ src/
23
+ ├── design/ # the UI kit. no business logic. ever.
24
+ │ ├── tokens.ts # raw scales
25
+ │ ├── theme.ts # semantic layer
26
+ │ ├── ThemeProvider.tsx
27
+ │ ├── types.ts
28
+ │ ├── helpers/
29
+ │ ├── index.ts
30
+ │ └── Button/
31
+ │ ├── Button.tsx
32
+ │ ├── Button.test.tsx
33
+ │ ├── types.ts
34
+ │ └── index.ts
35
+ └── ...
36
+ ```
37
+
38
+ Every component gets its own folder: component, tests, types, `index.ts`.
39
+
40
+ `src/design/` holds nothing that is not design.
41
+
42
+ ## Styling — no framework
43
+
44
+ There is no Tailwind, no NativeWind, and no UI library here. Styling is
45
+ `StyleSheet.create` over a typed token module.
46
+
47
+ That is a deliberate trade. A styling framework on React Native buys terse
48
+ class strings and costs a build-time transform, a peer-version treadmill,
49
+ and a layer between you and the platform when something renders wrong. At
50
+ the size of one design system it does not pay for itself.
51
+
52
+ ```
53
+ src/design/
54
+ ├── tokens.ts # spacing, radius, type scale, motion, hit targets
55
+ ├── theme.ts # semantic names over the palette
56
+ ├── ThemeProvider.tsx
57
+ ├── types.ts
58
+ └── index.ts
59
+ ```
60
+
61
+ `tokens.ts` uses `as const`, so every value is a literal type and a typo in
62
+ a component is a compile error rather than a silent `undefined`.
63
+
64
+ Components call `useTheme()`. They never import the palette directly — that
65
+ is what keeps a theme change to one file.
66
+
67
+ ## No ready-made components
68
+
69
+ The kit is built from React Native primitives — `Pressable`, `View`,
70
+ `Text`, `FlatList`. No Paper, no NativeBase, no imported button.
71
+
72
+ Platform primitives are the substrate, not a UI kit. Wrapping them in our
73
+ own components is the job; installing someone else's opinion is not.
74
+
75
+ ## Order of work
76
+
77
+ Tokens before components. Extract every axis from the design source —
78
+ colour, spacing, typography, radii, elevation, motion — into `tokens.ts`,
79
+ then map meaning onto them in `theme.ts`.
80
+
81
+ A component with a hardcoded `#2563eb` or `padding: 13` is a bug.
82
+
83
+ ## Platform rules
84
+
85
+ - Minimum touch target 44×44pt. Smaller is a defect, not a design choice.
86
+ - Respect safe areas. Never assume a rectangular screen.
87
+ - Long lists are virtualized. A `map()` over server data will jank.
88
+ - Handle the keyboard: avoidance, dismissal, and the return key.
89
+ - Every screen has a loading, empty, and error state. All three.
90
+ - Test on both platforms. "Works on iOS" is half an answer.
91
+
92
+ ## Memory — the collector cannot end what you still hold
93
+
94
+ React does not free a subscription for you, and the collector cannot free
95
+ what a live closure still references. So:
96
+
97
+ - **Every effect that starts something returns the function that stops
98
+ it.** `AppState`, `Dimensions`, `Keyboard`, notification and deep-link
99
+ listeners all return a subscription — `.remove()` it; `setInterval` →
100
+ `clearInterval`; a fetch → an `AbortController` aborted in the cleanup. The gate checks that the cleanup
101
+ exists; only you can check that it stops the right thing.
102
+ - State that only grows is a leak with a UI. A list that appends on every
103
+ event and never trims, a map of every id ever seen — bound it, or key it
104
+ by what is on screen.
105
+ - A closure created in render captures the render's state and is recreated
106
+ every render. That is by design; it becomes a cost when it is passed to a
107
+ memoized child or a long list. `.claude/rules/performance.orchestra.md`
108
+ says where that matters.
109
+ - A module-level cache is process-lifetime. Give it a maximum and an
110
+ eviction rule, or it is the thing that grows with uptime.
111
+
112
+ On a phone the budget is smaller and the collector is Hermes, not V8. A
113
+ leaked screen is a leaked native view tree, and the OS kills the app when it
114
+ notices — which reads as a crash with no stack trace.
115
+
116
+ ## Testing
117
+
118
+ Coverage target is effectively total. The deciding rule:
119
+
120
+ > If one of our functions calls into a third-party library, that function
121
+ > has a test. When Dependabot bumps the library and behavior changes, the
122
+ > test must fail.
123
+
124
+ Tooling differs from web on purpose: `jest-expo` plus React Native Testing
125
+ Library. Vitest browser mode drives real browsers, which React Native does
126
+ not have.
127
+
128
+ - Interaction tests are primary — press, focus, disabled, navigation.
129
+ - Snapshots are secondary. Snapshot-only is untested.
130
+ - E2E via Maestro, installed separately.
131
+
132
+ ## Accessibility
133
+
134
+ `accessibilityRole`, `accessibilityLabel`, and `accessibilityState` on
135
+ anything interactive. Test with VoiceOver and TalkBack. Respect the user's
136
+ font scale — a layout that breaks at large text is broken.
137
+
138
+ ## Tooling
139
+
140
+ Biome for lint and format. TypeScript strict with
141
+ `noUncheckedIndexedAccess`.
@@ -0,0 +1,12 @@
1
+ Scaffold a new iOS design system component.
2
+
3
+ Usage: `/ios:component {Name}`
4
+
5
+ 1. Load the `design-component` skill and follow it.
6
+ 2. Confirm every value has a token. Missing one? Add it to `Tokens.swift`
7
+ or the asset catalog first — never hardcode.
8
+ 3. Create `Sources/DesignSystem/{Name}/` and the matching test file.
9
+ 4. If the project needs a new target or setting, edit `project.yml` and run
10
+ `xcodegen generate`. Never touch the `.pbxproj`.
11
+ 5. Run the build and test via `xcodebuild`.
12
+ 6. Report the a11y checklist with each item checked or flagged.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: design-component
3
+ description: "Author a SwiftUI component in the iOS design system — folder layout, tokens, snapshot and behavior tests, accessibility. Use for work under Sources/DesignSystem/."
4
+ ---
5
+
6
+ # Authoring an iOS design system component
7
+
8
+ ## Folder
9
+
10
+ ```
11
+ Sources/DesignSystem/{Name}/
12
+ ├── {Name}.swift
13
+ └── {Name}Style.swift # if it has variants
14
+ Tests/DesignSystemTests/
15
+ └── {Name}Tests.swift
16
+ ```
17
+
18
+ ## Before writing
19
+
20
+ 1. Does a token exist for every value you need? If not, add it to
21
+ `Tokens.swift` or the asset catalog first. Never hardcode a color,
22
+ radius, spacing, or duration.
23
+ 2. Does SwiftUI already provide this? Do not reimplement `Button` to change
24
+ its background — write a `ButtonStyle`.
25
+ 3. Does this view need to know anything about the product? If yes, it does
26
+ not belong in `DesignSystem/`.
27
+
28
+ ## API
29
+
30
+ - Public initializers with sensible parameter names.
31
+ - Variants as an enum, not a set of booleans that can contradict.
32
+ - Accept a `@ViewBuilder` closure for content rather than a fixed `String`,
33
+ where a caller might reasonably want more.
34
+ - Nothing `public` that does not need to be.
35
+
36
+ ## Tests
37
+
38
+ Behavior first, snapshot second.
39
+
40
+ - Snapshot in light, dark, and the largest Dynamic Type size.
41
+ - Assert behavior separately — an action fires, a disabled control does not.
42
+ - If the component wraps a dependency, test *our* wrapper so a version bump
43
+ that changes behavior turns the suite red.
44
+
45
+ ## Accessibility checklist
46
+
47
+ - [ ] accessibility label present and meaningful
48
+ - [ ] correct traits (`.isButton`, `.isHeader`, …)
49
+ - [ ] value exposed where one applies (sliders, toggles)
50
+ - [ ] VoiceOver order is sensible, not just complete
51
+ - [ ] layout survives the largest Dynamic Type size
52
+ - [ ] touch target at least 44x44pt
53
+ - [ ] contrast holds in light and dark
54
+
55
+ ## Done means
56
+
57
+ View, style, tests in all three configurations, and the a11y checklist —
58
+ each item explicitly checked or flagged.