@mmerterden/multi-agent-pipeline 20.35.0 → 20.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -5
  3. package/README.tr.md +6 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +4 -4
  7. package/install/catalog-history.json +1 -1
  8. package/manifest.json +210 -136
  9. package/package.json +5 -1
  10. package/pipeline/commands/multi-agent/SKILL.md +33 -154
  11. package/pipeline/commands/multi-agent/analysis/SKILL.md +14 -68
  12. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +5 -8
  13. package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +38 -109
  14. package/pipeline/commands/multi-agent/channels/SKILL.md +38 -167
  15. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +37 -144
  16. package/pipeline/commands/multi-agent/design-check/SKILL.md +27 -131
  17. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +22 -48
  18. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +37 -180
  19. package/pipeline/commands/multi-agent/jira/SKILL.md +16 -46
  20. package/pipeline/commands/multi-agent/office-pair/SKILL.md +66 -0
  21. package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
  22. package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -184
  23. package/pipeline/commands/multi-agent/resume/SKILL.md +30 -136
  24. package/pipeline/commands/multi-agent/review/SKILL.md +24 -199
  25. package/pipeline/commands/multi-agent/setup/SKILL.md +25 -181
  26. package/pipeline/commands/multi-agent/stack/SKILL.md +9 -53
  27. package/pipeline/commands/multi-agent/status/SKILL.md +13 -2
  28. package/pipeline/commands/multi-agent/store-ready/SKILL.md +27 -226
  29. package/pipeline/commands/multi-agent/sync/SKILL.md +38 -167
  30. package/pipeline/commands/multi-agent/test-gen/SKILL.md +2 -4
  31. package/pipeline/commands/multi-agent/update/SKILL.md +18 -60
  32. package/pipeline/contract/CHANGELOG.md +9 -0
  33. package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
  34. package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
  35. package/pipeline/contract/fixtures/error-unsigned.json +1 -1
  36. package/pipeline/contract/fixtures/runs-awaiting-question.json +1 -1
  37. package/pipeline/contract/fixtures/runs-empty.json +1 -1
  38. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  39. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  40. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  41. package/pipeline/contract/fixtures/runs-running.json +1 -1
  42. package/pipeline/contract/manifest.json +5 -5
  43. package/pipeline/contract/types/index.d.ts +3 -2
  44. package/pipeline/lib/qr-code.mjs +26 -0
  45. package/pipeline/multi-agent-refs/autopilot-on/details.md +148 -0
  46. package/pipeline/multi-agent-refs/channels/generator.md +14 -0
  47. package/pipeline/multi-agent-refs/channels/preview.md +13 -0
  48. package/pipeline/multi-agent-refs/channels/reference.md +46 -0
  49. package/pipeline/multi-agent-refs/complaint-analysis/phases.md +159 -0
  50. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
  51. package/pipeline/multi-agent-refs/design-check/export.md +1 -0
  52. package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
  53. package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
  54. package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
  55. package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
  56. package/pipeline/multi-agent-refs/help/en.md +1 -0
  57. package/pipeline/multi-agent-refs/help/tr.md +1 -0
  58. package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
  59. package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
  60. package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
  61. package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
  62. package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
  63. package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
  64. package/pipeline/multi-agent-refs/resume/details.md +115 -2
  65. package/pipeline/multi-agent-refs/review/inputs.md +33 -1
  66. package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
  67. package/pipeline/multi-agent-refs/review/panel.md +75 -0
  68. package/pipeline/multi-agent-refs/review/valven.md +5 -1
  69. package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
  70. package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
  71. package/pipeline/multi-agent-refs/stack/background.md +72 -0
  72. package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
  73. package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
  74. package/pipeline/multi-agent-refs/sync/office.md +10 -0
  75. package/pipeline/multi-agent-refs/sync/overview.md +61 -0
  76. package/pipeline/multi-agent-refs/sync/repo.md +91 -0
  77. package/pipeline/multi-agent-refs/update/details.md +83 -0
  78. package/pipeline/schemas/runs-index.schema.json +2 -1
  79. package/pipeline/scripts/contract-server.mjs +1 -1
  80. package/pipeline/scripts/office-pair.mjs +40 -9
  81. package/pipeline/scripts/runs-index.mjs +34 -7
  82. package/pipeline/scripts/status-overview.mjs +401 -0
  83. package/pipeline/skills/.skill-manifest.json +69 -65
  84. package/pipeline/skills/.skills-index.json +15 -4
  85. package/pipeline/skills/shared/README.md +4 -3
  86. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +8 -71
  87. package/pipeline/skills/shared/core/apple-archive-compliance/references/examples.md +67 -0
  88. package/pipeline/skills/shared/core/apple-archive-compliance/references/version-history.md +8 -0
  89. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +11 -73
  90. package/pipeline/skills/shared/core/google-play-compliance/references/examples.md +69 -0
  91. package/pipeline/skills/shared/core/google-play-compliance/references/notes.md +7 -0
  92. package/pipeline/skills/shared/core/google-play-compliance/references/version-history.md +5 -0
  93. package/pipeline/skills/shared/core/multi-agent/SKILL.md +39 -262
  94. package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +35 -73
  95. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +37 -337
  96. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +27 -122
  97. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +8 -340
  98. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +37 -180
  99. package/pipeline/skills/shared/core/multi-agent-office-pair/SKILL.md +19 -0
  100. package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +1 -1
  101. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -184
  102. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +14 -82
  103. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +25 -277
  104. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +13 -2
  105. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +75 -219
  106. package/pipeline/skills/shared/external/alarmkit/SKILL.md +11 -162
  107. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-code.md +257 -0
  108. package/pipeline/skills/shared/external/android-architecture/SKILL.md +7 -58
  109. package/pipeline/skills/shared/external/android-architecture/references/code-examples.md +68 -0
  110. package/pipeline/skills/shared/external/app-intents/SKILL.md +3 -45
  111. package/pipeline/skills/shared/external/app-intents/references/code-examples.md +54 -0
  112. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +5 -81
  113. package/pipeline/skills/shared/external/app-store-optimization/references/engagement-and-localization.md +96 -0
  114. package/pipeline/skills/shared/external/app-store-review/SKILL.md +6 -112
  115. package/pipeline/skills/shared/external/app-store-review/references/monetization-and-submission.md +138 -0
  116. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +10 -117
  117. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models-code.md +186 -0
  118. package/pipeline/skills/shared/external/authentication/SKILL.md +3 -50
  119. package/pipeline/skills/shared/external/authentication/references/code-examples.md +59 -0
  120. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +3 -84
  121. package/pipeline/skills/shared/external/callkit-voip/references/code-examples.md +93 -0
  122. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +7 -263
  123. package/pipeline/skills/shared/external/ci-cd-pipelines/references/pipeline-examples.md +281 -0
  124. package/pipeline/skills/shared/external/clean-code/SKILL.md +8 -41
  125. package/pipeline/skills/shared/external/clean-code/references/code-examples.md +56 -0
  126. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +8 -146
  127. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-code.md +214 -0
  128. package/pipeline/skills/shared/external/compose-components/SKILL.md +6 -80
  129. package/pipeline/skills/shared/external/compose-components/references/code-examples.md +89 -0
  130. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +5 -64
  131. package/pipeline/skills/shared/external/compose-navigation/references/code-examples.md +70 -0
  132. package/pipeline/skills/shared/external/core-data/SKILL.md +9 -157
  133. package/pipeline/skills/shared/external/core-data/references/core-data-code.md +221 -0
  134. package/pipeline/skills/shared/external/core-nfc/SKILL.md +3 -89
  135. package/pipeline/skills/shared/external/core-nfc/references/code-examples.md +98 -0
  136. package/pipeline/skills/shared/external/coreml/SKILL.md +17 -133
  137. package/pipeline/skills/shared/external/coreml/references/coreml-code.md +208 -0
  138. package/pipeline/skills/shared/external/css-modern/SKILL.md +8 -131
  139. package/pipeline/skills/shared/external/css-modern/references/code-examples.md +142 -0
  140. package/pipeline/skills/shared/external/database-patterns/SKILL.md +6 -103
  141. package/pipeline/skills/shared/external/database-patterns/references/orm-examples.md +112 -0
  142. package/pipeline/skills/shared/external/html-semantic/SKILL.md +7 -48
  143. package/pipeline/skills/shared/external/html-semantic/references/code-examples.md +54 -0
  144. package/pipeline/skills/shared/external/humanizer/SKILL.md +6 -19
  145. package/pipeline/skills/shared/external/humanizer/references/voice.md +24 -0
  146. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +13 -170
  147. package/pipeline/skills/shared/external/ios-accessibility/references/assistive-tech-review.md +224 -0
  148. package/pipeline/skills/shared/external/ios-localization/SKILL.md +2 -24
  149. package/pipeline/skills/shared/external/ios-localization/references/code-examples.md +30 -0
  150. package/pipeline/skills/shared/external/ios-networking/SKILL.md +3 -58
  151. package/pipeline/skills/shared/external/ios-networking/references/code-examples.md +67 -0
  152. package/pipeline/skills/shared/external/ios-security/SKILL.md +4 -64
  153. package/pipeline/skills/shared/external/ios-security/references/code-examples.md +74 -0
  154. package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -90
  155. package/pipeline/skills/shared/external/live-activities/references/code-examples.md +108 -0
  156. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -23
  157. package/pipeline/skills/shared/external/localization-reuse-map/references/project-settings.md +22 -0
  158. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +5 -95
  159. package/pipeline/skills/shared/external/mapkit-location/references/code-examples.md +117 -0
  160. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +7 -106
  161. package/pipeline/skills/shared/external/nextjs-app-router/references/app-router-examples.md +124 -0
  162. package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -31
  163. package/pipeline/skills/shared/external/permissionkit/references/code-examples.md +37 -0
  164. package/pipeline/skills/shared/external/play-store-review/SKILL.md +6 -74
  165. package/pipeline/skills/shared/external/play-store-review/references/signing-and-policies.md +83 -0
  166. package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -125
  167. package/pipeline/skills/shared/external/push-notifications/references/notification-code.md +176 -0
  168. package/pipeline/skills/shared/external/python-patterns/SKILL.md +7 -158
  169. package/pipeline/skills/shared/external/python-patterns/references/python-examples.md +176 -0
  170. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +7 -157
  171. package/pipeline/skills/shared/external/rest-api-design/references/api-examples.md +175 -0
  172. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +11 -230
  173. package/pipeline/skills/shared/external/swift-architecture/references/pattern-examples.md +295 -0
  174. package/pipeline/skills/shared/external/swift-codable/SKILL.md +17 -149
  175. package/pipeline/skills/shared/external/swift-codable/references/codable-recipes.md +207 -0
  176. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +12 -99
  177. package/pipeline/skills/shared/external/swift-concurrency/references/code-examples.md +100 -0
  178. package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-additions.md +18 -0
  179. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +9 -78
  180. package/pipeline/skills/shared/external/swift-formatstyle/references/formatstyle-examples.md +113 -0
  181. package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -56
  182. package/pipeline/skills/shared/external/swift-language/references/code-examples.md +65 -0
  183. package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +1 -0
  184. package/pipeline/skills/shared/external/swiftdata/SKILL.md +6 -122
  185. package/pipeline/skills/shared/external/swiftdata/references/code-examples.md +140 -0
  186. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +12 -139
  187. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/interop-code.md +198 -0
  188. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +9 -81
  189. package/pipeline/skills/shared/external/tailwind-css/references/code-examples.md +91 -0
  190. package/pipeline/skills/shared/external/testing-backend/SKILL.md +7 -207
  191. package/pipeline/skills/shared/external/testing-backend/references/testing-examples.md +225 -0
  192. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +8 -89
  193. package/pipeline/skills/shared/external/typescript-patterns/references/code-examples.md +100 -0
  194. package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -20
  195. package/pipeline/skills/shared/external/vision-framework/references/code-examples.md +26 -0
  196. package/pipeline/skills/shared/external/vue-composition/SKILL.md +7 -169
  197. package/pipeline/skills/shared/external/vue-composition/references/vue-examples.md +187 -0
  198. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +7 -174
  199. package/pipeline/skills/shared/external/web-accessibility/references/a11y-examples.md +192 -0
  200. package/pipeline/skills/shared/external/web-performance/SKILL.md +8 -66
  201. package/pipeline/skills/shared/external/web-performance/references/code-examples.md +77 -0
  202. package/pipeline/skills/shared/external/web-testing/SKILL.md +11 -186
  203. package/pipeline/skills/shared/external/web-testing/references/testing-examples.md +213 -0
  204. package/pipeline/skills/skills-index.md +2 -1
  205. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/format-and-output.md +0 -0
  206. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/publish-and-snapshot.md +0 -0
  207. /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/sources-and-recipes.md +0 -0
