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.
- package/LICENSE +21 -0
- package/README.md +184 -2
- package/bin/build-template.js +208 -0
- package/bin/index.js +1139 -0
- package/package.json +41 -4
- package/template/.claude-plugin/plugin.json +10 -0
- package/template/.orchestra/README.md +401 -0
- package/template/.orchestra/blueprints/README.md +87 -0
- package/template/.orchestra/blueprints/api-only.md +46 -0
- package/template/.orchestra/blueprints/component-crud-resource.md +25 -0
- package/template/.orchestra/blueprints/saas-starter.md +69 -0
- package/template/.orchestra/config.yml +277 -0
- package/template/.orchestra/roles/orchestrator.md +105 -0
- package/template/.orchestra/roles/product-manager.md +438 -0
- package/template/.orchestra/scripts/compact-diff.awk +45 -0
- package/template/.orchestra/stacks/README.md +81 -0
- package/template/.orchestra/stacks/_lib/common.sh +3095 -0
- package/template/.orchestra/stacks/_repo/setup.sh +1109 -0
- package/template/.orchestra/stacks/backend-hono/claude/commands/endpoint.md +13 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/api-endpoint/SKILL.md +111 -0
- package/template/.orchestra/stacks/backend-hono/claude/skills/testing/SKILL.md +75 -0
- package/template/.orchestra/stacks/backend-hono/setup.sh +5945 -0
- package/template/.orchestra/stacks/backend-hono/stack.md +195 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/commands/component.md +13 -0
- package/template/.orchestra/stacks/mobile-android-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-android-native/setup.sh +607 -0
- package/template/.orchestra/stacks/mobile-android-native/stack.md +138 -0
- package/template/.orchestra/stacks/mobile-expo/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-expo/claude/skills/design-component/SKILL.md +92 -0
- package/template/.orchestra/stacks/mobile-expo/setup.sh +470 -0
- package/template/.orchestra/stacks/mobile-expo/stack.md +141 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/mobile-ios-native/claude/skills/design-component/SKILL.md +58 -0
- package/template/.orchestra/stacks/mobile-ios-native/setup.sh +468 -0
- package/template/.orchestra/stacks/mobile-ios-native/stack.md +130 -0
- package/template/.orchestra/stacks/web-nextjs/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-nextjs/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-nextjs/setup.sh +128 -0
- package/template/.orchestra/stacks/web-nextjs/stack.md +170 -0
- package/template/.orchestra/stacks/web-vite-react/claude/commands/component.md +12 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-component/SKILL.md +100 -0
- package/template/.orchestra/stacks/web-vite-react/claude/skills/design-tokens/SKILL.md +61 -0
- package/template/.orchestra/stacks/web-vite-react/setup.sh +185 -0
- package/template/.orchestra/stacks/web-vite-react/stack.md +177 -0
- package/template/.orchestra/trackers/github.sh +467 -0
- package/template/CLAUDE.md +54 -0
- package/template/agents/architect.md +83 -0
- package/template/agents/engineer-high.md +17 -0
- package/template/agents/engineer-low.md +17 -0
- package/template/agents/engineer-max.md +17 -0
- package/template/agents/engineer-medium.md +17 -0
- package/template/agents/engineer-xhigh.md +17 -0
- package/template/agents/lead.md +870 -0
- package/template/agents/milestone-reviewer.md +63 -0
- package/template/agents/orchestrator.md +105 -0
- package/template/agents/product-manager.md +438 -0
- package/template/agents/reviewer.md +301 -0
- package/template/agents/scaffolder.md +180 -0
- package/template/agents/tracker.md +202 -0
- package/template/commands/blueprint.md +23 -0
- package/template/commands/create-role.md +64 -0
- package/template/commands/export.md +45 -0
- package/template/commands/help.md +55 -0
- package/template/commands/hotfix.md +12 -0
- package/template/commands/orchestrator.md +12 -0
- package/template/commands/pm.md +18 -0
- package/template/commands/rewind.md +61 -0
- package/template/commands/setup.md +14 -0
- package/template/commands/start.md +29 -0
- package/template/commands/status.md +11 -0
- package/template/commands/sync.md +73 -0
- package/template/commands/verifier.md +53 -0
- package/template/hooks/audit-context.sh +153 -0
- package/template/hooks/enforce-role.sh +175 -0
- package/template/hooks/protect-system-files.sh +123 -0
- package/template/hooks/review-rounds.sh +172 -0
- package/template/rules/acceptance-check.orchestra.md +11 -0
- package/template/rules/analytics-kpi.orchestra.md +56 -0
- package/template/rules/code-standards.orchestra.md +34 -0
- package/template/rules/comments.orchestra.md +77 -0
- package/template/rules/commit-format.orchestra.md +20 -0
- package/template/rules/dependency-upgrade.orchestra.md +112 -0
- package/template/rules/design-parity.orchestra.md +170 -0
- package/template/rules/observability.orchestra.md +118 -0
- package/template/rules/performance.orchestra.md +186 -0
- package/template/rules/phase-limits.orchestra.md +3 -0
- package/template/rules/pull-request.orchestra.md +213 -0
- package/template/rules/role-boundaries.orchestra.md +117 -0
- package/template/rules/security.orchestra.md +106 -0
- package/template/rules/stuck-detection.orchestra.md +25 -0
- package/template/rules/testing-standards.orchestra.md +214 -0
- package/template/rules/tracker-authority.orchestra.md +190 -0
- package/template/rules/verification-gate.orchestra.md +56 -0
- package/template/skills/accessibility/SKILL.md +31 -0
- package/template/skills/android-testing/SKILL.md +112 -0
- package/template/skills/architecture/SKILL.md +175 -0
- package/template/skills/auth-setup/SKILL.md +105 -0
- package/template/skills/best-practices/SKILL.md +29 -0
- package/template/skills/code-optimizer/SKILL.md +27 -0
- package/template/skills/core-web-vitals/SKILL.md +32 -0
- package/template/skills/crud-api/SKILL.md +32 -0
- package/template/skills/debug/SKILL.md +28 -0
- package/template/skills/deployment/SKILL.md +104 -0
- package/template/skills/error-handling/SKILL.md +154 -0
- package/template/skills/frontend-design/SKILL.md +30 -0
- package/template/skills/fullstack-infrastructure/SKILL.md +108 -0
- package/template/skills/ios-testing/SKILL.md +100 -0
- package/template/skills/mobile-testing/SKILL.md +102 -0
- package/template/skills/observability/SKILL.md +91 -0
- package/template/skills/project-structure/SKILL.md +114 -0
- package/template/skills/push-notifications/SKILL.md +146 -0
- package/template/skills/react-best-practices/SKILL.md +31 -0
- package/template/skills/realtime/SKILL.md +146 -0
- package/template/skills/review/SKILL.md +82 -0
- package/template/skills/testing/SKILL.md +44 -0
- package/template/skills/web-quality-audit/SKILL.md +47 -0
- package/template/skills/web-testing/SKILL.md +120 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-testing
|
|
3
|
+
description: "Testing strategy for React Native and Expo apps. Use for any phase that writes or changes tests in a mobile app, or that adds an e2e flow."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Mobile Testing
|
|
7
|
+
|
|
8
|
+
## The fact everything else follows from
|
|
9
|
+
|
|
10
|
+
**Jest does not run React Native.** The `jest-expo` preset runs components in
|
|
11
|
+
Node against mocked native modules.
|
|
12
|
+
|
|
13
|
+
On a backend, an integration test exercises the real thing. Here, the
|
|
14
|
+
equivalent test exercises a simulation. The whole JS suite can be green while
|
|
15
|
+
the app fails to build, crashes on launch, or works on iOS and not Android.
|
|
16
|
+
|
|
17
|
+
So the device layer is not the optional top of a pyramid. It is where the
|
|
18
|
+
risk actually lives.
|
|
19
|
+
|
|
20
|
+
## The layers
|
|
21
|
+
|
|
22
|
+
| Layer | What belongs here | Runs against |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Pure logic | Calculations, conversions, date and streak logic, reducers — extracted **out** of components into plain modules | Node |
|
|
25
|
+
| Components (RNTL) | Render, interact, assert behaviour | Node, **mocked natives** |
|
|
26
|
+
| API seam (MSW) | Our own backend, faked at the network layer | Node |
|
|
27
|
+
| E2E (Maestro) | 5–10 flows, no more | **Real simulator/emulator** |
|
|
28
|
+
| The build | `expo-doctor`, `expo install --check`, a native build | CI |
|
|
29
|
+
|
|
30
|
+
## Where mobile bugs actually come from
|
|
31
|
+
|
|
32
|
+
| Failure | Caught by |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Does not build, or crashes on launch | the build, then e2e |
|
|
35
|
+
| Native module version off the SDK | `expo install --check` |
|
|
36
|
+
| Fine on iOS, broken on Android | e2e on both |
|
|
37
|
+
| Permission denied, backgrounded mid-request | e2e |
|
|
38
|
+
| Offline or flaky network | MSW |
|
|
39
|
+
| Stored data breaks on app upgrade | a dedicated migration test |
|
|
40
|
+
| Small screen, large accessibility font | e2e or manual |
|
|
41
|
+
| A notification that never arrives | nothing automated — a device |
|
|
42
|
+
|
|
43
|
+
Very little of that column says "component test". Weight the effort
|
|
44
|
+
accordingly.
|
|
45
|
+
|
|
46
|
+
The last row is the honest one: permission, token refresh, the background
|
|
47
|
+
handler and a deep link from a terminated app are all mocked into success by
|
|
48
|
+
`jest-expo`. Depth in `.claude/skills/push-notifications/SKILL.md`.
|
|
49
|
+
|
|
50
|
+
## Query by role, never by testID
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
screen.getByRole("button", { name: "Add water" }); // yes
|
|
54
|
+
screen.getByTestId("add-water-btn"); // no
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
RNTL resolves roles from `accessibilityRole` and the accessible name. Query
|
|
58
|
+
that way and **a missing accessibility prop becomes a failing test** —
|
|
59
|
+
accessibility stops being a review checklist item and becomes enforced by the
|
|
60
|
+
suite. A `testID` proves nothing about whether a screen reader can use the
|
|
61
|
+
screen.
|
|
62
|
+
|
|
63
|
+
## Extract the logic out of the component
|
|
64
|
+
|
|
65
|
+
A component test that exercises clamping, rounding or formatting is testing
|
|
66
|
+
a pure function through three layers of render. Move the function to a plain
|
|
67
|
+
module: it gets faster, it can be mutation-tested, and the component test
|
|
68
|
+
shrinks to "renders what the module returned".
|
|
69
|
+
|
|
70
|
+
## E2E — Maestro, not Detox
|
|
71
|
+
|
|
72
|
+
Maestro is YAML flows and needs no native build knowledge. Detox is more
|
|
73
|
+
capable and a great deal more setup; not worth it below a large team. Maestro
|
|
74
|
+
is a CLI, installed by its own installer — not an npm package.
|
|
75
|
+
|
|
76
|
+
Keep the suite small and end-to-end in the real sense:
|
|
77
|
+
|
|
78
|
+
1. launch → sign in
|
|
79
|
+
2. the core action of the app, once
|
|
80
|
+
3. the screen reflects it
|
|
81
|
+
4. kill and relaunch — the data is still there
|
|
82
|
+
|
|
83
|
+
Step 4 catches storage and migration bugs that nothing else sees.
|
|
84
|
+
|
|
85
|
+
## Never
|
|
86
|
+
|
|
87
|
+
- Snapshot tests. Huge, meaningless, and "updated" reflexively. A snapshot
|
|
88
|
+
passes while the handler stops firing.
|
|
89
|
+
- Testing that React Navigation navigates. Assert that pressing the control
|
|
90
|
+
calls navigate with the right target; the library's own behaviour is not
|
|
91
|
+
ours to test.
|
|
92
|
+
- Chasing coverage through screen composition files. The value sits in the
|
|
93
|
+
logic modules and the e2e flows.
|
|
94
|
+
|
|
95
|
+
## Release risk splits in two
|
|
96
|
+
|
|
97
|
+
- **JS-only change** — ships over the air (EAS Update). The JS suite really
|
|
98
|
+
is the gate, and a bad one rolls back in minutes.
|
|
99
|
+
- **Native change** — SDK, a plugin, a native dependency. Needs a build and a
|
|
100
|
+
device test, and it **cannot be rolled back** once it is in a store.
|
|
101
|
+
|
|
102
|
+
Treat the second kind as a different category of change, not a bigger one.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: observability
|
|
3
|
+
description: "Wire error monitoring, structured logging, and analytics into an app. Use when setting up a new app, before first release, or when instrumenting a milestone's KPIs."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Wiring observability
|
|
7
|
+
|
|
8
|
+
Mandatory before first release. See
|
|
9
|
+
`.claude/rules/observability.orchestra.md` and
|
|
10
|
+
`.claude/rules/analytics-kpi.orchestra.md` for the rules this implements.
|
|
11
|
+
|
|
12
|
+
## Order
|
|
13
|
+
|
|
14
|
+
1. **Error monitoring first.** It is the thing you need most on the day
|
|
15
|
+
something breaks, and the thing hardest to add under pressure.
|
|
16
|
+
2. **Structured logging** — server side.
|
|
17
|
+
3. **Analytics** — once the KPIs are known. Instrumenting before you know
|
|
18
|
+
what you are measuring produces events nobody reads.
|
|
19
|
+
|
|
20
|
+
## Error monitoring checklist
|
|
21
|
+
|
|
22
|
+
- [ ] SDK initialised as early as the app's lifecycle allows
|
|
23
|
+
- [ ] release tagged with version **and** commit sha
|
|
24
|
+
- [ ] symbols uploaded in CI — source maps, dSYMs, mapping files
|
|
25
|
+
- [ ] environment set, so staging noise does not drown production signal
|
|
26
|
+
- [ ] PII scrubbing configured **before** the first event is sent
|
|
27
|
+
- [ ] a deliberate test error verified end to end, in the dashboard
|
|
28
|
+
- [ ] alerting routed somewhere a human actually reads
|
|
29
|
+
|
|
30
|
+
An error monitor nobody has seen an event from is not wired. Prove it.
|
|
31
|
+
|
|
32
|
+
## Logging checklist
|
|
33
|
+
|
|
34
|
+
- [ ] JSON output, one event per line
|
|
35
|
+
- [ ] correlation id per request, on every line for that request
|
|
36
|
+
- [ ] levels used honestly — `error` means someone should look
|
|
37
|
+
- [ ] no secrets, no PII, no full request bodies
|
|
38
|
+
- [ ] `debug` off in production
|
|
39
|
+
|
|
40
|
+
## Health checklist
|
|
41
|
+
|
|
42
|
+
Two endpoints, because they answer different questions. `/health` says the
|
|
43
|
+
process is up; `/ready` says it can serve. The platform healthcheck points at
|
|
44
|
+
readiness, so this is the thing that decides whether a release is promoted.
|
|
45
|
+
|
|
46
|
+
- [ ] `/health` cheap, no dependencies
|
|
47
|
+
- [ ] `/ready` probes every dependency the service cannot serve without
|
|
48
|
+
- [ ] it answers 503 and **names** the dependency that failed
|
|
49
|
+
- [ ] a test makes it fail — a readiness check that cannot fail is worse
|
|
50
|
+
than none
|
|
51
|
+
- [ ] a new dependency adds its probe and its failing test in the same change
|
|
52
|
+
- [ ] the platform's `healthcheckPath` points at `/ready`
|
|
53
|
+
|
|
54
|
+
## Feature flag checklist
|
|
55
|
+
|
|
56
|
+
A flag exists to protect the service, so the lookup must never be able to
|
|
57
|
+
take it down.
|
|
58
|
+
|
|
59
|
+
- [ ] one typed registry — no string literal at a call site
|
|
60
|
+
- [ ] every lookup bounded by a timeout
|
|
61
|
+
- [ ] a failed lookup is reported and answered with the flag's fallback,
|
|
62
|
+
never thrown into a handler
|
|
63
|
+
- [ ] a kill switch falls back to **false**: if we cannot ask whether the
|
|
64
|
+
risky path is allowed, the answer is no
|
|
65
|
+
|
|
66
|
+
## Analytics checklist
|
|
67
|
+
|
|
68
|
+
- [ ] event names and payloads in one typed schema module per app
|
|
69
|
+
- [ ] call sites import from the schema — no string literals
|
|
70
|
+
- [ ] naming convention picked once: `object_action`
|
|
71
|
+
- [ ] view event per screen or route
|
|
72
|
+
- [ ] start, success, **and failure with a reason code** on critical flows
|
|
73
|
+
- [ ] no PII in properties
|
|
74
|
+
- [ ] consent gate respected before anything is sent
|
|
75
|
+
- [ ] tests assert the event fires once with the right shape
|
|
76
|
+
- [ ] events observed actually arriving before the milestone closes
|
|
77
|
+
|
|
78
|
+
## Common failures
|
|
79
|
+
|
|
80
|
+
**Instrumenting the wrapper, not the action.** An event fired three layers
|
|
81
|
+
from the user eventually fires for the wrong reason. Instrument where the
|
|
82
|
+
action happens.
|
|
83
|
+
|
|
84
|
+
**Only tracking success.** A funnel with no failure events tells you people
|
|
85
|
+
left but never why — which is the only thing you wanted to know.
|
|
86
|
+
|
|
87
|
+
**Swallowing the error you just reported.** Report and rethrow, or handle it
|
|
88
|
+
so the user gets a correct outcome. Hiding a crash is not handling it.
|
|
89
|
+
|
|
90
|
+
**Shipping without verifying.** Untested instrumentation is usually broken
|
|
91
|
+
instrumentation. Send one real event and look at it.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-structure
|
|
3
|
+
description: "Where a new file goes — the three questions that decide it, and why third-party wrappers have a fixed home. Use whenever creating a module, component, hook or utility rather than editing an existing one."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Project Structure
|
|
7
|
+
|
|
8
|
+
## The fact everything else follows from
|
|
9
|
+
|
|
10
|
+
**"Where does this go?" answered by judgement produces a different answer
|
|
11
|
+
every time.**
|
|
12
|
+
|
|
13
|
+
Nobody is wrong individually. One phase puts the Stripe call in the handler,
|
|
14
|
+
the next puts it in `utils/`, a third writes a `PaymentService`. Six
|
|
15
|
+
milestones later the same concern lives in four places, and no gate noticed —
|
|
16
|
+
because none of it is a defect, it is drift.
|
|
17
|
+
|
|
18
|
+
So the answer is a procedure, not a preference.
|
|
19
|
+
|
|
20
|
+
## Three questions, in order
|
|
21
|
+
|
|
22
|
+
Take the first one that is true.
|
|
23
|
+
|
|
24
|
+
| Question | Where | What follows |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **Does it call a third party's SDK or HTTP API?** | `src/integrations/<provider>/` | Coverage held at **100%** |
|
|
27
|
+
| **Does more than one feature use it?** | `src/lib/` | Needs a doc comment with an example |
|
|
28
|
+
| **Otherwise** | `src/features/<domain>/` | Nothing outside may import its internals |
|
|
29
|
+
|
|
30
|
+
Feature-first, including on the backend. A layer-first tree — `controllers/`,
|
|
31
|
+
`services/`, `models/` — spreads one change across four directories and gives
|
|
32
|
+
you no place to draw a boundary: `services/` has no owner, so nothing stops
|
|
33
|
+
one domain reaching into another. See
|
|
34
|
+
`.claude/skills/architecture/SKILL.md` on what makes a boundary real.
|
|
35
|
+
|
|
36
|
+
**Before creating anything, look in `src/lib/` and the feature you are in.**
|
|
37
|
+
The duplicate that costs is the one written by someone who did not know the
|
|
38
|
+
first existed.
|
|
39
|
+
|
|
40
|
+
## Why integrations has a fixed home
|
|
41
|
+
|
|
42
|
+
Two reasons, and neither of them is swappability.
|
|
43
|
+
|
|
44
|
+
**The test suite needs something to hold.** The rule is that a function
|
|
45
|
+
calling into a third-party library has a test, so that a dependency bump which
|
|
46
|
+
changes behaviour turns the suite red — that is what makes a green Dependabot
|
|
47
|
+
PR a merge signal. Called inline across a dozen handlers, there is no function
|
|
48
|
+
to test and the bump goes green.
|
|
49
|
+
|
|
50
|
+
**The policy is applied once.** Timeout, bounded retry, circuit breaker,
|
|
51
|
+
mapping the provider's errors into ours, keeping the key out of the logs.
|
|
52
|
+
Spread across a dozen call sites you will miss one, and the one you miss is
|
|
53
|
+
the one that hangs.
|
|
54
|
+
|
|
55
|
+
### It is a seam, not an abstraction
|
|
56
|
+
|
|
57
|
+
| | Seam wrapper | Abstraction wrapper |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Exists for | somewhere to test, one place for policy | pretending the vendor is interchangeable |
|
|
60
|
+
| Shape | thin; this app's types, this app's needs | provider-neutral, lowest common denominator |
|
|
61
|
+
| Justified by | the testing rule, today | a migration that may never happen |
|
|
62
|
+
|
|
63
|
+
A seam wrapper is allowed to look obviously like the provider it wraps. It
|
|
64
|
+
should. The abstraction version is what
|
|
65
|
+
`.claude/rules/code-standards.orchestra.md` forbids — one implementation, and
|
|
66
|
+
a guess about the future.
|
|
67
|
+
|
|
68
|
+
**Migration is the secondary benefit, and it is version upgrades, not vendor
|
|
69
|
+
swaps.** A major with a renamed method touches one file instead of twelve.
|
|
70
|
+
That happens several times a year; a vendor swap mostly never does.
|
|
71
|
+
|
|
72
|
+
### The 100% asks for exercise, not a test file
|
|
73
|
+
|
|
74
|
+
An integration test that drives the wrapper through a faked HTTP boundary
|
|
75
|
+
satisfies it completely. Do not add a unit test beside the wrapper to repeat
|
|
76
|
+
what a route test already covers — that is writing a test to move a number,
|
|
77
|
+
which the testing rule tells you to refuse rather than comply with.
|
|
78
|
+
|
|
79
|
+
Fake the provider at the **network** boundary, never by mocking the wrapper.
|
|
80
|
+
Mocking the wrapper means the wrapper is never tested, and the wrapper is the
|
|
81
|
+
thing that breaks when the provider changes.
|
|
82
|
+
|
|
83
|
+
## Components
|
|
84
|
+
|
|
85
|
+
Two levels, not four. Atoms-elements-molecules-organisms sounds precise and
|
|
86
|
+
is not: nobody places `FormField` the same way twice, and the folders drift
|
|
87
|
+
into an argument.
|
|
88
|
+
|
|
89
|
+
| Level | Contains | May import |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| **Primitives** — `src/design/` | Button, Input, Text. No business logic, no data fetching. | other primitives only |
|
|
92
|
+
| **Compositions** — beside the feature that uses them | Anything that knows what the app does | primitives, and their own feature |
|
|
93
|
+
|
|
94
|
+
The rule worth keeping from atomic design is the **import direction**: a
|
|
95
|
+
primitive never imports a composition. That one is checkable by reading a
|
|
96
|
+
diff. The taxonomy is not.
|
|
97
|
+
|
|
98
|
+
A component used by a second feature moves up to shared — on the second use,
|
|
99
|
+
not in anticipation of it.
|
|
100
|
+
|
|
101
|
+
## Types live beside what they describe
|
|
102
|
+
|
|
103
|
+
A global `types/` becomes a file everything imports, which is the opposite of
|
|
104
|
+
a boundary. A type that describes a feature's data lives in that feature; a
|
|
105
|
+
type crossing a boundary lives with the interface that defines it.
|
|
106
|
+
|
|
107
|
+
## Signs the structure has stopped working
|
|
108
|
+
|
|
109
|
+
- A change to one behaviour touches four directories.
|
|
110
|
+
- Two features import each other.
|
|
111
|
+
- `lib/` is the biggest directory in the app.
|
|
112
|
+
- The same provider's SDK is imported in more than one place.
|
|
113
|
+
- A file is named for a technical role — `helpers`, `managers`, `services` —
|
|
114
|
+
rather than for what it does.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: push-notifications
|
|
3
|
+
description: "Delivering push to iOS and Android — permission timing, token lifecycle, payload choice, deep links, and why none of it can be tested in a JS runner. Use for any phase that sends, receives, or routes a notification."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Push Notifications
|
|
7
|
+
|
|
8
|
+
## The fact everything else follows from
|
|
9
|
+
|
|
10
|
+
**Nothing in the JS test suite can tell you push works.**
|
|
11
|
+
|
|
12
|
+
`jest-expo` runs components in Node against mocked native modules, so the
|
|
13
|
+
permission call, the token callback, the background handler and the deep link
|
|
14
|
+
are all mocked into success. The suite is green and the notification never
|
|
15
|
+
arrives.
|
|
16
|
+
|
|
17
|
+
Push needs a build with your own credentials, on a real device, with the app
|
|
18
|
+
in each of three states — foreground, background, terminated. Anything less
|
|
19
|
+
tests the mock. This is the same reason the device layer is not optional in
|
|
20
|
+
`.claude/skills/mobile-testing/SKILL.md`.
|
|
21
|
+
|
|
22
|
+
Resolve the SDK's current API and constraints from its own docs at
|
|
23
|
+
implementation time. This area changes across SDK releases, and a handler
|
|
24
|
+
signature written from memory compiles and silently never fires.
|
|
25
|
+
|
|
26
|
+
## Permission is a product decision, not a setup step
|
|
27
|
+
|
|
28
|
+
The prompt can be shown **once**. A denial is close to permanent — the system
|
|
29
|
+
stops presenting the dialog, and the only route back is Settings.
|
|
30
|
+
|
|
31
|
+
| When you ask | Roughly what you get |
|
|
32
|
+
|---|---|
|
|
33
|
+
| On first launch, cold | 15–20% |
|
|
34
|
+
| After onboarding | 40–50% |
|
|
35
|
+
| At the moment the user turns on a feature that needs it | 70–80% |
|
|
36
|
+
|
|
37
|
+
So: explain the value, or better, ask at the moment the user does something
|
|
38
|
+
that obviously requires it. Never on launch.
|
|
39
|
+
|
|
40
|
+
- After a denial, do not re-prompt. Offer a route to Settings instead.
|
|
41
|
+
- iOS provisional authorisation delivers quietly with no prompt at all —
|
|
42
|
+
worth considering when the value is not yet obvious to the user.
|
|
43
|
+
- Android 13+ needs a runtime permission of its own. Treat it as a real
|
|
44
|
+
prompt with the same timing rules, not a manifest entry.
|
|
45
|
+
|
|
46
|
+
## The token is a lifecycle, not a value
|
|
47
|
+
|
|
48
|
+
The single most common defect: a token cached once and never refreshed, so
|
|
49
|
+
delivery quietly stops for users who reinstalled.
|
|
50
|
+
|
|
51
|
+
1. **Take it from the callback**, every launch. Never from storage as the
|
|
52
|
+
source of truth.
|
|
53
|
+
2. **Refresh replaces it.** The refresh callback is not optional — the token
|
|
54
|
+
changes on reinstall, restore, and at the platform's discretion.
|
|
55
|
+
3. **Key it by device, and associate a user.** One user has several devices;
|
|
56
|
+
one device may see several users. A token with no user cannot be targeted;
|
|
57
|
+
a token with no device id produces duplicates.
|
|
58
|
+
4. **Delete on rejection.** When the provider says the device is not
|
|
59
|
+
registered, that token is dead. Keeping it means a growing share of your
|
|
60
|
+
sends fail, and your delivery rate becomes a number nobody trusts.
|
|
61
|
+
5. **Treat it as opaque.** No parsing, no format assumptions.
|
|
62
|
+
|
|
63
|
+
A push token identifies a device belonging to a person. It never goes in a
|
|
64
|
+
log line, an error report, or an analytics property — see
|
|
65
|
+
`.claude/rules/security.orchestra.md`.
|
|
66
|
+
|
|
67
|
+
## Payload: visible or data-only
|
|
68
|
+
|
|
69
|
+
This is the choice that decides whether your handler runs at all.
|
|
70
|
+
|
|
71
|
+
| You want | Payload | Because |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| The user to see something now | **Visible** | Delivery is prioritised and the system displays it |
|
|
74
|
+
| To sync in the background | **Data-only** | A visible payload is handled by the system, not your code |
|
|
75
|
+
| Your own notification UI | **Data-only** | You build and post it |
|
|
76
|
+
|
|
77
|
+
Two constraints that bite:
|
|
78
|
+
|
|
79
|
+
- **Silent delivery is best-effort.** The OS may delay or drop it — battery,
|
|
80
|
+
Doze, Low Power Mode. Never carry something time-critical this way.
|
|
81
|
+
- **The background handler has seconds, not minutes.** Enqueue the work and
|
|
82
|
+
return. Doing the work inline gets the process killed mid-write.
|
|
83
|
+
|
|
84
|
+
On Android, a notification channel must exist before anything appears at all,
|
|
85
|
+
and a notification icon must be white-on-transparent — a coloured one renders
|
|
86
|
+
as a white square, which reads as a bug in your app.
|
|
87
|
+
|
|
88
|
+
## Foreground, background, terminated
|
|
89
|
+
|
|
90
|
+
Three states, three code paths, and they are handled in different places.
|
|
91
|
+
A notification that works in the foreground and does nothing from terminated
|
|
92
|
+
is the normal outcome of testing only one.
|
|
93
|
+
|
|
94
|
+
Deep linking is where this shows up first: opening from a terminated state
|
|
95
|
+
means the route is resolved **before** your navigation tree exists. Buffer
|
|
96
|
+
the target and apply it once navigation is ready, rather than dropping it.
|
|
97
|
+
|
|
98
|
+
Every one of the three gets tested by hand on a device before the phase
|
|
99
|
+
closes. There is no substitute.
|
|
100
|
+
|
|
101
|
+
## The send side
|
|
102
|
+
|
|
103
|
+
Sending is a third-party seam, so it has a test —
|
|
104
|
+
`.claude/rules/testing-standards.orchestra.md`. Fake the provider **at the
|
|
105
|
+
HTTP boundary**, never by mocking your own `sendPush()`, because the wrapper
|
|
106
|
+
is exactly what breaks when the provider changes.
|
|
107
|
+
|
|
108
|
+
Treat it like any other outbound call: timeout, bounded retry with jitter on
|
|
109
|
+
transient failures only, and a reported failure rather than a swallowed one.
|
|
110
|
+
See `.claude/skills/architecture/SKILL.md`.
|
|
111
|
+
|
|
112
|
+
- **A send is not a delivery.** Accepted by the provider means queued. Record
|
|
113
|
+
the two separately or your funnel is fiction.
|
|
114
|
+
- **Batch and rate-limit your own sends.** A fan-out that hits the provider
|
|
115
|
+
as fast as it can is throttled, and the throttling arrives as partial
|
|
116
|
+
failure.
|
|
117
|
+
- **Never send in a loop over users inside a request handler.** Queue it.
|
|
118
|
+
|
|
119
|
+
## Measuring it
|
|
120
|
+
|
|
121
|
+
Push is a feature with a funnel, so it is instrumented like one —
|
|
122
|
+
`.claude/rules/analytics-kpi.orchestra.md`:
|
|
123
|
+
|
|
124
|
+
- permission asked, granted, denied
|
|
125
|
+
- token registered, refreshed, rejected
|
|
126
|
+
- sent, delivered, opened
|
|
127
|
+
- and the **failure** at each step, with a reason code
|
|
128
|
+
|
|
129
|
+
Without the failure events you will know engagement dropped and never why.
|
|
130
|
+
No PII in any of those properties, and the token is never a property.
|
|
131
|
+
|
|
132
|
+
## Anti-patterns
|
|
133
|
+
|
|
134
|
+
- **Asking on first launch.** You spend the one prompt you get on a user who
|
|
135
|
+
does not yet know what the app does.
|
|
136
|
+
- **Re-prompting after denial.** The system ignores it; the user does not.
|
|
137
|
+
- **Caching the token long-term.** Delivery stops silently for reinstalls.
|
|
138
|
+
- **A visible payload when you needed the background handler.** The system
|
|
139
|
+
displays it and your code never runs.
|
|
140
|
+
- **Relying on silent push for anything time-critical.** Delivery is not
|
|
141
|
+
guaranteed and you will not be told.
|
|
142
|
+
- **Heavy work in the background handler.** The process is killed partway.
|
|
143
|
+
- **Skipping the notification channel.** Nothing appears at all on modern
|
|
144
|
+
Android, with no error anywhere.
|
|
145
|
+
- **Shipping without testing the terminated state.** It is the state most
|
|
146
|
+
users are actually in.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-best-practices
|
|
3
|
+
description: "React and Next.js implementation patterns — data fetching, boundaries, imports, state. Use for any phase writing React components."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: React Best Practices
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Phase involves React or Next.js frontend development.
|
|
10
|
+
|
|
11
|
+
## Checklist
|
|
12
|
+
- [ ] **No waterfall fetches:** use `Promise.all` for parallel data, `Suspense` boundaries for streaming
|
|
13
|
+
- [ ] **Direct imports:** never import from barrel files (`index.ts`) — import from exact module path
|
|
14
|
+
- [ ] **Server components by default:** only add `"use client"` when state/effects/browser APIs needed
|
|
15
|
+
- [ ] **Lazy load heavy components:** `next/dynamic` or `React.lazy` for below-the-fold content
|
|
16
|
+
- [ ] **Memoize expensive renders:** `React.memo` for components with stable props, `useMemo` for expensive computations
|
|
17
|
+
- [ ] **Primitive dependencies in hooks:** avoid objects/arrays in `useEffect` deps — extract primitive values
|
|
18
|
+
- [ ] **No duplicate serialization:** don't pass same data as both prop and context
|
|
19
|
+
- [ ] **Parallel route fetching:** use `Promise.all` in `loader`/`getServerSideProps`, not sequential `await`
|
|
20
|
+
- [ ] **SWR/React Query for client data:** deduplication, caching, revalidation built-in
|
|
21
|
+
- [ ] **Passive event listeners:** `{ passive: true }` for scroll/touch handlers
|
|
22
|
+
- [ ] **Hoist static JSX:** constant JSX outside component body to avoid re-creation on render
|
|
23
|
+
- [ ] **Use `Map`/`Set` for lookups:** not `Array.find()` in render paths
|
|
24
|
+
|
|
25
|
+
## Common Mistakes
|
|
26
|
+
- Sequential `await` in data fetching → blocks waterfall, use `Promise.all`
|
|
27
|
+
- Barrel file imports → prevents tree-shaking, entire module loaded
|
|
28
|
+
- `"use client"` on everything → loses server component benefits (streaming, zero bundle)
|
|
29
|
+
- Complex objects in `useEffect` dependencies → infinite re-render loop
|
|
30
|
+
- Event listeners without cleanup → memory leak in `useEffect`
|
|
31
|
+
- Fetching in `useEffect` without SWR/React Query → no dedup, no cache, race conditions
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: realtime
|
|
3
|
+
description: "Pushing updates to a connected client — choosing between polling, SSE and WebSocket, authenticating the connection, surviving reconnects, and what changes when there is more than one instance. Use for any phase adding live updates, presence, or a subscription."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Realtime
|
|
7
|
+
|
|
8
|
+
## The fact everything else follows from
|
|
9
|
+
|
|
10
|
+
**A connection is state, and state is what makes a service hard to deploy.**
|
|
11
|
+
|
|
12
|
+
A stateless service scales by adding an instance and deploys by replacing
|
|
13
|
+
one. A connection breaks both: the client is pinned to an instance, a deploy
|
|
14
|
+
drops every connection at once, and an event published on instance A never
|
|
15
|
+
reaches a subscriber on instance B.
|
|
16
|
+
|
|
17
|
+
None of that is a reason to avoid realtime. It is a reason to be sure you
|
|
18
|
+
need it, and to decide the multi-instance answer **before** the second
|
|
19
|
+
instance exists — retrofitting it is a rewrite.
|
|
20
|
+
|
|
21
|
+
## Choose the cheapest thing that works
|
|
22
|
+
|
|
23
|
+
Work down. Take the first row that is genuinely true.
|
|
24
|
+
|
|
25
|
+
| If | Use | Why |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Updates are seconds-stale-tolerant, or rare | **Polling** | No connection state, no deploy problem, works through every proxy |
|
|
28
|
+
| Server → client only: feeds, progress, notifications, streamed output | **SSE** | Plain HTTP, reconnects and resumes by itself, no protocol to operate |
|
|
29
|
+
| Genuinely bidirectional and low latency: collaboration, presence, games | **WebSocket** | Only case that pays for the state |
|
|
30
|
+
|
|
31
|
+
Most "realtime" requirements are SSE requirements. A chat that only receives
|
|
32
|
+
is server-to-client. Reaching for WebSocket first buys you connection
|
|
33
|
+
management you did not need.
|
|
34
|
+
|
|
35
|
+
**SSE's resume is the reason to prefer it.** The browser reconnects on its
|
|
36
|
+
own and sends the last event id it saw; give every event an id and a client
|
|
37
|
+
that missed a minute can be caught up. With WebSocket you write that
|
|
38
|
+
yourself, and most implementations do not.
|
|
39
|
+
|
|
40
|
+
## Authenticate the handshake
|
|
41
|
+
|
|
42
|
+
The upgrade request is the only place authorisation happens. After it, you
|
|
43
|
+
have a socket with no request cycle to check anything against.
|
|
44
|
+
|
|
45
|
+
- Authenticate **during** the handshake, and reject before the upgrade
|
|
46
|
+
completes. Never accept first and check on the first message.
|
|
47
|
+
- **The browser WebSocket API cannot set headers.** Pass a short-lived,
|
|
48
|
+
single-use ticket obtained over normal HTTP — never the session token in
|
|
49
|
+
the query string, which lands in every access log.
|
|
50
|
+
- **A token that expires mid-connection matters.** Decide now: close and let
|
|
51
|
+
the client reconnect, or re-authenticate in-band. Both are fine. Neither
|
|
52
|
+
happening is a connection authorised by a credential that expired hours ago.
|
|
53
|
+
- Authorise **per subscription**, not once at connect. Joining a room is an
|
|
54
|
+
authorisation decision, and it is the one most often forgotten.
|
|
55
|
+
|
|
56
|
+
## Assume every connection dies
|
|
57
|
+
|
|
58
|
+
Because they do — a tunnel closes, a phone changes network, a proxy times out
|
|
59
|
+
an idle socket, or you deployed.
|
|
60
|
+
|
|
61
|
+
- **Heartbeat both ways.** A TCP connection can be dead for minutes while
|
|
62
|
+
both sides believe it is open. Ping, expect a pong, close when it does not
|
|
63
|
+
arrive.
|
|
64
|
+
- **The client reconnects with exponential backoff and jitter.** Without
|
|
65
|
+
jitter, a deploy disconnects everyone at once and they all return at once —
|
|
66
|
+
you knock over the instance that just came up.
|
|
67
|
+
- **Reconnect is not resume.** State the recovery: replay from the last event
|
|
68
|
+
id, or refetch and replace. "The client will have missed some messages" is
|
|
69
|
+
a bug that reads as flakiness.
|
|
70
|
+
- **Cap the retries, then say so.** A client that reconnects forever in the
|
|
71
|
+
background is a battery complaint. Surface a disconnected state and let the
|
|
72
|
+
user retry.
|
|
73
|
+
|
|
74
|
+
## Bound everything
|
|
75
|
+
|
|
76
|
+
An open socket is an invitation, and none of these have a safe default.
|
|
77
|
+
|
|
78
|
+
- Maximum message size, enforced on receive.
|
|
79
|
+
- Maximum messages per second, per connection — rate limiting at the socket,
|
|
80
|
+
not just at the HTTP edge.
|
|
81
|
+
- Maximum connections per user, and per instance.
|
|
82
|
+
- Maximum subscriptions per connection.
|
|
83
|
+
- A send queue that **drops or disconnects** when the client stops reading,
|
|
84
|
+
rather than buffering until the process runs out of memory. This is the
|
|
85
|
+
failure that takes the service down, and it looks like a memory leak.
|
|
86
|
+
|
|
87
|
+
Never do synchronous work per message on the event loop. One expensive
|
|
88
|
+
handler stalls every other connection on that instance.
|
|
89
|
+
|
|
90
|
+
## More than one instance
|
|
91
|
+
|
|
92
|
+
The moment there are two, an event published on one has to reach subscribers
|
|
93
|
+
on the other. Pick the mechanism deliberately:
|
|
94
|
+
|
|
95
|
+
- **A pub/sub backplane** — every instance subscribes, every instance
|
|
96
|
+
forwards to its own connections. Simple, and the usual answer.
|
|
97
|
+
- **A dedicated realtime service** — worth it when the connection count and
|
|
98
|
+
the request load have genuinely different shapes.
|
|
99
|
+
|
|
100
|
+
Either way the backplane is now a dependency the service cannot serve
|
|
101
|
+
without, so **it gets a readiness probe** — see
|
|
102
|
+
`.claude/rules/observability.orchestra.md`. A service accepting connections
|
|
103
|
+
it can never deliver to is worse than one refusing them.
|
|
104
|
+
|
|
105
|
+
Sticky sessions are a workaround, not a design. They break on deploy, they
|
|
106
|
+
unbalance load, and they hide the problem until the day you scale.
|
|
107
|
+
|
|
108
|
+
## Deploys disconnect everybody
|
|
109
|
+
|
|
110
|
+
This is normal and it is survivable, if it is designed:
|
|
111
|
+
|
|
112
|
+
- Close connections **gracefully** on SIGTERM — send a close frame so clients
|
|
113
|
+
reconnect on purpose instead of timing out. The draining window exists for
|
|
114
|
+
this; see `.claude/skills/deployment/SKILL.md`.
|
|
115
|
+
- Client backoff with jitter spreads the return.
|
|
116
|
+
- The reconnect path is the same one used after a network drop, so it is
|
|
117
|
+
already tested.
|
|
118
|
+
|
|
119
|
+
## Testing it
|
|
120
|
+
|
|
121
|
+
- The **protocol** is a third-party seam: connect, authenticate, subscribe,
|
|
122
|
+
receive, reconnect — tested against a real server, not a mocked client.
|
|
123
|
+
`.claude/rules/testing-standards.orchestra.md`.
|
|
124
|
+
- The **failure paths** are the point: rejected handshake, expired
|
|
125
|
+
credential, dropped connection mid-stream, a subscription the user is not
|
|
126
|
+
allowed to make.
|
|
127
|
+
- **Never `sleep` and assert.** Wait for the event; a sleep is a flake with a
|
|
128
|
+
delay.
|
|
129
|
+
- Instrument connect, disconnect with reason, reconnect, and messages
|
|
130
|
+
dropped. A realtime feature with no disconnect-reason metric cannot be
|
|
131
|
+
debugged from the outside.
|
|
132
|
+
|
|
133
|
+
## Anti-patterns
|
|
134
|
+
|
|
135
|
+
- **WebSocket where SSE would do.** You now operate a protocol for a feature
|
|
136
|
+
that only pushes.
|
|
137
|
+
- **Authenticating after the upgrade.** The connection existed before anyone
|
|
138
|
+
checked.
|
|
139
|
+
- **The session token in the query string.** It is in the access log of every
|
|
140
|
+
hop.
|
|
141
|
+
- **No heartbeat.** Dead connections accumulate and are counted as users.
|
|
142
|
+
- **Unbounded server-side buffering.** Reads as a memory leak, is actually a
|
|
143
|
+
slow client.
|
|
144
|
+
- **Broadcasting to everyone and filtering on the client.** Every client
|
|
145
|
+
receives data it is not allowed to see; the filter is decoration.
|
|
146
|
+
- **Assuming one instance.** Works perfectly until the day it matters.
|