oh-my-agent-commandcode 0.0.0-stage → 20.38.3

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 (334) hide show
  1. package/README.md +35 -2
  2. package/filter-test-output.sh +52 -0
  3. package/mods/oma.ts +347 -0
  4. package/package.json +30 -4
  5. package/skills/_shared/conditional/experiment-ledger.md +28 -0
  6. package/skills/_shared/conditional/exploration-loop.md +22 -0
  7. package/skills/_shared/conditional/quality-score.md +33 -0
  8. package/skills/_shared/core/anti-ai-prose.md +191 -0
  9. package/skills/_shared/core/api-contracts/README.md +28 -0
  10. package/skills/_shared/core/api-contracts/template.md +90 -0
  11. package/skills/_shared/core/clarification-protocol.md +23 -0
  12. package/skills/_shared/core/code-intelligence.md +46 -0
  13. package/skills/_shared/core/collaboration-policy.md +28 -0
  14. package/skills/_shared/core/common-checklist.md +28 -0
  15. package/skills/_shared/core/context-budget.md +22 -0
  16. package/skills/_shared/core/context-loading.md +34 -0
  17. package/skills/_shared/core/difficulty-guide.md +26 -0
  18. package/skills/_shared/core/execution-policy.md +31 -0
  19. package/skills/_shared/core/lessons-learned.md +33 -0
  20. package/skills/_shared/core/prompt-structure.md +18 -0
  21. package/skills/_shared/core/quality-principles.md +11 -0
  22. package/skills/_shared/core/session-metrics.md +35 -0
  23. package/skills/_shared/core/skill-routing.md +39 -0
  24. package/skills/_shared/core/test-approach.md +54 -0
  25. package/skills/_shared/core/vendor-detection.md +52 -0
  26. package/skills/_shared/runtime/memory-protocol.md +118 -0
  27. package/skills/_shared/runtime/result-contract.md +64 -0
  28. package/skills/_shared/ui-interaction-quality.md +133 -0
  29. package/skills/oma-academic-writing/SKILL.md +181 -0
  30. package/skills/oma-academic-writing/resources/abstract-guide.md +51 -0
  31. package/skills/oma-academic-writing/resources/academic-verb-tiers.md +42 -0
  32. package/skills/oma-academic-writing/resources/anti-ai-checklist.md +52 -0
  33. package/skills/oma-academic-writing/resources/hedging-guide.md +132 -0
  34. package/skills/oma-academic-writing/resources/sentence-structure-reference.md +151 -0
  35. package/skills/oma-architecture/SKILL.md +203 -0
  36. package/skills/oma-architecture/resources/api-evolution.md +41 -0
  37. package/skills/oma-architecture/resources/checklist.md +42 -0
  38. package/skills/oma-architecture/resources/execution-protocol.md +115 -0
  39. package/skills/oma-architecture/resources/methodology-selection.md +92 -0
  40. package/skills/oma-architecture/resources/migration-patterns.md +28 -0
  41. package/skills/oma-architecture/resources/output-templates.md +111 -0
  42. package/skills/oma-architecture/resources/stakeholder-synthesis.md +80 -0
  43. package/skills/oma-backend/SKILL.md +202 -0
  44. package/skills/oma-backend/resources/checklist.md +45 -0
  45. package/skills/oma-backend/resources/error-playbook.md +97 -0
  46. package/skills/oma-backend/resources/execution-protocol.md +39 -0
  47. package/skills/oma-backend/resources/orm-reference.md +61 -0
  48. package/skills/oma-backend/variants/node/api-template.ts +315 -0
  49. package/skills/oma-backend/variants/node/snippets.md +475 -0
  50. package/skills/oma-backend/variants/node/stack.yaml +21 -0
  51. package/skills/oma-backend/variants/node/tech-stack.md +33 -0
  52. package/skills/oma-backend/variants/python/api-template.py +378 -0
  53. package/skills/oma-backend/variants/python/snippets.md +199 -0
  54. package/skills/oma-backend/variants/python/stack.yaml +18 -0
  55. package/skills/oma-backend/variants/python/tech-stack.md +45 -0
  56. package/skills/oma-backend/variants/rust/api-template.rs +635 -0
  57. package/skills/oma-backend/variants/rust/snippets.md +471 -0
  58. package/skills/oma-backend/variants/rust/stack.yaml +20 -0
  59. package/skills/oma-backend/variants/rust/tech-stack.md +36 -0
  60. package/skills/oma-backend/variants/stack.schema.json +73 -0
  61. package/skills/oma-brainstorm/SKILL.md +166 -0
  62. package/skills/oma-brainstorm/resources/triz-lite.md +117 -0
  63. package/skills/oma-coordination/SKILL.md +171 -0
  64. package/skills/oma-db/SKILL.md +201 -0
  65. package/skills/oma-db/resources/anti-patterns.md +177 -0
  66. package/skills/oma-db/resources/checklist.md +67 -0
  67. package/skills/oma-db/resources/document-templates.md +107 -0
  68. package/skills/oma-db/resources/error-playbook.md +50 -0
  69. package/skills/oma-db/resources/examples.md +170 -0
  70. package/skills/oma-db/resources/execution-protocol.md +127 -0
  71. package/skills/oma-db/resources/iso-controls.md +98 -0
  72. package/skills/oma-db/resources/migration-playbook.md +93 -0
  73. package/skills/oma-db/resources/query-tuning.md +57 -0
  74. package/skills/oma-db/resources/vector-db.md +159 -0
  75. package/skills/oma-debug/SKILL.md +146 -0
  76. package/skills/oma-debug/resources/bug-report-template.md +327 -0
  77. package/skills/oma-debug/resources/checklist.md +31 -0
  78. package/skills/oma-debug/resources/common-patterns.md +734 -0
  79. package/skills/oma-debug/resources/debugging-checklist.md +364 -0
  80. package/skills/oma-debug/resources/error-playbook.md +94 -0
  81. package/skills/oma-debug/resources/execution-protocol.md +40 -0
  82. package/skills/oma-design/SKILL.md +135 -0
  83. package/skills/oma-design/resources/asset-strategy.md +34 -0
  84. package/skills/oma-design/resources/checklist.md +63 -0
  85. package/skills/oma-design/resources/data-grid.md +48 -0
  86. package/skills/oma-design/resources/design-md-spec.md +107 -0
  87. package/skills/oma-design/resources/design-tokens.md +53 -0
  88. package/skills/oma-design/resources/error-playbook.md +18 -0
  89. package/skills/oma-design/resources/execution-protocol.md +95 -0
  90. package/skills/oma-design/resources/external-design-skills.md +130 -0
  91. package/skills/oma-design/resources/getdesign-fetcher.md +104 -0
  92. package/skills/oma-design/resources/product-interface-review.md +59 -0
  93. package/skills/oma-design/resources/redesign-protocol.md +52 -0
  94. package/skills/oma-design/resources/stitch-integration.md +244 -0
  95. package/skills/oma-dev-workflow/SKILL.md +375 -0
  96. package/skills/oma-dev-workflow/resources/affected-checks.py +58 -0
  97. package/skills/oma-dev-workflow/resources/api-workflows.md +58 -0
  98. package/skills/oma-dev-workflow/resources/database-patterns.md +107 -0
  99. package/skills/oma-dev-workflow/resources/i18n-patterns.md +63 -0
  100. package/skills/oma-dev-workflow/resources/release-coordination.md +240 -0
  101. package/skills/oma-dev-workflow/resources/tooling-profiles.md +217 -0
  102. package/skills/oma-dev-workflow/resources/troubleshooting.md +78 -0
  103. package/skills/oma-dev-workflow/resources/validation-pipeline.md +246 -0
  104. package/skills/oma-docs/SKILL.md +77 -0
  105. package/skills/oma-docs/resources/commands.md +76 -0
  106. package/skills/oma-explanation/SKILL.md +212 -0
  107. package/skills/oma-explanation/resources/document-structure.md +115 -0
  108. package/skills/oma-explanation/resources/draft-format.md +199 -0
  109. package/skills/oma-explanation/resources/html-contract.md +102 -0
  110. package/skills/oma-frontend/SKILL.md +227 -0
  111. package/skills/oma-frontend/resources/angular-rules.md +110 -0
  112. package/skills/oma-frontend/resources/checklist.md +76 -0
  113. package/skills/oma-frontend/resources/error-playbook.md +125 -0
  114. package/skills/oma-frontend/resources/execution-protocol.md +46 -0
  115. package/skills/oma-frontend/resources/snippets.md +354 -0
  116. package/skills/oma-frontend/resources/specialized-visual-implementation.md +193 -0
  117. package/skills/oma-frontend/resources/tailwind-rules.md +83 -0
  118. package/skills/oma-frontend/resources/tech-stack.md +156 -0
  119. package/skills/oma-hwp/SKILL.md +166 -0
  120. package/skills/oma-hwp/config/hwp-config.yaml +35 -0
  121. package/skills/oma-hwp/resources/balanced-tables.ts +39 -0
  122. package/skills/oma-hwp/resources/execution-protocol.md +145 -0
  123. package/skills/oma-hwp/resources/flatten-tables.ts +101 -0
  124. package/skills/oma-hwp/resources/package.json +13 -0
  125. package/skills/oma-hwp/resources/troubleshooting.md +70 -0
  126. package/skills/oma-hwp/resources/tsconfig.json +10 -0
  127. package/skills/oma-hwp/resources/turndown-plugin-gfm.d.ts +9 -0
  128. package/skills/oma-image/SKILL.md +142 -0
  129. package/skills/oma-image/config/image-config.yaml +70 -0
  130. package/skills/oma-image/resources/checklist.md +18 -0
  131. package/skills/oma-image/resources/execution-protocol.md +117 -0
  132. package/skills/oma-image/resources/invocation.md +105 -0
  133. package/skills/oma-image/resources/prompt-tips.md +71 -0
  134. package/skills/oma-image/resources/vendor-matrix.md +90 -0
  135. package/skills/oma-market/SKILL.md +142 -0
  136. package/skills/oma-market/resources/checklist.md +12 -0
  137. package/skills/oma-market/resources/error-playbook.md +12 -0
  138. package/skills/oma-market/resources/execution-protocol.md +60 -0
  139. package/skills/oma-market/resources/frameworks/pestel.md +101 -0
  140. package/skills/oma-market/resources/frameworks/porters-5f.md +90 -0
  141. package/skills/oma-market/resources/frameworks/swot.md +61 -0
  142. package/skills/oma-market/resources/intent-rules.md +35 -0
  143. package/skills/oma-market/resources/output-laws.md +53 -0
  144. package/skills/oma-mobile/SKILL.md +168 -0
  145. package/skills/oma-mobile/resources/checklist.md +77 -0
  146. package/skills/oma-mobile/resources/error-playbook.md +198 -0
  147. package/skills/oma-mobile/resources/execution-protocol.md +47 -0
  148. package/skills/oma-mobile/resources/screen-template.dart +331 -0
  149. package/skills/oma-mobile/resources/screen-template.swift +239 -0
  150. package/skills/oma-mobile/resources/screen-template.tsx +179 -0
  151. package/skills/oma-mobile/resources/tech-stack.md +69 -0
  152. package/skills/oma-mobile/variants/README.md +45 -0
  153. package/skills/oma-mobile/variants/flutter/api-template.dart +461 -0
  154. package/skills/oma-mobile/variants/flutter/snippets.md +1353 -0
  155. package/skills/oma-mobile/variants/flutter/stack.yaml +21 -0
  156. package/skills/oma-mobile/variants/flutter/tech-stack.md +201 -0
  157. package/skills/oma-mobile/variants/kotlin-android/api-template.kt +182 -0
  158. package/skills/oma-mobile/variants/kotlin-android/snippets.md +871 -0
  159. package/skills/oma-mobile/variants/kotlin-android/stack.yaml +32 -0
  160. package/skills/oma-mobile/variants/kotlin-android/tech-stack.md +159 -0
  161. package/skills/oma-mobile/variants/react-native/api-template.ts +531 -0
  162. package/skills/oma-mobile/variants/react-native/snippets.md +1014 -0
  163. package/skills/oma-mobile/variants/react-native/stack.yaml +22 -0
  164. package/skills/oma-mobile/variants/react-native/tech-stack.md +192 -0
  165. package/skills/oma-mobile/variants/stack.schema.json +68 -0
  166. package/skills/oma-mobile/variants/swift-ios/api-template.swift +165 -0
  167. package/skills/oma-mobile/variants/swift-ios/snippets.md +1028 -0
  168. package/skills/oma-mobile/variants/swift-ios/stack.yaml +25 -0
  169. package/skills/oma-mobile/variants/swift-ios/tech-stack.md +174 -0
  170. package/skills/oma-observability/SKILL.md +225 -0
  171. package/skills/oma-observability/resources/anti-patterns.md +555 -0
  172. package/skills/oma-observability/resources/boundaries/cross-application.md +348 -0
  173. package/skills/oma-observability/resources/boundaries/multi-tenant.md +290 -0
  174. package/skills/oma-observability/resources/boundaries/release.md +372 -0
  175. package/skills/oma-observability/resources/boundaries/slo.md +225 -0
  176. package/skills/oma-observability/resources/checklist.md +123 -0
  177. package/skills/oma-observability/resources/examples.md +448 -0
  178. package/skills/oma-observability/resources/execution-protocol.md +166 -0
  179. package/skills/oma-observability/resources/incident-forensics.md +348 -0
  180. package/skills/oma-observability/resources/intent-rules.md +307 -0
  181. package/skills/oma-observability/resources/layers/L3-network.md +281 -0
  182. package/skills/oma-observability/resources/layers/L4-transport.md +299 -0
  183. package/skills/oma-observability/resources/layers/L7-application/crash-analytics.md +359 -0
  184. package/skills/oma-observability/resources/layers/L7-application/mobile-rum.md +278 -0
  185. package/skills/oma-observability/resources/layers/L7-application/waf.md +246 -0
  186. package/skills/oma-observability/resources/layers/L7-application/web-rum.md +350 -0
  187. package/skills/oma-observability/resources/layers/mesh.md +368 -0
  188. package/skills/oma-observability/resources/matrix.md +177 -0
  189. package/skills/oma-observability/resources/meta-observability.md +605 -0
  190. package/skills/oma-observability/resources/observability-as-code.md +666 -0
  191. package/skills/oma-observability/resources/signals/audit.md +310 -0
  192. package/skills/oma-observability/resources/signals/cost.md +303 -0
  193. package/skills/oma-observability/resources/signals/logs.md +263 -0
  194. package/skills/oma-observability/resources/signals/metrics.md +478 -0
  195. package/skills/oma-observability/resources/signals/privacy.md +351 -0
  196. package/skills/oma-observability/resources/signals/profiles.md +245 -0
  197. package/skills/oma-observability/resources/signals/traces.md +360 -0
  198. package/skills/oma-observability/resources/standards.md +214 -0
  199. package/skills/oma-observability/resources/transport/collector-topology.md +308 -0
  200. package/skills/oma-observability/resources/transport/otlp-grpc-vs-http.md +256 -0
  201. package/skills/oma-observability/resources/transport/sampling-recipes.md +297 -0
  202. package/skills/oma-observability/resources/transport/udp-statsd-mtu.md +167 -0
  203. package/skills/oma-observability/resources/vendor-categories.md +352 -0
  204. package/skills/oma-orchestration/SKILL.md +302 -0
  205. package/skills/oma-orchestration/config/cli-config.yaml +92 -0
  206. package/skills/oma-orchestration/resources/memory-schema.md +232 -0
  207. package/skills/oma-orchestration/resources/subagent-prompt-template.md +30 -0
  208. package/skills/oma-orchestration/scripts/parallel-run.sh +6 -0
  209. package/skills/oma-orchestration/scripts/spawn-agent.sh +5 -0
  210. package/skills/oma-orchestration/scripts/verify.sh +20 -0
  211. package/skills/oma-orchestration/templates/backend-task.md +19 -0
  212. package/skills/oma-orchestration/templates/debug-task.md +16 -0
  213. package/skills/oma-orchestration/templates/frontend-task.md +18 -0
  214. package/skills/oma-orchestration/templates/mobile-task.md +18 -0
  215. package/skills/oma-orchestration/templates/qa-task.md +16 -0
  216. package/skills/oma-orchestration/templates/tasks-example.yaml +15 -0
  217. package/skills/oma-pdf/SKILL.md +162 -0
  218. package/skills/oma-pdf/config/pdf-config.yaml +27 -0
  219. package/skills/oma-pdf/resources/execution-protocol.md +127 -0
  220. package/skills/oma-pm/SKILL.md +157 -0
  221. package/skills/oma-pm/resources/error-playbook.md +75 -0
  222. package/skills/oma-pm/resources/examples.md +439 -0
  223. package/skills/oma-pm/resources/execution-protocol.md +62 -0
  224. package/skills/oma-pm/resources/iso-planning.md +110 -0
  225. package/skills/oma-pm/resources/plan-phase-protocol.md +66 -0
  226. package/skills/oma-pm/resources/task-template.json +86 -0
  227. package/skills/oma-qa/SKILL.md +141 -0
  228. package/skills/oma-qa/resources/checklist.md +339 -0
  229. package/skills/oma-qa/resources/error-playbook.md +98 -0
  230. package/skills/oma-qa/resources/execution-protocol.md +169 -0
  231. package/skills/oma-qa/resources/iso-quality.md +82 -0
  232. package/skills/oma-qa/resources/verify-ship-protocol.md +116 -0
  233. package/skills/oma-recap/SKILL.md +245 -0
  234. package/skills/oma-recap/resources/output-formats.md +83 -0
  235. package/skills/oma-refactor/SKILL.md +184 -0
  236. package/skills/oma-refactor/resources/definition.md +108 -0
  237. package/skills/oma-refactor/resources/governance.md +33 -0
  238. package/skills/oma-refactor/resources/hotspots.py +44 -0
  239. package/skills/oma-refactor/resources/measurement.md +93 -0
  240. package/skills/oma-scholar/SKILL.md +276 -0
  241. package/skills/oma-scholar/config/scholar-config.yaml +79 -0
  242. package/skills/oma-scholar/resources/api-endpoints.md +174 -0
  243. package/skills/oma-scholar/resources/checklist.md +92 -0
  244. package/skills/oma-scholar/resources/execution-protocol.md +224 -0
  245. package/skills/oma-scholar/resources/fallback-providers.md +143 -0
  246. package/skills/oma-scholar/resources/setup-openalex.md +85 -0
  247. package/skills/oma-scholar/resources/sidecar-spec.md +341 -0
  248. package/skills/oma-scholar/resources/upstream-spec-cache.md +972 -0
  249. package/skills/oma-scm/SKILL.md +103 -0
  250. package/skills/oma-scm/resources/cm-operations.md +56 -0
  251. package/skills/oma-scm/resources/codeowners-playbook.md +45 -0
  252. package/skills/oma-scm/resources/conventional-commits.md +108 -0
  253. package/skills/oma-scm/resources/merge-risk.md +76 -0
  254. package/skills/oma-scm/resources/onboarding-risk-signals.md +71 -0
  255. package/skills/oma-search/SKILL.md +125 -0
  256. package/skills/oma-search/resources/checklist.md +36 -0
  257. package/skills/oma-search/resources/error-playbook.md +66 -0
  258. package/skills/oma-search/resources/examples.md +80 -0
  259. package/skills/oma-search/resources/execution-protocol.md +86 -0
  260. package/skills/oma-search/resources/intent-rules.md +47 -0
  261. package/skills/oma-search/resources/trust-registry.md +53 -0
  262. package/skills/oma-security/SKILL.md +172 -0
  263. package/skills/oma-security/resources/artex.md +108 -0
  264. package/skills/oma-security/resources/ci.md +134 -0
  265. package/skills/oma-security/resources/cisco-source.md +108 -0
  266. package/skills/oma-security/resources/deepsec-config.md +45 -0
  267. package/skills/oma-security/resources/deepsec-matchers.md +35 -0
  268. package/skills/oma-security/resources/deepsec-scanning.md +45 -0
  269. package/skills/oma-security/resources/deepsec-setup.md +41 -0
  270. package/skills/oma-security/resources/findings-contract.md +408 -0
  271. package/skills/oma-security/resources/mcp-scanning.md +122 -0
  272. package/skills/oma-security/resources/skill-scanning.md +106 -0
  273. package/skills/oma-security/resources/validation.md +71 -0
  274. package/skills/oma-skill-creation/SKILL.md +84 -0
  275. package/skills/oma-skill-creation/resources/prompt-evaluation.md +36 -0
  276. package/skills/oma-skill-creation/resources/ssl-lite-template.md +108 -0
  277. package/skills/oma-skill-creation/resources/validation-checklist.md +101 -0
  278. package/skills/oma-slide/SKILL.md +277 -0
  279. package/skills/oma-slide/resources/animation-patterns.md +245 -0
  280. package/skills/oma-slide/resources/assets/deck-stage.js +514 -0
  281. package/skills/oma-slide/resources/assets/viewport-base.css +257 -0
  282. package/skills/oma-slide/resources/canva-integration.md +203 -0
  283. package/skills/oma-slide/resources/checklist.md +46 -0
  284. package/skills/oma-slide/resources/design-doctrine.md +260 -0
  285. package/skills/oma-slide/resources/fixed-stage.md +232 -0
  286. package/skills/oma-slide/resources/generation-protocol.md +393 -0
  287. package/skills/oma-slide/resources/native-pptx.md +84 -0
  288. package/skills/oma-slide/resources/selection-index.json +461 -0
  289. package/skills/oma-slide/resources/style-presets.LICENSE +10 -0
  290. package/skills/oma-slide/resources/style-presets.md +520 -0
  291. package/skills/oma-tf-infra/SKILL.md +201 -0
  292. package/skills/oma-tf-infra/resources/checklist.md +53 -0
  293. package/skills/oma-tf-infra/resources/cost-optimization.md +574 -0
  294. package/skills/oma-tf-infra/resources/error-playbook.md +68 -0
  295. package/skills/oma-tf-infra/resources/examples.md +38 -0
  296. package/skills/oma-tf-infra/resources/execution-protocol.md +129 -0
  297. package/skills/oma-tf-infra/resources/iso-42001-infra.md +229 -0
  298. package/skills/oma-tf-infra/resources/multi-cloud-examples.md +241 -0
  299. package/skills/oma-tf-infra/resources/policy-testing-examples.md +332 -0
  300. package/skills/oma-translation/SKILL.md +229 -0
  301. package/skills/oma-translation/resources/anti-ai-patterns.md +104 -0
  302. package/skills/oma-translation/resources/lang/_template.md +73 -0
  303. package/skills/oma-translation/resources/lang/en.md +123 -0
  304. package/skills/oma-translation/resources/lang/ja.md +274 -0
  305. package/skills/oma-translation/resources/lang/ko.md +228 -0
  306. package/skills/oma-translation/resources/lang/zh.md +285 -0
  307. package/skills/oma-translation/resources/style-skills.md +104 -0
  308. package/skills/oma-translation/resources/translation-rubric.md +126 -0
  309. package/skills/oma-video/SKILL.md +229 -0
  310. package/skills/oma-video/config/video-config.yaml +60 -0
  311. package/skills/oma-video/resources/checklist.md +26 -0
  312. package/skills/oma-video/resources/execution-protocol.md +122 -0
  313. package/skills/oma-video/resources/existing-media.md +185 -0
  314. package/skills/oma-video/resources/hyperframes-authoring/README.md +44 -0
  315. package/skills/oma-video/resources/hyperframes-authoring/demo.md +31 -0
  316. package/skills/oma-video/resources/hyperframes-authoring/explainer.md +10 -0
  317. package/skills/oma-video/resources/hyperframes-authoring/shorts.md +10 -0
  318. package/skills/oma-video/resources/motion-principles.md +81 -0
  319. package/skills/oma-video/resources/mpt/driver.py +301 -0
  320. package/skills/oma-video/resources/prompt-tips.md +75 -0
  321. package/skills/oma-video/resources/script-schema.md +81 -0
  322. package/skills/oma-video/resources/sound-design.md +127 -0
  323. package/skills/oma-video/resources/strudel/package.json +11 -0
  324. package/skills/oma-video/resources/strudel/page.html +87 -0
  325. package/skills/oma-video/resources/strudel/render.mjs +295 -0
  326. package/skills/oma-video/resources/vendor-matrix.md +124 -0
  327. package/skills/oma-voice/SKILL.md +249 -0
  328. package/skills/oma-voice/config/voice-config.yaml +30 -0
  329. package/skills/oma-voice/resources/audio-processing.md +119 -0
  330. package/skills/oma-voice/resources/checklist.md +47 -0
  331. package/skills/oma-voice/resources/execution-protocol.md +215 -0
  332. package/skills/oma-voice/resources/prompt-tips.md +43 -0
  333. package/skills/oma-voice/resources/voice-matrix.md +51 -0
  334. package/skills/oma-voice/resources/voicestudio.md +87 -0