@@ -132,28 +132,7 @@ A SwiftUI app still needs an app delegate for this: the device token is only
132
132
  delivered to `UIApplicationDelegate` callbacks. Attach one with
133
133
  `@UIApplicationDelegateAdaptor`.
134
134
 
135
- ```swift
136
- final class AppDelegate: NSObject, UIApplicationDelegate {
137
- func application(
138
- _ application: UIApplication,
139
- didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
140
- ) -> Bool {
141
- UNUserNotificationCenter.current().delegate = InboxNotificationDelegate.shared
142
- return true
143
- }
144
-
145
- func application(_ application: UIApplication,
146
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
147
- let hex = deviceToken.reduce(into: "") { $0 += String(format: "%02x", $1) }
148
- Task { await DeviceTokenUploader.shared.send(hex) }
149
- }
150
-
151
- func application(_ application: UIApplication,
152
- didFailToRegisterForRemoteNotificationsWithError error: Error) {
153
- // Expected on Simulator: it can show simulated pushes but never gets an APNs token.
154
- }
155
- }
156
- ```
135
+ Code for this section: [Notification code](references/notification-code.md).
157
136
 
158
137
  ### Order of calls
159
138
 
@@ -201,41 +180,11 @@ content.threadIdentifier = "garden" // groups these together in Notification C
201
180
 
202
181
  ### Triggers
203
182
 
204
- ```swift
205
- // Once, 15 minutes from now. A repeating interval must be at least 60 seconds.
206
- let soon = UNTimeIntervalNotificationTrigger(timeInterval: 900, repeats: false)
207
-
208
- // Every day at 07:30.
209
- var morning = DateComponents()
210
- morning.hour = 7
211
- morning.minute = 30
212
- let daily = UNCalendarNotificationTrigger(dateMatching: morning, repeats: true)
213
-
214
- // On arriving at a place. Needs at least When In Use location permission.
215
- let gym = CLCircularRegion(
216
- center: CLLocationCoordinate2D(latitude: 52.37, longitude: 4.89),
217
- radius: 150,
218
- identifier: "gym"
219
- )
220
- gym.notifyOnEntry = true
221
- gym.notifyOnExit = false
222
- let arrival = UNLocationNotificationTrigger(region: gym, repeats: false)
223
- ```
183
+ Code for this section: [Notification code](references/notification-code.md).
224
184
 
225
185
  ### Scheduling and cleanup
226
186
 
227
- ```swift
228
- let center = UNUserNotificationCenter.current()
229
- let request = UNNotificationRequest(identifier: "water-basil", content: content, trigger: daily)
230
- try await center.add(request)
231
-
232
- let waiting = await center.pendingNotificationRequests()
233
-
234
- center.removePendingNotificationRequests(withIdentifiers: ["water-basil"])
235
- center.removeAllPendingNotificationRequests()
236
- center.removeDeliveredNotifications(withIdentifiers: ["water-basil"])
237
- center.removeAllDeliveredNotifications()
238
- ```
187
+ Code for this section: [Notification code](references/notification-code.md).
239
188
 