@@ -0,0 +1,1014 @@
1
+ # Mobile Agent - Code Snippets (React Native)
2
+
3
+ Copy-paste ready patterns. Use these as starting points; adapt to the specific task.
4
+ Screens and components **never** call axios directly — they consume hooks built on TanStack Query.
5
+
6
+ ---
7
+
8
+ ## 1. Package Dependencies
9
+
10
+ ```json
11
+ // package.json (relevant dependencies)
12
+ {
13
+ "dependencies": {
14
+ // Core (RN 0.86 defaults to the New Architecture; 0.82+ is New-Arch only)
15
+ "react": "^19.0.0",
16
+ "react-native": "^0.86.0",
17
+
18
+ // Navigation (v7 is the current stable line)
19
+ "@react-navigation/native": "^7.3.10",
20
+ "@react-navigation/native-stack": "^7.3.10",
21
+ "@react-navigation/bottom-tabs": "^7.3.10",
22
+ "react-native-screens": "^4.26.2",
23
+ "react-native-safe-area-context": "^5.8.0",
24
+
25
+ // HTTP transport
26
+ "axios": "^1.7.7",
27
+ "axios-retry": "^4.5.0",
28
+
29
+ // Server-state cache + offline persistence
30
+ "@tanstack/react-query": "^5.101.2",
31
+ "@tanstack/query-sync-storage-persister": "^5.101.2",
32
+ "@tanstack/react-query-persist-client": "^5.101.2",
33
+
34
+ // Network status (drives onlineManager + useNetworkStatus)
35
+ "@react-native-community/netinfo": "^11.4.1",
36
+
37
+ // Local storage (v4 is the Nitro module; requires RN 0.76+)
38
+ "react-native-mmkv": "^4.3.2",
39
+ "react-native-nitro-modules": "0.35.9", // MMKV 4.3.2 uses this in its development dependencies
40
+
41
+ // Secrets (choose one)
42
+ "expo-secure-store": "^13.0.0",
43
+ // -- OR (bare RN) --
44
+ "react-native-keychain": "^10.0.0",
45
+
46
+ // Client-state
47
+ "zustand": "^5.0.14"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^5.5.4",
51
+ "@types/react": "^19.0.0",
52
+ "jest": "^29.7.0",
53
+ // Matchers (toBeVisible, etc.) ship inside @testing-library/react-native >=12.4,
54
+ // so no separate matcher package is needed. React Native ships its own types too.
55
+ "@testing-library/react-native": "^14.0.1",
56
+ "babel-jest": "^29.7.0"
57
+ }
58
+ }
59
+ ```
60
+
61
+ ```json
62
+ // tsconfig.json (strict baseline)
63
+ {
64
+ "compilerOptions": {
65
+ "strict": true,
66
+ "target": "ES2022",
67
+ "lib": ["ES2022"],
68
+ "jsx": "react-native",
69
+ "moduleResolution": "bundler",
70
+ "baseUrl": ".",
71
+ "paths": {
72
+ "@api/*": ["src/api/*"],
73
+ "@features/*": ["src/features/*"],
74
+ "@shared/*": ["src/shared/*"],
75
+ "@navigation/*": ["src/navigation/*"],
76
+ "@store/*": ["src/store/*"]
77
+ },
78
+ "skipLibCheck": true
79
+ }
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 2. QueryClient + MMKV Persister Setup
86
+
87
+ ```typescript
88
+ // src/shared/utils/storage.ts
89
+ // Canonical MMKV singletons. Every module that needs MMKV imports from here —
90
+ // never call `createMMKV(...)` anywhere else. MMKV stores plain text unless an
91
+ // encryptionKey is passed, so it holds NON-SECRET data only (prefs, offline
92
+ // flags, the query cache). Secrets live in the Keychain-backed auth store (§10).
93
+
94
+ import { createMMKV } from 'react-native-mmkv';
95
+
96
+ export const storage = createMMKV({ id: 'app-storage' });
97
+
98
+ // Separate instance namespaced for the query cache to avoid key collisions.
99
+ export const queryStorage = createMMKV({ id: 'query-cache' });
100
+ ```
101
+
102
+ ```typescript
103
+ // src/api/queryClient.ts
104
+ // Creates the single QueryClient, the MMKV cache persister, and wires
105
+ // onlineManager to NetInfo. Import this module once in App.tsx — never create
106
+ // per-component QueryClients.
107
+
108
+ import { QueryClient, onlineManager } from '@tanstack/react-query';
109
+ import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
110
+ import NetInfo from '@react-native-community/netinfo';
111
+ import { queryStorage } from '@shared/utils/storage';
112
+
113
+ // Drive TanStack Query's online state from the real network status so paused
114
+ // mutations resume and stale queries refetch the moment connectivity returns.
115
+ onlineManager.setEventListener((setOnline) =>
116
+ NetInfo.addEventListener((state) => setOnline(!!state.isConnected)),
117
+ );
118
+
119
+ // Persister adapter: TanStack Query serialises/deserialises its cache as a
120
+ // single JSON string under the key below. MMKV provides synchronous access so
121
+ // the cache hydrates before the first render, enabling true offline-first UX.
122
+ let cacheSessionVersion = 0;
123
+ export const createAccountPersister = (accountId: string) => {
124
+ const version = cacheSessionVersion;
125
+ return createSyncStoragePersister({
126
+ key: `query-cache:${accountId}`,
127
+ storage: {
128
+ getItem: (key) => queryStorage.getString(key) ?? null,
129
+ setItem: (key, value) => {
130
+ if (version === cacheSessionVersion) queryStorage.set(key, value);
131
+ },
132
+ removeItem: (key) => queryStorage.remove(key),
133
+ },
134
+ });
135
+ };
136
+
137
+ const createQueryClient = () => new QueryClient({
138
+ defaultOptions: {
139
+ queries: {
140
+ // Data older than 60 s triggers a background revalidation on next mount.
141
+ staleTime: 60_000,
142
+ // gcTime MUST be >= the persister's maxAge (see App.tsx below), or the
143
+ // in-memory entry is garbage-collected before the persisted copy can be
144
+ // restored — which silently defeats "survives app restart". 24h matches
145
+ // persistQueryClient's default maxAge.
146
+ gcTime: 1000 * 60 * 60 * 24,
147
+ // Retry failed queries up to 3 times with exponential back-off.
148
+ retry: 3,
149
+ // Do not refetch when the window regains focus (mobile has no browser focus).
150
+ refetchOnWindowFocus: false,
151
+ },
152
+ mutations: {
153
+ retry: 0,
154
+ },
155
+ },
156
+ });
157
+ export let queryClient = createQueryClient();
158
+ export async function resetAccountQueryCache(accountId: string | null): Promise<void> {
159
+ cacheSessionVersion += 1; // block delayed disk writes from the retired session
160
+ await queryClient.cancelQueries();
161
+ queryClient.clear();
162
+ if (accountId) await createAccountPersister(accountId).removeClient();
163
+ queryClient = createQueryClient(); // old callbacks retain an unmounted client
164
+ }
165
+
166
+ ```
167
+
168
+ ```typescript
169
+ // src/App.tsx (provider wiring — abridged)
170
+ import React, { useEffect, useMemo, useState } from 'react';
171
+ import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
172
+ import { NavigationContainer } from '@react-navigation/native';
173
+ import { queryClient, createAccountPersister } from '@api/queryClient';
174
+ import { RootNavigator } from '@navigation/RootNavigator';
175
+ import { hydrateAuth, useAuthStore } from '@store/authStore';
176
+ import { LoadingView } from '@shared/components/LoadingView';
177
+
178
+ // Bump on breaking changes to any cached shape to discard the persisted cache.
179
+ // Keep it stable across a release (e.g. the app version string).
180
+ const CACHE_BUSTER = '1.0.0';
181
+
182
+ export default function App() {
183
+ const [ready, setReady] = useState(false);
184
+ const accountId = useAuthStore((state) => state.accountId);
185
+ const sessionVersion = useAuthStore((state) => state.sessionVersion);
186
+ const persister = useMemo(() => accountId ? createAccountPersister(accountId) : null, [accountId, sessionVersion]);
187
+ useEffect(() => {
188
+ hydrateAuth().catch(() => useAuthStore.getState().clearToken()).finally(() => setReady(true));
189
+ }, []);
190
+ if (!ready) return <LoadingView label="Restoring session" />;
191
+ if (!accountId || !persister) return <NavigationContainer><RootNavigator /></NavigationContainer>;
192
+
193
+ return (
194
+ // PersistQueryClientProvider restores the MMKV-persisted cache before the
195
+ // first render, so screens show stale data instantly while revalidating.
196
+ // maxAge caps how long a persisted cache is trusted; it must be <= the
197
+ // QueryClient's gcTime (24h) or entries GC out before they can be restored.
198
+ <PersistQueryClientProvider
199
+ key={`${accountId}:${sessionVersion}`}
200
+ client={queryClient}
201
+ persistOptions={{
202
+ persister,
203
+ maxAge: 1000 * 60 * 60 * 24,
204
+ buster: CACHE_BUSTER,
205
+ }}
206
+ >
207
+ <NavigationContainer>
208
+ <RootNavigator />
209
+ </NavigationContainer>
210
+ </PersistQueryClientProvider>
211
+ );
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 3. Axios Instance with Auth Interceptor
218
+
219
+ ```typescript
220
+ // src/api/client.ts
221
+ // Singleton axios instance — the ONLY place in the codebase that creates an
222
+ // axios instance. All src/api/*.ts functions import from here.
223
+
224
+ import axios, {
225
+ type AxiosInstance,
226
+ type AxiosRequestConfig,
227
+ type InternalAxiosRequestConfig,
228
+ type AxiosResponse,
229
+ type AxiosError,
230
+ } from 'axios';
231
+ import axiosRetry from 'axios-retry';
232
+ import { useAuthStore } from '@store/authStore';
233
+
234
+ const BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL ?? 'https://api.example.com';
235
+
236
+ export const apiClient: AxiosInstance = axios.create({
237
+ baseURL: BASE_URL,
238
+ timeout: 15_000,
239
+ headers: { 'Content-Type': 'application/json' },
240
+ });
241
+
242
+ // --- Request interceptor: inject bearer token ---
243
+ // Read synchronously from the in-memory auth store (hydrated from the Keychain
244
+ // at app start, §10). Secrets NEVER live in plain-text MMKV.
245
+ export type SessionRequestConfig = AxiosRequestConfig & { _sessionVersion: number; _accountId: string | null };
246
+ type SessionRequest = InternalAxiosRequestConfig & { _sessionVersion?: number; _accountId?: string | null; _retried?: boolean };
247
+ apiClient.interceptors.request.use((config: SessionRequest) => {
248
+ config._sessionVersion ??= useAuthStore.getState().sessionVersion;
249
+ if (!('_accountId' in config)) config._accountId = useAuthStore.getState().accountId;
250
+ if (config._sessionVersion !== useAuthStore.getState().sessionVersion ||
251
+ config._accountId !== useAuthStore.getState().accountId) {
252
+ throw new axios.CanceledError('Request belongs to a retired session');
253
+ }
254
+ const token = useAuthStore.getState().accessToken;
255
+ if (token) {
256
+ config.headers.set('Authorization', `Bearer ${token}`);
257
+ }
258
+ return config;
259
+ });
260
+
261
+ // --- Single-flight token refresh ---
262
+ // Only one refresh is ever in flight; concurrent 401s await the same promise
263
+ // and then replay their original request exactly once.
264
+ let refreshPromise: { version: number; promise: Promise<string | null> } | null = null;
265
+
266
+ async function refreshAccessToken(): Promise<string | null> {
267
+ const version = useAuthStore.getState().sessionVersion;
268
+ try {
269
+ // Bare axios (no interceptors/retry) so the refresh can't recurse on itself.
270
+ const { data } = await axios.post<{ accessToken: string }>(
271
+ `${BASE_URL}/auth/refresh`,
272
+ {},
273
+ { withCredentials: true }, // refresh token rides as an httpOnly cookie
274
+ );
275
+ if (!await useAuthStore.getState().updateToken(data.accessToken, version)) return null;
276
+ return data.accessToken;
277
+ } catch {
278
+ await useAuthStore.getState().clearToken(version);
279
+ return null;
280
+ }
281
+ }
282
+
283
+ // --- Response interceptor: refresh once on 401, else logout ---
284
+ apiClient.interceptors.response.use(
285
+ (response: AxiosResponse) => response,
286
+ async (error: AxiosError) => {
287
+ const original = error.config as
288
+ | SessionRequest
289
+ | undefined;
290
+
291
+ if (error.response?.status === 401 && original && !original._retried &&
292
+ !original.signal?.aborted && useAuthStore.getState().isAuthenticated &&
293
+ original._sessionVersion === useAuthStore.getState().sessionVersion) {
294
+ original._retried = true;
295
+ const version = useAuthStore.getState().sessionVersion;
296
+ if (!refreshPromise || refreshPromise.version !== version) {
297
+ const promise = refreshAccessToken().finally(() => {
298
+ if (refreshPromise?.promise === promise) refreshPromise = null;
299
+ });
300
+ refreshPromise = { version, promise };
301
+ }
302
+ const newToken = await refreshPromise.promise;
303
+ if (newToken && version === useAuthStore.getState().sessionVersion && !original.signal?.aborted) {
304
+ original.headers.set('Authorization', `Bearer ${newToken}`);
305
+ return apiClient(original); // replay the original request once
306
+ }
307
+ }
308
+ return Promise.reject(error);
309
+ },
310
+ );
311
+
312
+ // --- Retry: idempotent requests only, 3 attempts, exponential back-off ---
313
+ axiosRetry(apiClient, {
314
+ retries: 3,
315
+ retryCondition: (error) =>
316
+ ['get', 'head', 'options', 'put', 'delete'].includes(error.config?.method?.toLowerCase() ?? '') &&
317
+ axiosRetry.isNetworkOrIdempotentRequestError(error) &&
318
+ error.response?.status !== 401,
319
+ retryDelay: axiosRetry.exponentialDelay,
320
+ });
321
+ ```
322
+
323
+ ---
324
+
325
+ ## 4. Typed API Data Module (src/api/todos.ts)
326
+
327
+ ```typescript
328
+ // src/api/todos.ts
329
+ // Typed axios functions for the /todos resource.
330
+ // These are PURE DATA FUNCTIONS — they return decoded TypeScript objects and
331
+ // know nothing about React, hooks, or the query cache. They are the transport
332
+ // seam: screens never import from this file directly, only through query/mutation
333
+ // hooks (src/features/todos/queries.ts and mutations.ts).
334
+
335
+ import { apiClient, type SessionRequestConfig } from './client';
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Domain types
339
+ // ---------------------------------------------------------------------------
340
+
341
+ export interface Todo {
342
+ id: string;
343
+ title: string;
344
+ completed: boolean;
345
+ createdAt: string;
346
+ }
347
+
348
+ export interface CreateTodoInput {
349
+ title: string;
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // API functions
354
+ // ---------------------------------------------------------------------------
355
+
356
+ /** Fetch all todos for the authenticated user. */
357
+ export async function fetchTodos(accountId: string | null, sessionVersion: number, signal?: AbortSignal): Promise<Todo[]> {
358
+ const config: SessionRequestConfig = { _accountId: accountId, _sessionVersion: sessionVersion, signal };
359
+ const { data } = await apiClient.get<Todo[]>('/todos', config);
360
+ return data;
361
+ }
362
+
363
+ /** Fetch a single todo by ID. */
364
+ export async function fetchTodo(id: string, accountId: string | null, sessionVersion: number, signal?: AbortSignal): Promise<Todo> {
365
+ const config: SessionRequestConfig = { _accountId: accountId, _sessionVersion: sessionVersion, signal };
366
+ const { data } = await apiClient.get<Todo>(`/todos/${id}`, config);
367
+ return data;
368
+ }
369
+
370
+ /** Create a new todo. */
371
+ export async function createTodo(input: CreateTodoInput, accountId: string | null, sessionVersion: number): Promise<Todo> {
372
+ const config: SessionRequestConfig = { _accountId: accountId, _sessionVersion: sessionVersion };
373
+ const { data } = await apiClient.post<Todo>('/todos', input, config);
374
+ return data;
375
+ }
376
+
377
+ /** Toggle the completed flag on a todo. */
378
+ export async function toggleTodo(id: string, accountId: string | null, sessionVersion: number): Promise<Todo> {
379
+ const config: SessionRequestConfig = { _accountId: accountId, _sessionVersion: sessionVersion };
380
+ const { data } = await apiClient.patch<Todo>(`/todos/${id}/toggle`, undefined, config);
381
+ return data;
382
+ }
383
+
384
+ /** Permanently delete a todo. */
385
+ export async function deleteTodo(id: string, accountId: string | null, sessionVersion: number): Promise<void> {
386
+ const config: SessionRequestConfig = { _accountId: accountId, _sessionVersion: sessionVersion };
387
+ await apiClient.delete(`/todos/${id}`, config);
388
+ }
389
+ ```
390
+
391
+ ---
392
+
393
+ ## 5. Feature Query and Mutation Hooks
394
+
395
+ ```typescript
396
+ // src/features/todos/queries.ts
397
+ // Read hooks — wrap src/api/todos.ts functions with TanStack Query.
398
+ // Explicit staleTime / gcTime on every query; never rely on defaults alone.
399
+
400
+ import { useQuery } from '@tanstack/react-query';
401
+ import { useAuthStore } from '@store/authStore';
402
+ import { fetchTodos, fetchTodo } from '@api/todos';
403
+
404
+ // Centralise query key definitions so mutations can reference them without
405
+ // string duplication. Key shape: [account/tenant, operation, ...params].
406
+ export const todoKeys = {
407
+ all: (accountId: string | null) => ['account', accountId, 'todos'] as const,
408
+ lists: (accountId: string | null) => [...todoKeys.all(accountId), 'list'] as const,
409
+ detail: (accountId: string | null, id: string) => [...todoKeys.all(accountId), 'detail', id] as const,
410
+ };
411
+
412
+ /** Fetch and cache the todo list. Returns stale data instantly, revalidates in
413
+ * the background when data is older than staleTime. */
414
+ export function useTodosQuery() {
415
+ const accountId = useAuthStore((state) => state.accountId);
416
+ const sessionVersion = useAuthStore((state) => state.sessionVersion);
417
+ return useQuery({
418
+ queryKey: todoKeys.lists(accountId),
419
+ queryFn: ({ signal }) => fetchTodos(accountId, sessionVersion, signal),
420
+ enabled: Boolean(accountId),
421
+ staleTime: 60_000, // 1 minute — adjust per resource freshness requirement
422
+ gcTime: 1000 * 60 * 60 * 24, // 24h — must stay >= the persister maxAge (§2)
423
+ });
424
+ }
425
+
426
+ /** Fetch and cache a single todo. */
427
+ export function useTodoDetailQuery(id: string) {
428
+ const accountId = useAuthStore((state) => state.accountId);
429
+ const sessionVersion = useAuthStore((state) => state.sessionVersion);
430
+ return useQuery({
431
+ queryKey: todoKeys.detail(accountId, id),
432
+ queryFn: ({ signal }) => fetchTodo(id, accountId, sessionVersion, signal),
433
+ staleTime: 60_000,
434
+ gcTime: 1000 * 60 * 60 * 24, // 24h — see useTodosQuery
435
+ enabled: Boolean(accountId && id),
436
+ });
437
+ }
438
+ ```
439
+
440
+ ```typescript
441
+ // src/features/todos/mutations.ts
442
+ // Write hooks — wrap src/api/todos.ts functions with useMutation.
443
+ // Every mutation MUST call invalidateQueries for affected keys on onSuccess
444
+ // so the cache repopulates from the server on the next read.
445
+
446
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
447
+ import { useAuthStore } from '@store/authStore';
448
+ import { createTodo, toggleTodo, deleteTodo, type CreateTodoInput, type Todo } from '@api/todos';
449
+ import { todoKeys } from './queries';
450
+
451
+ // A paused/optimistic mutation from an old provider must not use a new login.
452
+ function runAccountMutation<T>(accountId: string | null, version: number, operation: (version: number) => Promise<T>): Promise<T> {
453
+ const current = useAuthStore.getState();
454
+ if (!accountId || current.accountId !== accountId || current.sessionVersion !== version) {
455
+ return Promise.reject(new Error('Mutation belongs to a retired session'));
456
+ }
457
+ return operation(version);
458
+ }
459
+
460
+ /** Create a new todo then invalidate the list cache. */
461
+ export function useCreateTodo() {
462
+ const accountId = useAuthStore((state) => state.accountId);
463
+ const sessionVersion = useAuthStore((state) => state.sessionVersion);
464
+ const queryClient = useQueryClient();
465
+
466
+ return useMutation({
467
+ mutationFn: (input: CreateTodoInput) => runAccountMutation(accountId, sessionVersion, (version) => createTodo(input, accountId, version)),
468
+ onSuccess: () => {
469
+ // Invalidate the list so the next useTodosQuery fetches fresh data.
470
+ queryClient.invalidateQueries({ queryKey: todoKeys.lists(accountId) });
471
+ },
472
+ });
473
+ }
474
+
475
+ /** Toggle a todo's completed flag then invalidate list + detail caches. */
476
+ export function useToggleTodo() {
477
+ const accountId = useAuthStore((state) => state.accountId);
478
+ const sessionVersion = useAuthStore((state) => state.sessionVersion);
479
+ const queryClient = useQueryClient();
480
+
481
+ return useMutation({
482
+ mutationFn: (id: string) => runAccountMutation(accountId, sessionVersion, (version) => toggleTodo(id, accountId, version)),
483
+ // Optimistic update: flip the flag in the cache immediately for instant UI
484
+ // feedback; roll back if the mutation fails.
485
+ onMutate: async (id) => {
486
+ // Cancel in-flight queries for both keys so they can't clobber our patch.
487
+ await queryClient.cancelQueries({ queryKey: todoKeys.lists(accountId) });
488
+ await queryClient.cancelQueries({ queryKey: todoKeys.detail(accountId, id) });
489
+
490
+ const previousList = queryClient.getQueryData<Todo[]>(todoKeys.lists(accountId));
491
+ const previousDetail = queryClient.getQueryData<Todo>(todoKeys.detail(accountId, id));
492
+
493
+ // Patch each cache entry only when it exists — returning `old` untouched
494
+ // when undefined avoids materialising a fake empty list.
495
+ queryClient.setQueryData<Todo[] | undefined>(todoKeys.lists(accountId), (old) =>
496
+ old ? old.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)) : old,
497
+ );
498
+ queryClient.setQueryData<Todo | undefined>(todoKeys.detail(accountId, id), (old) =>
499
+ old ? { ...old, completed: !old.completed } : old,
500
+ );
501
+
502
+ return { previousList, previousDetail };
503
+ },
504
+ onError: (_err, id, context) => {
505
+ // Roll back both keys on error.
506
+ if (context?.previousList !== undefined) {
507
+ queryClient.setQueryData(todoKeys.lists(accountId), context.previousList);
508
+ }
509
+ if (context?.previousDetail !== undefined) {
510
+ queryClient.setQueryData(todoKeys.detail(accountId, id), context.previousDetail);
511
+ }
512
+ },
513
+ onSettled: (_data, _err, id) => {
514
+ // Reconcile with server truth for both keys once the mutation settles.
515
+ queryClient.invalidateQueries({ queryKey: todoKeys.lists(accountId) });
516
+ queryClient.invalidateQueries({ queryKey: todoKeys.detail(accountId, id) });
517
+ },
518
+ });
519
+ }
520
+
521
+ /** Delete a todo then invalidate the list cache. */
522
+ export function useDeleteTodo() {
523
+ const accountId = useAuthStore((state) => state.accountId);
524
+ const sessionVersion = useAuthStore((state) => state.sessionVersion);
525
+ const queryClient = useQueryClient();
526
+
527
+ return useMutation({
528
+ mutationFn: (id: string) => runAccountMutation(accountId, sessionVersion, (version) => deleteTodo(id, accountId, version)),
529
+ onSuccess: (_data, id) => {
530
+ queryClient.invalidateQueries({ queryKey: todoKeys.lists(accountId) });
531
+ // Remove the detail cache entry immediately — it is no longer valid.
532
+ queryClient.removeQueries({ queryKey: todoKeys.detail(accountId, id) });
533
+ },
534
+ });
535
+ }
536
+ ```
537
+
538
+ ---
539
+
540
+ ## 6. Screen Consuming the Hooks
541
+
542
+ ```typescript
543
+ // src/features/todos/ui/TodosScreen.tsx
544
+ // Consumes useTodosQuery and useDeleteTodo. Note: no axios import here.
545
+
546
+ import React from 'react';
547
+ import {
548
+ View,
549
+ Text,
550
+ FlatList,
551
+ TouchableOpacity,
552
+ RefreshControl,
553
+ StyleSheet,
554
+ } from 'react-native';
555
+ import type { NativeStackScreenProps } from '@react-navigation/native-stack';
556
+ import type { RootStackParamList } from '@navigation/types';
557
+ import { LoadingView } from '@shared/components/LoadingView';
558
+ import { ErrorView } from '@shared/components/ErrorView';
559
+ import { EmptyStateView } from '@shared/components/EmptyStateView';
560
+ import { useTodosQuery } from '../queries';
561
+ import { useDeleteTodo, useToggleTodo } from '../mutations';
562
+
563
+ type Props = NativeStackScreenProps<RootStackParamList, 'TodoList'>;
564
+
565
+ export function TodosScreen({ navigation }: Props) {
566
+ const { data: todos, isPending, isError, error, refetch, isRefetching } = useTodosQuery();
567
+ const toggleMutation = useToggleTodo();
568
+ const deleteMutation = useDeleteTodo();
569
+
570
+ // States delegate to the shared components (§12) so every screen renders them
571
+ // identically. isPending is the v5 first-load flag (there is no cached data yet).
572
+ if (isPending) return <LoadingView label="Loading todos" />;
573
+ if (isError) return <ErrorView error={error} onRetry={refetch} />;
574
+ if (todos.length === 0) return <EmptyStateView message="No todos yet. Add your first one!" />;
575
+
576
+ // --- Data state ---
577
+ return (
578
+ <FlatList
579
+ data={todos}
580
+ keyExtractor={(item) => item.id}
581
+ refreshControl={
582
+ <RefreshControl refreshing={isRefetching} onRefresh={refetch} />
583
+ }
584
+ renderItem={({ item }) => (
585
+ <View style={styles.row}>
586
+ <TouchableOpacity
587
+ style={styles.checkbox}
588
+ onPress={() => toggleMutation.mutate(item.id)}
589
+ accessibilityRole="checkbox"
590
+ accessibilityState={{ checked: item.completed }}
591
+ >
592
+ <Text>{item.completed ? '☑' : '☐'}</Text>
593
+ </TouchableOpacity>
594
+
595
+ <TouchableOpacity
596
+ style={styles.titleContainer}
597
+ onPress={() => navigation.navigate('TodoDetail', { id: item.id })}
598
+ >
599
+ <Text style={[styles.title, item.completed && styles.completed]}>
600
+ {item.title}
601
+ </Text>
602
+ </TouchableOpacity>
603
+
604
+ <TouchableOpacity
605
+ onPress={() => deleteMutation.mutate(item.id)}
606
+ testID={`todo-delete-${item.id}`}
607
+ accessibilityLabel={`Delete ${item.title}`}
608
+ >
609
+ <Text style={styles.deleteIcon}>✕</Text>
610
+ </TouchableOpacity>
611
+ </View>
612
+ )}
613
+ />
614
+ );
615
+ }
616
+
617
+ const styles = StyleSheet.create({
618
+ row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#e0e0e0' },
619
+ checkbox: { marginRight: 12 },
620
+ titleContainer: { flex: 1 },
621
+ title: { fontSize: 16, color: '#212121' },
622
+ completed: { textDecorationLine: 'line-through', color: '#9e9e9e' },
623
+ deleteIcon: { color: '#9e9e9e', fontSize: 16, paddingLeft: 12 },
624
+ });
625
+ ```
626
+
627
+ ---
628
+
629
+ ## 7. Navigation Setup with Typed Param List
630
+
631
+ ```typescript
632
+ // src/navigation/types.ts
633
+ export type RootStackParamList = {
634
+ TodoList: undefined;
635
+ TodoDetail: { id: string };
636
+ Login: undefined;
637
+ };
638
+ ```
639
+
640
+ ```typescript
641
+ // src/navigation/RootNavigator.tsx
642
+ import React from 'react';
643
+ import { createNativeStackNavigator } from '@react-navigation/native-stack';
644
+ import type { RootStackParamList } from './types';
645
+ import { TodosScreen } from '@features/todos/ui/TodosScreen';
646
+ import { TodoDetailScreen } from '@features/todos/ui/TodoDetailScreen';
647
+ import { LoginScreen } from '@features/auth/ui/LoginScreen';
648
+ import { useAuthStore } from '@store/authStore';
649
+
650
+ const Stack = createNativeStackNavigator<RootStackParamList>();
651
+
652
+ export function RootNavigator() {
653
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
654
+
655
+ return (
656
+ <Stack.Navigator>
657
+ {isAuthenticated ? (
658
+ <>
659
+ <Stack.Screen
660
+ name="TodoList"
661
+ component={TodosScreen}
662
+ options={{ title: 'My Todos' }}
663
+ />
664
+ <Stack.Screen
665
+ name="TodoDetail"
666
+ component={TodoDetailScreen}
667
+ options={{ title: 'Todo Detail' }}
668
+ />
669
+ </>
670
+ ) : (
671
+ <Stack.Screen
672
+ name="Login"
673
+ component={LoginScreen}
674
+ options={{ headerShown: false }}
675
+ />
676
+ )}
677
+ </Stack.Navigator>
678
+ );
679
+ }
680
+ ```
681
+
682
+ ---
683
+
684
+ ## 8. Jest Test: Component with QueryClientProvider
685
+
686
+ ```typescript
687
+ // src/features/todos/__tests__/TodosScreen.test.tsx
688
+ // Wraps the component in a QueryClientProvider with a no-retry, zero-staleTime
689
+ // QueryClient. The api module is mocked at the Jest boundary — tests assert on
690
+ // component output, not axios internals.
691
+
692
+ import React from 'react';
693
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
694
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
695
+ import { NavigationContainer } from '@react-navigation/native';
696
+ import { TodosScreen } from '../ui/TodosScreen';
697
+
698
+ // Mock the entire api/todos module — the seam between hooks and transport.
699
+ jest.mock('@api/todos', () => ({
700
+ fetchTodos: jest.fn(),
701
+ toggleTodo: jest.fn(),
702
+ deleteTodo: jest.fn(),
703
+ }));
704
+
705
+ import * as todosApi from '@api/todos';
706
+
707
+ // Helper: create a fresh QueryClient per test to avoid state bleed.
708
+ function createTestQueryClient() {
709
+ return new QueryClient({
710
+ defaultOptions: {
711
+ queries: {
712
+ retry: false, // Surface errors immediately in tests
713
+ staleTime: 0, // Always fetch in tests; no stale cache reads
714
+ gcTime: Infinity, // Keep data for the test duration
715
+ },
716
+ mutations: { retry: false },
717
+ },
718
+ });
719
+ }
720
+
721
+ // Helper: wrap the component with required providers.
722
+ function renderWithProviders(ui: React.ReactElement) {
723
+ const testQueryClient = createTestQueryClient();
724
+ return render(
725
+ <QueryClientProvider client={testQueryClient}>
726
+ <NavigationContainer>
727
+ {ui}
728
+ </NavigationContainer>
729
+ </QueryClientProvider>,
730
+ );
731
+ }
732
+
733
+ const mockNavigation = { navigate: jest.fn() } as any;
734
+
735
+ describe('TodosScreen', () => {
736
+ afterEach(() => {
737
+ jest.clearAllMocks();
738
+ });
739
+
740
+ it('shows a loading indicator while fetching', async () => {
741
+ // Delay resolution so we can catch the loading state.
742
+ (todosApi.fetchTodos as jest.Mock).mockReturnValue(new Promise(() => {}));
743
+ renderWithProviders(<TodosScreen navigation={mockNavigation} route={{} as any} />);
744
+
745
+ expect(screen.getByLabelText('Loading todos')).toBeTruthy();
746
+ });
747
+
748
+ it('renders todo titles after successful fetch', async () => {
749
+ (todosApi.fetchTodos as jest.Mock).mockResolvedValue([
750
+ { id: '1', title: 'Buy milk', completed: false, createdAt: '' },
751
+ { id: '2', title: 'Walk the dog', completed: true, createdAt: '' },
752
+ ]);
753
+
754
+ renderWithProviders(<TodosScreen navigation={mockNavigation} route={{} as any} />);
755
+
756
+ expect(await screen.findByText('Buy milk')).toBeTruthy();
757
+ expect(screen.getByText('Walk the dog')).toBeTruthy();
758
+ });
759
+
760
+ it('shows empty state when the list is empty', async () => {
761
+ (todosApi.fetchTodos as jest.Mock).mockResolvedValue([]);
762
+
763
+ renderWithProviders(<TodosScreen navigation={mockNavigation} route={{} as any} />);
764
+
765
+ expect(await screen.findByText(/No todos yet/i)).toBeTruthy();
766
+ });
767
+
768
+ it('shows an error message and retry button on fetch failure', async () => {
769
+ (todosApi.fetchTodos as jest.Mock).mockRejectedValue(new Error('Network error'));
770
+
771
+ renderWithProviders(<TodosScreen navigation={mockNavigation} route={{} as any} />);
772
+
773
+ expect(await screen.findByText('Network error')).toBeTruthy();
774
+ expect(screen.getByText('Retry')).toBeTruthy();
775
+ });
776
+
777
+ it('calls deleteTodo when the delete button is pressed', async () => {
778
+ (todosApi.fetchTodos as jest.Mock).mockResolvedValue([
779
+ { id: '42', title: 'Delete me', completed: false, createdAt: '' },
780
+ ]);
781
+ (todosApi.deleteTodo as jest.Mock).mockResolvedValue(undefined);
782
+
783
+ renderWithProviders(<TodosScreen navigation={mockNavigation} route={{} as any} />);
784
+
785
+ const deleteButton = await screen.findByLabelText('Delete Delete me');
786
+ fireEvent.press(deleteButton);
787
+
788
+ await waitFor(() => {
789
+ expect(todosApi.deleteTodo).toHaveBeenCalledWith('42');
790
+ });
791
+ });
792
+ });
793
+ ```
794
+
795
+ ---
796
+
797
+ ## 9. Cache Rules Recap
798
+
799
+ > These rules apply to every `useQuery` and `useMutation` in the codebase.
800
+ > They define the repository-layer cache contract for TanStack Query: the
801
+ > data-fetching layer is the single source of caching truth.
802
+
803
+ 1. **Cache decoded objects, not bytes.** TanStack Query stores the return value of `queryFn` — a decoded TypeScript object. Never cache raw `AxiosResponse` or `ArrayBuffer`.
804
+ 2. **Explicit `staleTime` and `gcTime` on every `useQuery`.** No implicit infinite TTL. `staleTime` governs background revalidation; `gcTime` governs memory reclamation.
805
+ 3. **Query keys = `[account/tenant, operation, ...params]`.** Never key on URLs. Centralise key definitions in a `todoKeys` (or `<resource>Keys`) object so mutations and queries share the same reference.
806
+ 4. **Invalidate on write.** Every `useMutation` calls `queryClient.invalidateQueries({ queryKey })` for all affected list and detail keys in `onSuccess`. Optimistic updates (optional) must pair a rollback in `onError`.
807
+ 5. **MMKV persistence for offline-first.** The `PersistQueryClientProvider` + MMKV persister hydrates the cache before the first render. Stale data renders immediately; revalidation runs in the background.
808
+ 6. **Repository seam.** `src/api/*.ts` functions are the only axios callers. Screens and components never import from `axios` or from `src/api/` directly — they consume query/mutation hooks.
809
+ 7. **Query cache = transient server-owned data.** Durable non-secret user data belongs in MMKV; secrets (the access token) live only in memory in the Keychain-backed auth store (§10), never in plain-text MMKV. Never use TanStack Query as a system of record.
810
+
811
+ ---
812
+
813
+ ## 10. Auth Store (Zustand + Keychain)
814
+
815
+ ```typescript
816
+ // src/store/authStore.ts
817
+ // Auth session store. The access token lives ONLY in memory here and in the
818
+ // platform secure enclave via react-native-keychain (iOS Keychain / Android
819
+ // Keystore) — never in plain-text MMKV. The axios request interceptor (§3)
820
+ // reads `accessToken` synchronously via useAuthStore.getState().
821
+
822
+ import { create } from 'zustand';
823
+ import * as Keychain from 'react-native-keychain';
824
+ import { resetAccountQueryCache } from '@api/queryClient';
825
+
826
+ // Namespaces the credential entry. Use a stable, app-unique service string.
827
+ const KEYCHAIN_SERVICE = 'com.example.app.auth';
828
+
829
+ interface AuthState {
830
+ accessToken: string | null;
831
+ accountId: string | null; // validated non-secret account/tenant ID from the server
832
+ sessionVersion: number;
833
+ isAuthenticated: boolean;
834
+ setToken: (token: string, accountId: string) => Promise<void>;
835
+ updateToken: (token: string, version: number) => Promise<boolean>;
836
+ clearToken: (expectedVersion?: number) => Promise<void>;
837
+ }
838
+
839
+ // Serialize credential writes: a late refresh cannot undo logout.
840
+ let sessionTransition: Promise<unknown> = Promise.resolve();
841
+ function transition<T>(operation: () => Promise<T>): Promise<T> {
842
+ const next = sessionTransition.then(operation);
843
+ sessionTransition = next.catch(() => undefined);
844
+ return next;
845
+ }
846
+
847
+ export const useAuthStore = create<AuthState>()((set, get) => ({
848
+ accessToken: null,
849
+ accountId: null,
850
+ sessionVersion: 0,
851
+ isAuthenticated: false,
852
+ setToken: (token, accountId) => transition(async () => {
853
+ if (!accountId) throw new Error('Validated account ID is required');
854
+ const previousAccount = get().accountId;
855
+ set({ accessToken: null, accountId: null, isAuthenticated: false,
856
+ sessionVersion: get().sessionVersion + 1 }); // unmount old account before awaiting
857
+ await resetAccountQueryCache(previousAccount);
858
+ await Keychain.setGenericPassword(accountId, token, { service: KEYCHAIN_SERVICE });
859
+ set({ accessToken: token, accountId, isAuthenticated: true });
860
+ }),
861
+ updateToken: (token, version) => transition(async () => {
862
+ const accountId = get().accountId;
863
+ if (version !== get().sessionVersion || !accountId) return false;
864
+ await Keychain.setGenericPassword(accountId, token, { service: KEYCHAIN_SERVICE });
865
+ set({ accessToken: token });
866
+ return true;
867
+ }),
868
+ clearToken: (expectedVersion) => transition(async () => {
869
+ // Check inside the queue: a queued new login may have finished meanwhile.
870
+ if (expectedVersion !== undefined && expectedVersion !== get().sessionVersion) return;
871
+ const previousAccount = get().accountId;
872
+ set({ accessToken: null, accountId: null, isAuthenticated: false,
873
+ sessionVersion: get().sessionVersion + 1 });
874
+ await resetAccountQueryCache(previousAccount);
875
+ await Keychain.resetGenericPassword({ service: KEYCHAIN_SERVICE });
876
+ }),
877
+ }));
878
+
879
+ // Call before mounting account providers. The username is the validated account
880
+ // ID written by setToken; discard legacy token-only entries during migration.
881
+ export async function hydrateAuth(): Promise<void> {
882
+ const creds = await Keychain.getGenericPassword({ service: KEYCHAIN_SERVICE });
883
+ if (creds && creds.username !== 'accessToken') {
884
+ await useAuthStore.getState().setToken(creds.password, creds.username);
885
+ } else if (creds) {
886
+ await useAuthStore.getState().clearToken();
887
+ }
888
+ }
889
+ ```
890
+
891
+ ---
892
+
893
+ ## 11. Network Status Hook
894
+
895
+ ```typescript
896
+ // src/shared/hooks/useNetworkStatus.ts
897
+ // Reactive online/offline flag backed by NetInfo. Complements the onlineManager
898
+ // wiring in §2 (which drives TanStack Query) — use this hook for UI affordances
899
+ // such as an offline banner.
900
+
901
+ import { useEffect, useState } from 'react';
902
+ import NetInfo from '@react-native-community/netinfo';
903
+
904
+ /** Returns true while the device reports an active network connection. */
905
+ export function useNetworkStatus(): boolean {
906
+ const [isOnline, setIsOnline] = useState(true);
907
+
908
+ useEffect(() => {
909
+ // addEventListener returns its own unsubscribe function.
910
+ const unsubscribe = NetInfo.addEventListener((state) =>
911
+ setIsOnline(!!state.isConnected),
912
+ );
913
+ return unsubscribe;
914
+ }, []);
915
+
916
+ return isOnline;
917
+ }
918
+ ```
919
+
920
+ ---
921
+
922
+ ## 12. Shared UI Components (Loading / Error / Empty)
923
+
924
+ ```typescript
925
+ // src/shared/components/LoadingView.tsx
926
+ import React from 'react';
927
+ import { View, ActivityIndicator, StyleSheet } from 'react-native';
928
+
929
+ export function LoadingView({ label = 'Loading' }: { label?: string }) {
930
+ return (
931
+ <View style={styles.center}>
932
+ <ActivityIndicator size="large" accessibilityLabel={label} />
933
+ </View>
934
+ );
935
+ }
936
+
937
+ const styles = StyleSheet.create({
938
+ center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
939
+ });
940
+ ```
941
+
942
+ ```typescript
943
+ // src/shared/components/ErrorView.tsx
944
+ import React from 'react';
945
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
946
+
947
+ type Props = { error: unknown; onRetry?: () => void };
948
+
949
+ export function ErrorView({ error, onRetry }: Props) {
950
+ const message = error instanceof Error ? error.message : 'Something went wrong.';
951
+ return (
952
+ <View style={styles.center}>
953
+ <Text style={styles.errorText}>{message}</Text>
954
+ {onRetry ? (
955
+ <TouchableOpacity style={styles.retryButton} onPress={onRetry} accessibilityRole="button">
956
+ <Text style={styles.retryText}>Retry</Text>
957
+ </TouchableOpacity>
958
+ ) : null}
959
+ </View>
960
+ );
961
+ }
962
+
963
+ const styles = StyleSheet.create({
964
+ center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
965
+ errorText: { color: '#d32f2f', textAlign: 'center', marginBottom: 12 },
966
+ retryButton: { backgroundColor: '#1976d2', borderRadius: 8, paddingHorizontal: 20, paddingVertical: 10 },
967
+ retryText: { color: '#fff', fontWeight: '600' },
968
+ });
969
+ ```
970
+
971
+ ```typescript
972
+ // src/shared/components/EmptyStateView.tsx
973
+ import React from 'react';
974
+ import { View, Text, StyleSheet } from 'react-native';
975
+
976
+ export function EmptyStateView({ message }: { message: string }) {
977
+ return (
978
+ <View style={styles.center}>
979
+ <Text style={styles.emptyText}>{message}</Text>
980
+ </View>
981
+ );
982
+ }
983
+
984
+ const styles = StyleSheet.create({
985
+ center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
986
+ emptyText: { color: '#757575', textAlign: 'center' },
987
+ });
988
+ ```
989
+
990
+ ---
991
+
992
+ ## 13. Maestro E2E Flow
993
+
994
+ Before this flow, sign into an isolated test account and seed a todo with title
995
+ Buy milk and a known ID. Supply that ID as MAESTRO_TODO_ID. Keep the signed-in
996
+ session: clearState would return the example navigator to Login. This example
997
+ covers existing row actions; add/login flows belong to their actual screens.
998
+
999
+ ```yaml
1000
+ appId: com.example.app
1001
+ ---
1002
+ - launchApp:
1003
+ clearState: false
1004
+ - assertVisible: "My Todos"
1005
+ - assertVisible: "Buy milk"
1006
+ - tapOn:
1007
+ id: "todo-checkbox-${MAESTRO_TODO_ID}"
1008
+ - tapOn: "Buy milk"
1009
+ - assertVisible: "Todo Detail"
1010
+ - back
1011
+ - tapOn:
1012
+ id: "todo-delete-${MAESTRO_TODO_ID}"
1013
+ - assertNotVisible: "Buy milk"
1014
+ ```