240
189
  ## Remote Payloads
241
190
 
@@ -321,38 +270,6 @@ Let the device pick the language by sending keys instead of text:
321
270
  Install `UNUserNotificationCenter.current().delegate` before launch finishes,
322
271
  from `App.init` or from `application(_:didFinishLaunchingWithOptions:)`.
323
272
 
324
- ```swift
325
- @MainActor
326
- final class InboxNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
327
- static let shared = InboxNotificationDelegate()
328
-
329
- nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
330
- willPresent notification: UNNotification) async
331
- -> UNNotificationPresentationOptions {
332
- [.banner, .sound, .badge]
333
- }
334
-
335
- nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
336
- didReceive response: UNNotificationResponse) async {
337
- let action = response.actionIdentifier
338
- let threadID = response.notification.request.content.userInfo["threadID"] as? String
339
- let typed = (response as? UNTextInputNotificationResponse)?.userText
340
- await route(action: action, threadID: threadID, typed: typed)
341
- }
342
-
343
- private func route(action: String, threadID: String?, typed: String?) async {
344
- switch action {
345
- case UNNotificationDefaultActionIdentifier:
346
- AppRouter.shared.openThread(threadID) // tapped the notification
347
- case UNNotificationDismissActionIdentifier:
348
- break // swiped away
349
- default:
350
- await handleChatAction(action, threadID: threadID, typed: typed)
351
- }
352
- }
353
- }
354
- ```
355
-
356
273
  The system does not promise to call these methods on the main thread, and
357
274
  `UNNotification` and `UNNotificationResponse` are not `Sendable`. Mark the
358
275
  protocol methods `nonisolated`, copy the plain values you need out of the
@@ -362,6 +279,8 @@ actor.
362
279
  Leave out `willPresent` and anything that arrives while your app is frontmost
363
280
  is dropped without a banner.
364
281
 
282
+ Code for this section: [Notification code](references/notification-code.md).
283
+
365
284
  ### Deep links from a tap
366
285
 
367
286
  Keep a single `@Observable @MainActor` router with an optional
@@ -374,29 +293,6 @@ it back to `nil`. A version that also switches tabs lives in the
374
293
 
375
294
  Register categories at launch, before any notification can arrive.
376
295
 
377
- ```swift
378
- let reply = UNTextInputNotificationAction(
379
- identifier: "REPLY",
380
- title: "Reply",
381
- options: [],
382
- textInputButtonTitle: "Send",
383
- textInputPlaceholder: "Message"
384
- )
385
- let archive = UNNotificationAction(identifier: "ARCHIVE", title: "Archive", options: [.destructive])
386
- let open = UNNotificationAction(identifier: "OPEN", title: "Open", options: [.foreground])
387
- let approve = UNNotificationAction(identifier: "APPROVE", title: "Approve", options: [.authenticationRequired])
388
-
389
- let chat = UNNotificationCategory(
390
- identifier: "CHAT",
391
- actions: [reply, archive],
392
- intentIdentifiers: [],
393
- options: [.customDismissAction] // dismissal also reaches didReceive
394
- )
395
- let request = UNNotificationCategory(identifier: "EXPENSE", actions: [approve, open], intentIdentifiers: [])
396
-
397
- UNUserNotificationCenter.current().setNotificationCategories([chat, request])
398
- ```
399
-
400
296
  | Option | Effect |
401
297
  |--------|--------|
402
298
  | `.authenticationRequired` | Runs only after the device is unlocked |
@@ -404,23 +300,9 @@ UNUserNotificationCenter.current().setNotificationCategories([chat, request])
404
300
  | `.foreground` | Tapping it opens the app |
405
301
 
406
302
  A text action's response arrives as a `UNTextInputNotificationResponse`; its
407
- `userText` is what the delegate above passes on as `typed`:
303
+ `userText` is what the delegate passes on as `typed`.
408
304
 
409
- ```swift
410
- @MainActor
411
- func handleChatAction(_ action: String, threadID: String?, typed: String?) async {
412
- guard let threadID else { return }
413
- switch action {
414
- case "REPLY":
415
- guard let typed, !typed.isEmpty else { return }
416
- await ChatService.shared.send(typed, to: threadID)
417
- case "ARCHIVE":
418
- await ChatService.shared.archive(threadID)
419
- default:
420
- break
421
- }
422
- }
423
- ```
305
+ Code for this section: [Notification code](references/notification-code.md).
424
306
 
425
307
  ## Grouping
426
308
 
@@ -470,6 +352,7 @@ let chat = UNNotificationCategory(
470
352
 
471
353
  ## Apple Documentation
472
354
 
355
+ - [Notification code](references/notification-code.md): open for the code behind APNs registration, local triggers, the delegate and actions and categories.
473
356
  - [APNs registration](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)
474
357
  - [Requesting permission](https://developer.apple.com/documentation/usernotifications/asking-permission-to-use-notifications)
475
358
  - [Building a remote payload](https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification)
@@ -0,0 +1,176 @@
1
+ # Notification code
2
+
3
+ <!-- toc -->
4
+ - [Registering with APNs](#registering-with-apns)
5
+ - [Triggers](#triggers)
6
+ - [Scheduling and cleanup](#scheduling-and-cleanup)
7
+ - [The delegate](#the-delegate)
8
+ - [Actions and Categories](#actions-and-categories)
9
+ <!-- /toc -->
10
+
11
+ Code for APNs registration, local triggers and scheduling, the notification delegate and actions and categories. SKILL.md keeps the rules and review guidance.
12
+
13
+ ## Registering with APNs
14
+
15
+ A SwiftUI app still needs an app delegate for this: the device token is only
16
+ delivered to `UIApplicationDelegate` callbacks. Attach one with
17
+ `@UIApplicationDelegateAdaptor`.
18
+
19
+ ```swift
20
+ final class AppDelegate: NSObject, UIApplicationDelegate {
21
+ func application(
22
+ _ application: UIApplication,
23
+ didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
24
+ ) -> Bool {
25
+ UNUserNotificationCenter.current().delegate = InboxNotificationDelegate.shared
26
+ return true
27
+ }
28
+
29
+ func application(_ application: UIApplication,
30
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
31
+ let hex = deviceToken.reduce(into: "") { $0 += String(format: "%02x", $1) }
32
+ Task { await DeviceTokenUploader.shared.send(hex) }
33
+ }
34
+
35
+ func application(_ application: UIApplication,
36
+ didFailToRegisterForRemoteNotificationsWithError error: Error) {
37
+ // Expected on Simulator: it can show simulated pushes but never gets an APNs token.
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### Triggers
43
+
44
+ ```swift
45
+ // Once, 15 minutes from now. A repeating interval must be at least 60 seconds.
46
+ let soon = UNTimeIntervalNotificationTrigger(timeInterval: 900, repeats: false)
47
+
48
+ // Every day at 07:30.
49
+ var morning = DateComponents()
50
+ morning.hour = 7
51
+ morning.minute = 30
52
+ let daily = UNCalendarNotificationTrigger(dateMatching: morning, repeats: true)
53
+
54
+ // On arriving at a place. Needs at least When In Use location permission.
55
+ let gym = CLCircularRegion(
56
+ center: CLLocationCoordinate2D(latitude: 52.37, longitude: 4.89),
57
+ radius: 150,
58
+ identifier: "gym"
59
+ )
60
+ gym.notifyOnEntry = true
61
+ gym.notifyOnExit = false
62
+ let arrival = UNLocationNotificationTrigger(region: gym, repeats: false)
63
+ ```
64
+
65
+ ### Scheduling and cleanup
66
+
67
+ ```swift
68
+ let center = UNUserNotificationCenter.current()
69
+ let request = UNNotificationRequest(identifier: "water-basil", content: content, trigger: daily)
70
+ try await center.add(request)
71
+
72
+ let waiting = await center.pendingNotificationRequests()
73
+
74
+ center.removePendingNotificationRequests(withIdentifiers: ["water-basil"])
75
+ center.removeAllPendingNotificationRequests()
76
+ center.removeDeliveredNotifications(withIdentifiers: ["water-basil"])
77
+ center.removeAllDeliveredNotifications()
78
+ ```
79
+
80
+ ### The delegate
81
+
82
+ Install `UNUserNotificationCenter.current().delegate` before launch finishes,
83
+ from `App.init` or from `application(_:didFinishLaunchingWithOptions:)`.
84
+
85
+ ```swift
86
+ @MainActor
87
+ final class InboxNotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
88
+ static let shared = InboxNotificationDelegate()
89
+
90
+ nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
91
+ willPresent notification: UNNotification) async
92
+ -> UNNotificationPresentationOptions {
93
+ [.banner, .sound, .badge]
94
+ }
95
+
96
+ nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
97
+ didReceive response: UNNotificationResponse) async {
98
+ let action = response.actionIdentifier
99
+ let threadID = response.notification.request.content.userInfo["threadID"] as? String
100
+ let typed = (response as? UNTextInputNotificationResponse)?.userText
101
+ await route(action: action, threadID: threadID, typed: typed)
102
+ }
103
+
104
+ private func route(action: String, threadID: String?, typed: String?) async {
105
+ switch action {
106
+ case UNNotificationDefaultActionIdentifier:
107
+ AppRouter.shared.openThread(threadID) // tapped the notification
108
+ case UNNotificationDismissActionIdentifier:
109
+ break // swiped away
110
+ default:
111
+ await handleChatAction(action, threadID: threadID, typed: typed)
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ The system does not promise to call these methods on the main thread, and
118
+ `UNNotification` and `UNNotificationResponse` are not `Sendable`. Mark the
119
+ protocol methods `nonisolated`, copy the plain values you need out of the
120
+ response (all payload data sits in `content.userInfo`), then hop to the main
121
+ actor.
122
+
123
+ Leave out `willPresent` and anything that arrives while your app is frontmost
124
+ is dropped without a banner.
125
+
126
+ ## Actions and Categories
127
+
128
+ Register categories at launch, before any notification can arrive.
129
+
130
+ ```swift
131
+ let reply = UNTextInputNotificationAction(
132
+ identifier: "REPLY",
133
+ title: "Reply",
134
+ options: [],
135
+ textInputButtonTitle: "Send",
136
+ textInputPlaceholder: "Message"
137
+ )
138
+ let archive = UNNotificationAction(identifier: "ARCHIVE", title: "Archive", options: [.destructive])
139
+ let open = UNNotificationAction(identifier: "OPEN", title: "Open", options: [.foreground])
140
+ let approve = UNNotificationAction(identifier: "APPROVE", title: "Approve", options: [.authenticationRequired])
141
+
142
+ let chat = UNNotificationCategory(
143
+ identifier: "CHAT",
144
+ actions: [reply, archive],
145
+ intentIdentifiers: [],
146
+ options: [.customDismissAction] // dismissal also reaches didReceive
147
+ )
148
+ let request = UNNotificationCategory(identifier: "EXPENSE", actions: [approve, open], intentIdentifiers: [])
149
+
150
+ UNUserNotificationCenter.current().setNotificationCategories([chat, request])
151
+ ```
152
+
153
+ | Option | Effect |
154
+ |--------|--------|
155
+ | `.authenticationRequired` | Runs only after the device is unlocked |
156
+ | `.destructive` | Shown in red; for delete or remove |
157
+ | `.foreground` | Tapping it opens the app |
158
+
159
+ A text action's response arrives as a `UNTextInputNotificationResponse`; its
160
+ `userText` is what the delegate above passes on as `typed`:
161
+
162
+ ```swift
163
+ @MainActor
164
+ func handleChatAction(_ action: String, threadID: String?, typed: String?) async {
165
+ guard let threadID else { return }
166
+ switch action {
167
+ case "REPLY":
168
+ guard let typed, !typed.isEmpty else { return }
169
+ await ChatService.shared.send(typed, to: threadID)
170
+ case "ARCHIVE":
171
+ await ChatService.shared.archive(threadID)
172
+ default:
173
+ break
174
+ }
175
+ }
176
+ ```
@@ -105,117 +105,11 @@ user_set: set[User] = {user}
105
105
 
106
106
  ## Pydantic v2
107
107
 
108
- ```python
109
- from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
110
- from datetime import datetime
111
-
112
- class CreateUserRequest(BaseModel):
113
- model_config = ConfigDict(str_strip_whitespace=True, frozen=True)
114
-
115
- name: str = Field(min_length=1, max_length=100)
116
- email: str = Field(pattern=r'^[\w.-]+@[\w.-]+\.\w+$')
117
- age: int = Field(ge=0, le=150)
118
- role: Role = Role.USER
119
- tags: list[str] = Field(default_factory=list, max_length=10)
120
-
121
- @field_validator('email')
122
- @classmethod
123
- def normalize_email(cls, v: str) -> str:
124
- return v.lower()
125
-
126
- @model_validator(mode='after')
127
- def validate_admin_age(self) -> 'CreateUserRequest':
128
- if self.role == Role.ADMIN and self.age < 18:
129
- raise ValueError("Admin must be 18 or older")
130
- return self
131
-
132
- class UserResponse(BaseModel):
133
- id: str
134
- name: str
135
- email: str
136
- role: Role
137
- created_at: datetime
138
-
139
- model_config = ConfigDict(from_attributes=True)
140
-
141
- # Validation
142
- try:
143
- user = CreateUserRequest(name="Alice", email="ALICE@example.com", age=25)
144
- print(user.email) # "alice@example.com" (normalized)
145
- except ValidationError as e:
146
- print(e.errors())
147
-
148
- # Serialization
149
- user_dict = user.model_dump()
150
- user_json = user.model_dump_json()
151
-
152
- # From ORM object
153
- db_user = db.query(UserModel).first()
154
- response = UserResponse.model_validate(db_user)
155
- ```
108
+ Code for this section: [Python pattern examples](references/python-examples.md).
156
109
 
157
110
  ## Async/Await with asyncio
158
111
 
159
- ```python
160
- import asyncio
161
- from collections.abc import AsyncIterator
162
- import httpx
163
-
164
- # Async function
165
- async def fetch_user(user_id: str) -> dict:
166
- async with httpx.AsyncClient() as client:
167
- response = await client.get(f"https://api.example.com/users/{user_id}")
168
- response.raise_for_status()
169
- return response.json()
170
-
171
- # Parallel execution
172
- async def fetch_all_users(user_ids: list[str]) -> list[dict]:
173
- async with httpx.AsyncClient() as client:
174
- tasks = [
175
- client.get(f"https://api.example.com/users/{uid}")
176
- for uid in user_ids
177
- ]
178
- responses = await asyncio.gather(*tasks, return_exceptions=True)
179
- return [
180
- r.json() for r in responses
181
- if not isinstance(r, Exception) and r.status_code == 200
182
- ]
183
-
184
- # Async context manager
185
- from contextlib import asynccontextmanager
186
-
187
- @asynccontextmanager
188
- async def get_db_session() -> AsyncIterator[AsyncSession]:
189
- session = AsyncSession(engine)
190
- try:
191
- yield session
192
- await session.commit()
193
- except Exception:
194
- await session.rollback()
195
- raise
196
- finally:
197
- await session.close()
198
-
199
- # Async generator
200
- async def stream_events(url: str) -> AsyncIterator[dict]:
201
- async with httpx.AsyncClient() as client:
202
- async with client.stream("GET", url) as response:
203
- async for line in response.aiter_lines():
204
- if line.startswith("data: "):
205
- yield json.loads(line[6:])
206
-
207
- # Semaphore for rate limiting
208
- async def fetch_with_limit(urls: list[str], max_concurrent: int = 10) -> list[str]:
209
- semaphore = asyncio.Semaphore(max_concurrent)
210
-
211
- async def fetch_one(url: str) -> str:
212
- async with semaphore:
213
- async with httpx.AsyncClient() as client:
214
- resp = await client.get(url)
215
- return resp.text
216
-
217
- return await asyncio.gather(*[fetch_one(url) for url in urls])
218
- ```
112
+ Code for this section: [Python pattern examples](references/python-examples.md).
219
113
 
220
114
  ## Context Managers
221
115
 
@@ -301,56 +195,7 @@ active_users = filter_active(parse_csv(read_lines("users.csv")))
301
195
 
302
196
  ## Pattern Matching (match/case)
303
197
 
304
- ```python
305
- from dataclasses import dataclass
306
-
307
- @dataclass
308
- class Point:
309
- x: float
310
- y: float
311
-
312
- def describe_point(point: Point) -> str:
313
- match point:
314
- case Point(x=0, y=0):
315
- return "origin"
316
- case Point(x=0, y=y):
317
- return f"on y-axis at y={y}"
318
- case Point(x=x, y=0):
319
- return f"on x-axis at x={x}"
320
- case Point(x=x, y=y) if x == y:
321
- return f"on diagonal at {x}"
322
- case _:
323
- return f"at ({point.x}, {point.y})"
324
-
325
- # Matching on type and structure
326
- def handle_response(response: dict) -> str:
327
- match response:
328
- case {"status": 200, "data": data}:
329
- return f"Success: {data}"
330
- case {"status": 404}:
331
- return "Not found"
332
- case {"status": status} if 500 <= status < 600:
333
- return f"Server error: {status}"
334
- case {"error": str(message)}:
335
- return f"Error: {message}"
336
- case _:
337
- return "Unknown response"
338
-
339
- # Matching sequences
340
- def process_command(command: list[str]) -> None:
341
- match command:
342
- case ["quit" | "exit"]:
343
- sys.exit(0)
344
- case ["go", direction]:
345
- move(direction)
346
- case ["pick", "up", item]:
347
- pick_up(item)
348
- case ["drop", *items] if items:
349
- for item in items:
350
- drop(item)
351
- case _:
352
- print(f"Unknown command: {' '.join(command)}")
353
- ```
198
+ Code for this section: [Python pattern examples](references/python-examples.md).
354
199
 
355
200
  ## Do's
356
201
 
@@ -384,3 +229,7 @@ def process_command(command: list[str]) -> None:
384
229
  | Type checker error with Protocol | Missing method or wrong signature | Match Protocol method signatures exactly |
385
230
  | Slow async code | Sequential awaits instead of parallel | Use `asyncio.gather()` or `asyncio.TaskGroup` for concurrency |
386
231
  | "TypeError: unhashable type" | Using mutable object as dict key | Use `frozen=True` dataclass or tuple instead |
232
+
233
+ ## References
234
+
235
+ - [Python pattern examples](references/python-examples.md): open when writing Pydantic v2 models, asyncio concurrency or `match`/`case` code.