@mrciphersmith/keryx 0.3.3 → 0.3.6

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 (140) hide show
  1. package/dist/cli.js +996 -366
  2. package/docs/README.md +2 -0
  3. package/package.json +1 -1
  4. package/src/gdskills/bundled/install-manifest.json +520 -48
  5. package/src/gdskills/bundled/stacks/csharp-dotnet/agent-refs.json +4 -0
  6. package/src/gdskills/bundled/stacks/csharp-dotnet/governance/eval.json +1881 -0
  7. package/src/gdskills/bundled/stacks/csharp-dotnet/governance/scout.json +33 -0
  8. package/src/gdskills/bundled/stacks/csharp-dotnet/pack.json +38 -0
  9. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/coding-style.mdc +100 -0
  10. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/patterns.mdc +107 -0
  11. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/security.mdc +86 -0
  12. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/testing.mdc +89 -0
  13. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/SKILL.md +143 -0
  14. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/evals.json +77 -0
  15. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/SKILL.md +121 -0
  16. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/evals.json +77 -0
  17. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/SKILL.md +134 -0
  18. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/evals.json +76 -0
  19. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/SKILL.md +130 -0
  20. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/evals.json +77 -0
  21. package/src/gdskills/bundled/stacks/django/agent-refs.json +3 -0
  22. package/src/gdskills/bundled/stacks/django/governance/eval.json +1763 -0
  23. package/src/gdskills/bundled/stacks/django/governance/scout.json +40 -0
  24. package/src/gdskills/bundled/stacks/django/pack.json +43 -0
  25. package/src/gdskills/bundled/stacks/django/rules/coding-style.mdc +80 -0
  26. package/src/gdskills/bundled/stacks/django/rules/patterns.mdc +92 -0
  27. package/src/gdskills/bundled/stacks/django/rules/security.mdc +92 -0
  28. package/src/gdskills/bundled/stacks/django/rules/testing.mdc +89 -0
  29. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/SKILL.md +149 -0
  30. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/evals.json +49 -0
  31. package/src/gdskills/bundled/stacks/django/skills/django-code-review/SKILL.md +137 -0
  32. package/src/gdskills/bundled/stacks/django/skills/django-code-review/evals.json +48 -0
  33. package/src/gdskills/bundled/stacks/django/skills/django-implementation/SKILL.md +147 -0
  34. package/src/gdskills/bundled/stacks/django/skills/django-implementation/evals.json +75 -0
  35. package/src/gdskills/bundled/stacks/django/skills/django-migrate/SKILL.md +166 -0
  36. package/src/gdskills/bundled/stacks/django/skills/django-migrate/evals.json +49 -0
  37. package/src/gdskills/bundled/stacks/django/skills/django-testing/SKILL.md +130 -0
  38. package/src/gdskills/bundled/stacks/django/skills/django-testing/evals.json +48 -0
  39. package/src/gdskills/bundled/stacks/fastapi/agent-refs.json +3 -0
  40. package/src/gdskills/bundled/stacks/fastapi/governance/eval.json +1777 -0
  41. package/src/gdskills/bundled/stacks/fastapi/governance/scout.json +34 -0
  42. package/src/gdskills/bundled/stacks/fastapi/pack.json +43 -0
  43. package/src/gdskills/bundled/stacks/fastapi/rules/coding-style.mdc +68 -0
  44. package/src/gdskills/bundled/stacks/fastapi/rules/patterns.mdc +108 -0
  45. package/src/gdskills/bundled/stacks/fastapi/rules/security.mdc +99 -0
  46. package/src/gdskills/bundled/stacks/fastapi/rules/testing.mdc +85 -0
  47. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/SKILL.md +157 -0
  48. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/evals.json +76 -0
  49. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/SKILL.md +150 -0
  50. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/evals.json +74 -0
  51. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/SKILL.md +158 -0
  52. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/evals.json +75 -0
  53. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/SKILL.md +146 -0
  54. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/evals.json +74 -0
  55. package/src/gdskills/bundled/stacks/flutter-dart/agent-refs.json +4 -0
  56. package/src/gdskills/bundled/stacks/flutter-dart/governance/eval.json +1849 -0
  57. package/src/gdskills/bundled/stacks/flutter-dart/governance/scout.json +33 -0
  58. package/src/gdskills/bundled/stacks/flutter-dart/pack.json +41 -0
  59. package/src/gdskills/bundled/stacks/flutter-dart/rules/coding-style.mdc +98 -0
  60. package/src/gdskills/bundled/stacks/flutter-dart/rules/patterns.mdc +88 -0
  61. package/src/gdskills/bundled/stacks/flutter-dart/rules/security.mdc +91 -0
  62. package/src/gdskills/bundled/stacks/flutter-dart/rules/testing.mdc +101 -0
  63. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/SKILL.md +134 -0
  64. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/evals.json +79 -0
  65. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/SKILL.md +124 -0
  66. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/evals.json +74 -0
  67. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/SKILL.md +139 -0
  68. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/evals.json +77 -0
  69. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/SKILL.md +134 -0
  70. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/evals.json +74 -0
  71. package/src/gdskills/bundled/stacks/java-kotlin-spring/agent-refs.json +3 -0
  72. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/eval.json +2194 -0
  73. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/scout.json +39 -0
  74. package/src/gdskills/bundled/stacks/java-kotlin-spring/pack.json +40 -0
  75. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/coding-style.mdc +67 -0
  76. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/patterns.mdc +65 -0
  77. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/security.mdc +69 -0
  78. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/testing.mdc +80 -0
  79. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/SKILL.md +144 -0
  80. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/evals.json +74 -0
  81. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/SKILL.md +129 -0
  82. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/evals.json +74 -0
  83. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/SKILL.md +147 -0
  84. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/evals.json +75 -0
  85. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/SKILL.md +139 -0
  86. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/evals.json +74 -0
  87. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/SKILL.md +128 -0
  88. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/evals.json +73 -0
  89. package/src/gdskills/bundled/stacks/kotlin-android/agent-refs.json +4 -0
  90. package/src/gdskills/bundled/stacks/kotlin-android/governance/eval.json +1889 -0
  91. package/src/gdskills/bundled/stacks/kotlin-android/governance/scout.json +34 -0
  92. package/src/gdskills/bundled/stacks/kotlin-android/pack.json +38 -0
  93. package/src/gdskills/bundled/stacks/kotlin-android/rules/coding-style.mdc +89 -0
  94. package/src/gdskills/bundled/stacks/kotlin-android/rules/patterns.mdc +96 -0
  95. package/src/gdskills/bundled/stacks/kotlin-android/rules/security.mdc +90 -0
  96. package/src/gdskills/bundled/stacks/kotlin-android/rules/testing.mdc +89 -0
  97. package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/SKILL.md +150 -0
  98. package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/evals.json +77 -0
  99. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/SKILL.md +151 -0
  100. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/evals.json +76 -0
  101. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/SKILL.md +139 -0
  102. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/evals.json +78 -0
  103. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/SKILL.md +131 -0
  104. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/evals.json +77 -0
  105. package/src/gdskills/bundled/stacks/python/agent-refs.json +2 -1
  106. package/src/gdskills/bundled/stacks/python/pack.json +1 -1
  107. package/src/gdskills/bundled/stacks/rust/agent-refs.json +3 -0
  108. package/src/gdskills/bundled/stacks/rust/governance/eval.json +1823 -0
  109. package/src/gdskills/bundled/stacks/rust/governance/scout.json +32 -0
  110. package/src/gdskills/bundled/stacks/rust/pack.json +42 -0
  111. package/src/gdskills/bundled/stacks/rust/rules/coding-style.mdc +93 -0
  112. package/src/gdskills/bundled/stacks/rust/rules/patterns.mdc +85 -0
  113. package/src/gdskills/bundled/stacks/rust/rules/security.mdc +85 -0
  114. package/src/gdskills/bundled/stacks/rust/rules/testing.mdc +82 -0
  115. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/SKILL.md +141 -0
  116. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/evals.json +78 -0
  117. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/SKILL.md +127 -0
  118. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/evals.json +72 -0
  119. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/SKILL.md +133 -0
  120. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/evals.json +79 -0
  121. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/SKILL.md +130 -0
  122. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/evals.json +75 -0
  123. package/src/gdskills/bundled/stacks/swift-ios/agent-refs.json +4 -0
  124. package/src/gdskills/bundled/stacks/swift-ios/governance/eval.json +1803 -0
  125. package/src/gdskills/bundled/stacks/swift-ios/governance/scout.json +32 -0
  126. package/src/gdskills/bundled/stacks/swift-ios/pack.json +38 -0
  127. package/src/gdskills/bundled/stacks/swift-ios/rules/coding-style.mdc +92 -0
  128. package/src/gdskills/bundled/stacks/swift-ios/rules/patterns.mdc +112 -0
  129. package/src/gdskills/bundled/stacks/swift-ios/rules/security.mdc +78 -0
  130. package/src/gdskills/bundled/stacks/swift-ios/rules/testing.mdc +90 -0
  131. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/SKILL.md +144 -0
  132. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/evals.json +75 -0
  133. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/SKILL.md +122 -0
  134. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/evals.json +75 -0
  135. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/SKILL.md +131 -0
  136. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/evals.json +75 -0
  137. package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/SKILL.md +149 -0
  138. package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/evals.json +76 -0
  139. package/src/gdskills/bundled/agents/python-build-fixer.md +0 -52
  140. package/src/gdskills/bundled/agents/python-code-auditor.md +0 -49
@@ -0,0 +1,75 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "The compiler is complaining that this class can't cross a concurrency boundary safely",
5
+ "xcodebuild fails saying this type doesn't conform to a required protocol for the checker",
6
+ "SwiftLint is blocking my build over a rule I don't understand",
7
+ "This type isn't allowed to be passed into a Task the way I've written it",
8
+ "My scheme's build fails but the same code compiled fine before enabling strict concurrency",
9
+ "The build is red because of a warning about an optional that's never actually nil"
10
+ ],
11
+ "negative": [
12
+ "gradle build is failing for this Android module",
13
+ "npm run build is failing with a webpack error",
14
+ "cargo build is failing for this Rust crate",
15
+ "Implement a new feature in this SwiftUI screen",
16
+ "Review this Swift diff for retain cycles",
17
+ "Write Swift Testing coverage for this view model",
18
+ "pip install is failing for this Python project"
19
+ ]
20
+ },
21
+ "scenarios": [
22
+ {
23
+ "id": "sendable-conformance-error",
24
+ "prompt": "The compiler is flagging a class I pass into a Task as not conforming to Sendable, and the build won't succeed under the project's strict concurrency checking. How do I fix this properly?",
25
+ "strictness": "high",
26
+ "expected_behavior": [
27
+ {
28
+ "grader": "judge",
29
+ "rubric": "A correct answer investigates why the class isn't Sendable (mutable stored state accessed from multiple isolation contexts) and fixes the actual root cause -- isolating the class with an actor or @MainActor, or making it genuinely immutable so it can conform to Sendable safely -- rather than defaulting to slapping @unchecked Sendable on it just to clear the compiler error without auditing whether its state is actually safe to share.",
30
+ "pass_criteria": [
31
+ "Investigates or names the actual root cause: the class has mutable stored state that isn't isolated, which is why the compiler won't accept a Sendable conformance for it.",
32
+ "Proposes a real fix -- converting the type to an actor, isolating it to @MainActor, or making its stored properties immutable (let) so a genuine Sendable conformance holds -- as the primary fix direction, not a suppression.",
33
+ "If @unchecked Sendable is mentioned at all, it is framed as requiring an explicit, stated justification of why the access pattern is actually safe -- not offered as the default or easiest fix."
34
+ ],
35
+ "fail_criteria": [
36
+ "Recommends adding `@unchecked Sendable` as the fix, without first establishing (or asking for) why the type's mutable state is actually safe to share across the isolation boundary."
37
+ ]
38
+ }
39
+ ],
40
+ "calibration": {
41
+ "known_right": "First figure out why the compiler won't let this conform to `Sendable`: it almost always means the class has mutable (`var`) stored state that isn't isolated to any particular actor, so the compiler can't prove that two tasks accessing it concurrently won't race. Look at what that class actually does -- if its state is only ever meant to be touched from the main thread/SwiftUI, convert it to `@MainActor final class ...`, which both documents that and gets you a `Sendable`-safe way to cross into a `Task` (since `@MainActor` types can be safely referenced from a `Task` that also hops to the main actor). If the state genuinely needs to be shared and mutated from multiple concurrent contexts, convert the class to an `actor` instead, which gives you compiler-enforced serialized access to its state. If the class's stored properties are all actually immutable after initialization, you can conform it to `Sendable` directly without needing `@unchecked` at all. Only reach for `@unchecked Sendable` if you've actually audited the access pattern and can state concretely why it's safe despite the compiler's inability to prove it -- and write that justification down as a comment, since `@unchecked` is an unchecked promise, not a fix.",
42
+ "known_wrong": "The quickest way to get the build green again is to just add `@unchecked Sendable` to the class's conformance list -- that tells the compiler to trust you instead of proving it itself, and it'll stop flagging the error immediately. You don't need to go digging into why the class isn't naturally Sendable; `@unchecked Sendable` is exactly what it's for, and plenty of existing code in most projects already uses it the same way to get past this kind of check.",
43
+ "vague": "Make the class properly safe to share across the concurrency boundary instead of just working around the compiler's complaint.",
44
+ "subtle_wrong": "Add `@unchecked Sendable` to the class for now to unblock the build, and leave a `// TODO: audit thread-safety` comment above it so it's easy to find later. That gets the strict-concurrency checker off your back today without you having to restructure the type's isolation right now, and the TODO means it's still tracked for whenever there's time to look into it properly."
45
+ },
46
+ "anti_patterns": ["@unchecked Sendable"]
47
+ },
48
+ {
49
+ "id": "optional-binding-warning-no-force-unwrap",
50
+ "prompt": "A static analysis pass on my code flags a guard-let as redundant, saying the value can never actually be nil at that point, and the build treats these findings as errors. How should I fix this?",
51
+ "strictness": "high",
52
+ "expected_behavior": [
53
+ {
54
+ "grader": "judge",
55
+ "rubric": "A correct answer investigates why the compiler believes the value can never be nil at that point (the type or an earlier control-flow guarantee makes the guard-let redundant) and fixes the actual declaration or control flow -- removing the now-unnecessary optionality or restructuring the check -- rather than force-unwrapping the value or otherwise silencing the warning without understanding why the compiler is making that claim.",
56
+ "pass_criteria": [
57
+ "Investigates or names the root cause: something about the type's declaration or the surrounding control flow makes the compiler certain the value is never nil at this point, which is why the guard-let is flagged as redundant.",
58
+ "Proposes fixing the actual declaration/control flow -- e.g. changing the value's type to non-optional if it truly never needs to be optional, or restructuring the logic so the check reflects reality -- rather than suppressing the warning.",
59
+ "Does not recommend force-unwrapping (`!`) the value as the fix for the flagged warning."
60
+ ],
61
+ "fail_criteria": [
62
+ "Recommends replacing the guard-let with a force-unwrap (`!`) to make the warning go away, without addressing why the compiler considers the optional redundant in the first place."
63
+ ]
64
+ }
65
+ ],
66
+ "calibration": {
67
+ "known_right": "Start by figuring out why the compiler thinks this value can never be nil at that point -- that's usually because something upstream already narrows it: the property's declared type isn't actually optional despite being wrapped in `guard let` out of habit, or an earlier `guard`/`if` in the same function already established non-nil-ness and this second check is now redundant. Trace the value back to its declaration and every place it's assigned. If the underlying type genuinely never needs to be optional, change its declaration to non-optional and remove the now-unnecessary `guard let` entirely -- that's the real fix, since the optionality was never doing anything. If it's redundant because of an earlier check in this specific function, remove the duplicate guard rather than keeping two checks for the same thing. Don't reach for a force-unwrap here -- that doesn't address why the compiler flagged this, it just trades a warning for a crash risk if the compiler's assumption ever stops holding (e.g. after a future refactor reintroduces real optionality).",
68
+ "known_wrong": "Since the compiler is saying the value can't be nil anyway, just replace the `guard let value = value else { return }` with a force-unwrap: `let value = value!`. That clears the warning immediately since you're no longer doing a redundant nil check the compiler doesn't like, and since it genuinely can't be nil right now, the force-unwrap is safe in practice.",
69
+ "vague": "Clean up the optional so the code reflects that this value is actually never nil here.",
70
+ "subtle_wrong": "Change the `guard let value = value else { return }` to `let value = value!` since the compiler has already confirmed it can't be nil at this point -- that removes the redundant branch and the warning along with it. It's a bit more concise than tracing back through the declaration to see whether the type itself should be made non-optional, and functionally it's the same thing since the guard was never actually going to fail."
71
+ },
72
+ "anti_patterns": []
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: swift-code-review
3
+ description: "Use when reviewing a Swift/iOS change for concurrency, memory, and safety risks -- force-unwraps, retain cycles from closures capturing self, missing @MainActor isolation, @unchecked Sendable used to silence checks, and secrets stored outside the Keychain. Read-only, no edits."
4
+ triggers:
5
+ - "review this Swift diff for force unwraps"
6
+ - "check this iOS change for retain cycles"
7
+ - "review this SwiftUI pull request for concurrency issues"
8
+ - "any Keychain misuse in this Swift change"
9
+ - "check @MainActor isolation in this diff"
10
+ - "review this Swift diff for Sendable violations"
11
+ metadata:
12
+ origin: authored
13
+ category: review
14
+ version: "1.0.0"
15
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
16
+ license: "MIT"
17
+ ---
18
+
19
+ # Swift/iOS code review
20
+
21
+ Read-only review of a Swift/iOS change for concurrency, memory, and
22
+ safety risks specific to Swift: force-unwraps, retain cycles, missing
23
+ actor isolation, concurrency-check suppressions, and secret storage. This
24
+ skill never edits code — it reports findings. `rules/coding-style.mdc`,
25
+ `rules/patterns.mdc`, and `rules/security.mdc` are the rule set findings
26
+ are checked against.
27
+
28
+ ## Workflow
29
+
30
+ ### Step 1: Scope the review
31
+
32
+ 1. Identify the changed files (`git diff` against the review base) —
33
+ review only `*.swift` files in the diff, not the whole repository.
34
+ 2. Read enough of the surrounding, unchanged code to know whether a
35
+ flagged pattern is new in this diff or pre-existing; note pre-existing
36
+ issues separately from ones the diff introduces.
37
+
38
+ ### Step 2: Check each changed file against the focus list
39
+
40
+ **Optionals and force operations**
41
+ - A force-unwrap (`!`) or force-try (`try!`) on a value that is not a
42
+ guaranteed-safe programmer invariant (a network response, decoded
43
+ JSON, user input, anything from an external source) — flag it and
44
+ suggest `guard let`/`if let`/`try?`/explicit error handling instead.
45
+
46
+ **Concurrency and actor isolation**
47
+ - An `@Observable` class or any type whose state SwiftUI reads/mutates
48
+ is not isolated to `@MainActor` — flag the missing isolation as a
49
+ potential cross-actor data race.
50
+ - `@unchecked Sendable` or `nonisolated(unsafe)` applied without a
51
+ comment justifying why the type's mutable state is actually safe —
52
+ flag it as a suppression rather than a proven-safe boundary.
53
+ - A `Task {}` started with no stated reason it does not need to be
54
+ joined/cancelled, especially one that should be scoped to a view's
55
+ lifetime via `.task` instead of created manually — flag it.
56
+
57
+ **Memory and closures**
58
+ - A closure that outlives its creating call (stored callback, Combine
59
+ `sink`, a `Task` capturing a long-lived object) capturing `self`
60
+ strongly where that creates a retain cycle — flag a missing `[weak
61
+ self]`; distinguish it from a short-lived closure that returns before
62
+ the enclosing scope does, which does not need one.
63
+
64
+ **State ownership**
65
+ - `@State` used for a value that is actually owned and mutated by a
66
+ parent (should be `@Binding`), or a `@StateObject`/`ObservableObject`
67
+ created inline inside a view's body (recreated on every update
68
+ instead of owned once) — flag either as a state-ownership bug per
69
+ `rules/patterns.mdc`.
70
+
71
+ **Security**
72
+ - A secret, API key, or auth token stored in `UserDefaults`, hardcoded
73
+ as a string literal, or logged unredacted — flag per
74
+ `rules/security.mdc`.
75
+
76
+ ### Step 3: Report
77
+
78
+ For each finding: file:line, the pattern, why it matters (crash risk,
79
+ data race, leak, secret exposure), and the fix direction — but do not
80
+ apply it.
81
+
82
+ ```
83
+ Features/Order/OrderDetailModel.swift:18 — force-unwraps `response.items.first!`
84
+ on a decoded network response. Risk: a genuinely empty or malformed
85
+ response crashes the app instead of failing gracefully. Fix direction:
86
+ `guard let first = response.items.first else { throw ... }`.
87
+ ```
88
+
89
+ ## Rules
90
+
91
+ - NEVER edit code — findings and fix direction only.
92
+ - Flag force-unwraps/force-tries on non-guaranteed values, missing
93
+ `@MainActor` isolation, `@unchecked Sendable` suppressions, retain
94
+ cycles, state-ownership bugs, and secrets outside the Keychain; do not
95
+ report generic style nits already covered by SwiftLint/swift-format
96
+ (those are noise here).
97
+ - Distinguish a finding the diff introduces from a pre-existing one in
98
+ code the diff merely touches.
99
+ - When a suspected data race is not certain from reading alone, say "run
100
+ under Thread Sanitizer / the Swift 6 strict-concurrency checker to
101
+ confirm" rather than asserting a race exists without evidence.
102
+
103
+ ## Red Flags
104
+
105
+ | Rationalization | Why it is wrong |
106
+ |---|---|
107
+ | "The API always returns this field, the force-unwrap is fine" | "Always" is a claim about a system outside this diff's control; a malformed or versioned response crashes instead of failing gracefully |
108
+ | "This closure fires fast, `self` won't actually leak" | A retain cycle does not depend on how fast a closure fires — if it is stored or can outlive the call, an unweakened `self` capture leaks regardless |
109
+ | "`@unchecked Sendable` here is just to get the build green, we'll revisit it" | An unchecked promise with no audit trail rarely gets revisited; ask for the actual safety argument now or flag it as unresolved |
110
+ | "I'll just fix the force-unwrap myself since it's a one-line change" | This skill is read-only; report the finding and its fix direction, do not edit the file |
111
+
112
+ ## Verification
113
+
114
+ Do not report the review done until all of the following hold:
115
+
116
+ - Every changed `*.swift` file in the diff was read, not just files
117
+ named in the PR description.
118
+ - Every finding names a concrete file:line, the specific risk category
119
+ from Step 2, and a fix direction.
120
+ - No source file was modified by this review.
121
+ - Findings distinguish diff-introduced issues from pre-existing ones in
122
+ touched files.
@@ -0,0 +1,75 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "Review this Swift diff, a new function unwraps the decoded response with an exclamation mark",
5
+ "Check this iOS change for a closure that might be keeping a view model alive too long",
6
+ "Look over this pull request for anything that could crash on a malformed API response",
7
+ "Take a look at this Swift change for concurrency safety before it merges",
8
+ "Review this diff for whether the completion handler here is safe to store",
9
+ "Give this iOS pull request a pass for anything that could leak memory"
10
+ ],
11
+ "negative": [
12
+ "Review this Go diff for goroutine leaks",
13
+ "Review this Kotlin diff for coroutine cancellation issues",
14
+ "Review this diff and also fix the bugs you find in the Swift code",
15
+ "Review this Python code for SQL injection",
16
+ "Implement a fix for the retain cycle in this Swift closure",
17
+ "Run a general security review on this codebase",
18
+ "Review this Swift diff for naming conventions and formatting only"
19
+ ]
20
+ },
21
+ "scenarios": [
22
+ {
23
+ "id": "read-only-review",
24
+ "prompt": "Review this Swift diff: `let firstItem = response.items.first!` was added right after decoding a network response. What do you find?",
25
+ "strictness": "high",
26
+ "expected_behavior": [
27
+ {
28
+ "grader": "judge",
29
+ "rubric": "A correct answer identifies the force-unwrap on response.items.first as the problem, explains why it's wrong (items can legitimately be empty for a real response, so this crashes instead of failing gracefully), and reports this as a finding with a fix direction -- it never actually edits or claims to have edited the code, even partially, since this skill is read-only.",
30
+ "pass_criteria": [
31
+ "Identifies the force-unwrap (`!`) on `response.items.first` as the problem, naming the specific line/expression from this diff.",
32
+ "Explains the concrete consequence: a genuinely empty or malformed `items` array crashes the app at this line instead of being handled.",
33
+ "Presents this purely as a finding with a fix direction (e.g. `guard let`/`if let` instead of `!`), with no code change applied, described as applied, or described as a proof-of-concept edit made during the review."
34
+ ],
35
+ "fail_criteria": [
36
+ "Actually edits, patches, or claims to have already made even a partial or proof-of-concept code change (this skill is strictly read-only: findings and fix direction only, no edits of any size)."
37
+ ]
38
+ }
39
+ ],
40
+ "calibration": {
41
+ "known_right": "Finding: `let firstItem = response.items.first! ` force-unwraps the first element of a decoded network response. That's risky -- `items` is data coming back from a backend, and there's nothing in this diff guaranteeing it's non-empty; a legitimately empty result set, a malformed response, or a future API change that starts returning zero items would all crash the app at this line instead of failing gracefully. Fix direction: replace the force-unwrap with `guard let firstItem = response.items.first else { /* handle the empty case, e.g. return, throw, or show an empty state */ }`, or `if let` if the empty case should just skip this block rather than exit the function. I'm not making this change myself since this review is read-only -- flagging it with the fix direction above for whoever picks it up.",
42
+ "known_wrong": "That force-unwrap on `response.items.first!` is a clear crash risk if the array is ever empty, so I went ahead and fixed it directly: I changed the line to `guard let firstItem = response.items.first else { return }` and verified the surrounding function still compiles. The change is already in place, so this should be ready to merge once you take a look.",
43
+ "vague": "That force-unwrap on the response looks a little risky -- might be worth double-checking before this merges.",
44
+ "subtle_wrong": "Finding: `response.items.first!` force-unwraps a value from a network response, which is worth flagging since it's user-facing data. I made a small proof-of-concept tweak nearby to show what a safer version could look like (`guard let firstItem = response.items.first else { return }`), though I didn't verify it compiles cleanly with the rest of the function -- the actual fix would need someone to confirm the early-return here is the right behavior for this call site."
45
+ },
46
+ "anti_patterns": []
47
+ },
48
+ {
49
+ "id": "mainactor-isolation-check",
50
+ "prompt": "Review this Swift diff: a new @Observable class was added to back a SwiftUI screen's state, but it isn't marked @MainActor and nothing else in the file establishes main-actor isolation for it. What's wrong?",
51
+ "strictness": "high",
52
+ "expected_behavior": [
53
+ {
54
+ "grader": "judge",
55
+ "rubric": "A correct answer identifies the missing @MainActor isolation on the @Observable class as the problem and flags it as a potential cross-actor data race risk, since SwiftUI reads and mutates this state and expects it confined to the main actor; it suggests marking the type @MainActor as the fix direction, as a finding, without applying it, and does not dismiss the missing isolation as fine because the type is only touched from SwiftUI so far.",
56
+ "pass_criteria": [
57
+ "Explicitly identifies the missing @MainActor isolation on the @Observable class as the issue, not just 'this could be cleaned up'.",
58
+ "Explains the concrete risk: SwiftUI reads/mutates this state and expects main-actor confinement, so without @MainActor the type's mutable state can be accessed unsafely from another isolation context (a potential data race), especially once any async code touches it from a background context.",
59
+ "Names the fix direction -- marking the class @MainActor -- as a finding, not as a code change already made."
60
+ ],
61
+ "fail_criteria": [
62
+ "Dismisses the missing @MainActor isolation as fine because the type is currently only touched from SwiftUI view code, instead of flagging it as a real isolation gap that should be made explicit."
63
+ ]
64
+ }
65
+ ],
66
+ "calibration": {
67
+ "known_right": "Finding: this new `@Observable` class backs a SwiftUI screen's state but isn't marked `@MainActor`, and nothing else in the file establishes main-actor isolation for it (no module-wide default actor isolation set to `MainActor` that I can see). That's a gap worth flagging -- SwiftUI reads and mutates this type's properties directly from view code, which runs on the main actor, so the type's mutable state should be confined there explicitly. Without `@MainActor` on the type, the compiler can't guarantee that some other code path -- an async task, a callback from a background queue -- won't touch this state from off the main actor, which is exactly the kind of unsynchronized cross-actor access Swift's concurrency checking exists to catch. It happening to only be touched from SwiftUI so far doesn't make the isolation unnecessary; it makes it currently-unenforced. Fix direction: mark the class `@MainActor` (`@Observable @MainActor final class ...`) so the isolation is explicit and compiler-checked rather than incidental. Reporting this as a finding rather than applying the fix, since this review is read-only.",
68
+ "known_wrong": "This is worth a note but not a blocker -- the class is only ever touched from SwiftUI view code right now, and views already run on the main actor, so in practice this is already safe even without the `@MainActor` annotation. I wouldn't hold up the diff over adding an annotation that doesn't currently change behavior; it's fine to merge as-is and revisit if the type ever gets used from a background context.",
69
+ "vague": "That observable class not being explicitly isolated could be an issue -- might be worth checking before this merges.",
70
+ "subtle_wrong": "Finding: the new `@Observable` class has no `@MainActor` annotation. That said, since it's only constructed and read from within SwiftUI view bodies in this diff, and view bodies always run on the main actor already, the missing annotation is mostly a documentation gap rather than a real safety issue right now -- it would be nice to add for clarity, but I wouldn't treat it as blocking since nothing in this diff actually touches the type from a background context."
71
+ },
72
+ "anti_patterns": []
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: swift-testing
3
+ description: "Use when a Swift/iOS test suite needs writing, extending, or fixing -- Swift Testing's @Test/#expect/#require, parameterized tests, legacy XCTest, and mocking network/persistence dependencies at a protocol boundary rather than the type under test's internals."
4
+ triggers:
5
+ - "write tests for this Swift function"
6
+ - "add Swift Testing coverage for this view model"
7
+ - "fix this failing XCTest"
8
+ - "add a parameterized test with arguments"
9
+ - "mock the network layer for this test"
10
+ - "test this async Swift function"
11
+ metadata:
12
+ origin: authored
13
+ category: test
14
+ version: "1.0.0"
15
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
16
+ license: "MIT"
17
+ ---
18
+
19
+ # Swift/iOS testing
20
+
21
+ Write, extend, or fix a Swift test suite: Swift Testing (`@Test`/
22
+ `#expect`/`#require`) for new coverage, legacy `XCTest` where a project
23
+ has not migrated, and mocking external dependencies at a protocol
24
+ boundary. `rules/testing.mdc` carries the full rule set this skill's
25
+ checklist is built from — read it, not just this summary, before writing
26
+ tests.
27
+
28
+ ## Workflow
29
+
30
+ ### Step 1: Discover the project's test conventions
31
+
32
+ 1. Check whether the test target already imports `Testing` (Swift
33
+ Testing) or is built on `XCTestCase` (legacy XCTest) — match
34
+ whichever the target already uses for the kind of test you are
35
+ adding; introduce Swift Testing for new unit-test coverage rather
36
+ than mixing frameworks within one file.
37
+ 2. Read 1-2 neighboring test files for: naming convention, how mocks/
38
+ test doubles are constructed, and whether a protocol boundary already
39
+ exists for the dependency you need to fake.
40
+ 3. Note anything the test subject needs isolated to `@MainActor` — a
41
+ test exercising `@MainActor`-isolated state needs the same isolation
42
+ on the test itself.
43
+
44
+ ### Step 2: Plan test cases
45
+
46
+ **Functions:** happy path, edge cases (nil/empty/boundary inputs), error
47
+ cases (the specific error thrown, not just "it throws"), async
48
+ cancellation where relevant.
49
+
50
+ **Parameterized (Swift Testing):** `@Test("description", arguments:
51
+ [...])` over a hand-rolled loop or several copy-pasted test functions
52
+ that differ only by input.
53
+
54
+ **Dependencies (network, persistence, platform services):** identify the
55
+ protocol boundary the production code already depends on (or define one
56
+ if it does not exist yet) and construct a test double conforming to that
57
+ protocol — never mock the type under test's own private methods or
58
+ internal collaborators.
59
+
60
+ ### Step 3: Write
61
+
62
+ 1. Create/extend the test file at the project's own convention path and
63
+ framework (`Testing` or `XCTestCase`).
64
+ 2. `@Test func name() async throws { ... }` with `#expect(...)` for
65
+ checks that should record and continue, `try #require(...)` for ones
66
+ that must stop the test immediately (Swift Testing); or the XCTest
67
+ equivalents (`XCTAssert...`, `XCTUnwrap`) on a legacy target.
68
+ 3. Inject the protocol-typed test double into the type under test
69
+ through its existing initializer/dependency-injection point — do not
70
+ reach into private state to swap a dependency.
71
+ 4. Await async calls directly (`await`) instead of bridging with
72
+ `XCTestExpectation`/a semaphore, unless the project's toolchain
73
+ predates async test support.
74
+ 5. Never wait for concurrent work with a fixed delay
75
+ (`Thread.sleep`/`Task.sleep` as a guess); await the call, join a
76
+ `TaskGroup`, or await a real fulfillment signal.
77
+
78
+ ### Step 4: Run and fix
79
+
80
+ ```bash
81
+ xcodebuild test -scheme <Scheme> -destination 'platform=iOS Simulator,name=<Simulator>'
82
+ # or, for a Swift package:
83
+ swift test
84
+ ```
85
+
86
+ Fix failing tests (max 3 iterations) — fix the test, not the source
87
+ under test, unless the test itself has correctly caught a real bug (say
88
+ so in the report rather than silently changing production code).
89
+
90
+ ### Step 5: Report
91
+
92
+ ```
93
+ Generated: OrderDetailModelTests.swift
94
+ - 6 @Test functions (2 parameterized), OrderClient mocked at its protocol boundary
95
+ - xcodebuild test passes
96
+ ```
97
+
98
+ ## Rules
99
+
100
+ - ALWAYS match the project's existing framework (Swift Testing vs
101
+ XCTest) and naming/mock conventions found in Step 1, not a different
102
+ project's style.
103
+ - NEVER modify source code — only test files (and test doubles/fixtures
104
+ alongside them).
105
+ - NEVER mock a dependency below its protocol boundary — fake the
106
+ protocol (network client, persistence layer), not the type under
107
+ test's own internals or private state.
108
+ - NEVER use a fixed delay (`Thread.sleep`/`Task.sleep` as a guess) to
109
+ wait for async/concurrent work to finish.
110
+
111
+ ## Red Flags
112
+
113
+ | Rationalization | Why it is wrong |
114
+ |---|---|
115
+ | "I'll just stub the private `fetchFromDisk()` method on the view model itself" | Mocking a type's own internals couples the test to an implementation detail; it breaks on a harmless refactor and proves nothing about the type's real, protocol-shaped dependency |
116
+ | "A short `Task.sleep(for: .milliseconds(200))` should be enough for the async call to finish" | Non-deterministic under load/CI; `await` the call directly or join the real completion signal instead of guessing a duration |
117
+ | "This test keeps failing, I'll loosen the assertion to just check `result != nil`" | Covers nothing about *what* the result should be, hiding a regression next time this test should have caught one |
118
+ | "It's simpler to keep using XCTestCase for this new test even though the rest of the target moved to Swift Testing" | Match what the target has actually adopted; mixing frameworks within one file/target without a reason adds inconsistency for no benefit |
119
+
120
+ ## Verification
121
+
122
+ Do not report the work done until all of the following hold:
123
+
124
+ - The test file sits at the project's own convention path, matching the
125
+ framework and style read in Step 1.
126
+ - `xcodebuild test`/`swift test` exits 0 with every generated test
127
+ passing.
128
+ - Any faked dependency conforms to the same protocol the production code
129
+ depends on — not a subclass override or a private-method stub.
130
+ - `git status` shows only test files (and test doubles/fixtures) added
131
+ or modified; no source file under test changed.
@@ -0,0 +1,75 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "I need coverage for this view model that talks to a backend, what's the right way to fake the network for it",
5
+ "Write a few test cases for this order validation function",
6
+ "This screen kicks off a background task and I want a deterministic way to wait for it in a test",
7
+ "Add coverage that exercises several different discount inputs for this pricing function",
8
+ "How do I fake persistence for this view model's tests without touching the real database",
9
+ "This async function needs test coverage, what does a good test for it look like",
10
+ "Set up test doubles for the networking layer in this feature's test suite"
11
+ ],
12
+ "negative": [
13
+ "Write instrumented UI tests with Android's Espresso for this screen",
14
+ "Add widget tests for this Flutter screen using WidgetTester",
15
+ "Write unit tests for this ASP.NET Core controller with xUnit",
16
+ "Add Jest tests for this React component's rendering",
17
+ "Implement this feature in SwiftUI with an async network call",
18
+ "Review this Swift diff for retain cycles",
19
+ "Write pytest fixtures for this Python service"
20
+ ]
21
+ },
22
+ "scenarios": [
23
+ {
24
+ "id": "mock-network-boundary",
25
+ "prompt": "I have a view model that fetches an order from a backend client and I need to test it without hitting the real network. How should I set this up?",
26
+ "strictness": "high",
27
+ "expected_behavior": [
28
+ {
29
+ "grader": "judge",
30
+ "rubric": "A correct answer fakes the network dependency at a protocol boundary the view model already depends on (or a newly introduced narrow protocol) -- injecting a test-double conforming type through the view model's existing initializer -- rather than mocking the view model's own private methods, reaching into its internal state, or subclassing the concrete network client to override its behavior.",
31
+ "pass_criteria": [
32
+ "Names a protocol boundary for the network dependency (e.g. an `OrderClient` protocol) that the view model depends on, not the concrete network/URLSession type directly.",
33
+ "Describes injecting a test-double conforming type through the view model's existing initializer/dependency injection point.",
34
+ "Does not describe mocking the view model's own private methods or internal state to fake the network response."
35
+ ],
36
+ "fail_criteria": [
37
+ "Recommends mocking or stubbing a private method or internal property on the view model itself (the type under test) to fake the network response, instead of faking the network client it depends on through a protocol boundary."
38
+ ]
39
+ }
40
+ ],
41
+ "calibration": {
42
+ "known_right": "Define a narrow protocol the view model depends on, e.g. `protocol OrderClient { func fetchOrder(id: String) async throws -> Order }`, and have the view model take one through its initializer (`init(client: OrderClient)`) instead of constructing a concrete `URLSession`-backed client itself. In production, inject the real `URLSessionOrderClient` conforming to that protocol; in the test, inject a `FakeOrderClient` that also conforms to `OrderClient` and returns a canned `Order` (or throws a canned error) without touching the network at all. The view model never has to know it's under test -- it just calls `client.fetchOrder(id:)` through the protocol either way. This keeps the test fast and deterministic, and it survives a refactor of how the real client is implemented internally (switching from `URLSession` to something else) without touching the test, since the fake only depends on the protocol's shape.",
43
+ "known_wrong": "The simplest way is to subclass the view model in the test target and override its private `performFetch()` method to return a canned `Order` directly, skipping the network call entirely. That way you don't need to introduce a whole protocol just for one test -- you're testing the view model's public behavior anyway, and overriding the internal fetch method gets you there with a lot less boilerplate than defining and injecting a fake client type.",
44
+ "vague": "Fake out the network so the test doesn't make a real request and can check the view model's behavior reliably.",
45
+ "subtle_wrong": "Register a custom `URLProtocol` subclass globally with `URLProtocol.registerClass(...)` in the test's setup to intercept every request in the process during the test run and return a canned order response -- that way the view model doesn't need any changes at all, and you don't need to inject anything into it, since interception happens underneath any `URLSession` including `URLSession.shared`. No protocol boundary, no initializer change, no test double passed in -- the fake just answers whatever request happens to go out."
46
+ },
47
+ "anti_patterns": []
48
+ },
49
+ {
50
+ "id": "no-fixed-delay-async-wait",
51
+ "prompt": "My test starts an async task in a view model and I want to wait for it to finish before asserting on the result. What's the right way to do this?",
52
+ "strictness": "high",
53
+ "expected_behavior": [
54
+ {
55
+ "grader": "judge",
56
+ "rubric": "A correct answer waits for the async work with a real synchronization mechanism -- awaiting the async call directly (marking the test async and using await), joining a TaskGroup, or awaiting a real fulfillment signal (an XCTestExpectation that is actually fulfilled by the code under test) -- never a fixed Task.sleep/Thread.sleep delay used as a guess at how long the work will take, whether alone or as extra insurance alongside a real await.",
57
+ "pass_criteria": [
58
+ "Uses `await` directly on the async call (marking the test function `async`), or an equivalent real join (a fulfilled `XCTestExpectation`, joining a `TaskGroup`), as the actual mechanism for knowing the work finished.",
59
+ "Explains why this is reliable compared to a fixed delay -- it does not depend on guessing how long the async work takes."
60
+ ],
61
+ "fail_criteria": [
62
+ "Recommends a fixed `Task.sleep`/`Thread.sleep` delay, anywhere in the wait step, as the mechanism (or as 'extra insurance' alongside a real await) for knowing the async work has finished before asserting."
63
+ ]
64
+ }
65
+ ],
66
+ "calibration": {
67
+ "known_right": "Mark the test function `async` and `await` the call directly: `let result = await viewModel.loadOrder()` (or, if the work is triggered as a side effect rather than returned, `await` the view model's own async method that starts it). That blocks the test exactly until the work is actually done, not for a guessed duration, so it holds up whether the work finishes in a millisecond or takes longer under CI load. If the async work is truly fire-and-forget from a synchronous call site and you can't await it directly, use an `XCTestExpectation` that the code under test explicitly fulfills when the work completes, and `await fulfillment(of: [expectation], timeout: ...)` -- the timeout there is a safety net for a hang, not the thing the test is actually relying on to know the work finished. Avoid `Task.sleep`/`Thread.sleep` for this: a fixed delay either wastes time when the work finishes early or flakes when it doesn't finish inside the guessed window.",
68
+ "known_wrong": "Easiest way is to just add `try? await Task.sleep(for: .milliseconds(300))` right after triggering the async work, then run your assertions -- that gives it plenty of time to finish in almost every run without needing to restructure the test to await anything directly. If it's occasionally still flaky in CI, bump it up to half a second; that's a lot simpler than wiring up an expectation or making the whole call chain awaitable just for a test.",
69
+ "vague": "Wait for the async work to actually complete before you check the result, rather than guessing how long it takes.",
70
+ "subtle_wrong": "Mark the test `async` and `await viewModel.loadOrder()` directly, which is the right way to know the call itself finished. Since the view model also kicks off a secondary background task from inside that call, add a short `try? await Task.sleep(for: .milliseconds(100))` right after the awaited call too, just to be safe that the secondary task has also settled before asserting on the view model's full state."
71
+ },
72
+ "anti_patterns": ["Task.sleep"]
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: swiftui-implementation
3
+ description: "Use when implementing or extending a feature in a SwiftUI/iOS app -- state ownership (@State/@Binding/@Environment/@Observable), Swift concurrency (async/await, actors, @MainActor, TaskGroup, Sendable), and view composition."
4
+ triggers:
5
+ - "implement this feature in SwiftUI"
6
+ - "add a new screen to this iOS app"
7
+ - "wire up state for this SwiftUI view"
8
+ - "add an async network call from this view"
9
+ - "make this model observable in SwiftUI"
10
+ - "extend this iOS feature with a new view"
11
+ - "add a view model for this SwiftUI screen"
12
+ metadata:
13
+ origin: authored
14
+ category: implement
15
+ version: "1.0.0"
16
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
17
+ license: "MIT"
18
+ ---
19
+
20
+ # SwiftUI/iOS implementation
21
+
22
+ Implement or extend a feature in a SwiftUI/iOS codebase: state ownership,
23
+ Swift concurrency structuring, and view composition. Scoped to
24
+ Swift/SwiftUI specifically — `rules/coding-style.mdc`, `rules/patterns.mdc`,
25
+ and `rules/security.mdc` carry the full stack-specific rule set this
26
+ skill's checklist is built from; read them before writing code, not just
27
+ this summary.
28
+
29
+ ## Workflow
30
+
31
+ ### Step 1: Discover the project's own conventions
32
+
33
+ 1. Find the deployment target (the Xcode project's iOS Deployment
34
+ Target, or a Swift Package's platform requirement) — it decides
35
+ whether `@Observable` (iOS 17+) is available or the project is still
36
+ on `ObservableObject`/`@Published`.
37
+ 2. Read 1-2 neighboring views/view models for: how state is currently
38
+ owned (`@State` + `@Observable`, or `@StateObject` + `ObservableObject`),
39
+ how dependencies are injected (`@Environment`, initializer injection),
40
+ and whether the project already defines protocol boundaries for
41
+ networking/persistence.
42
+ 3. Check whether the project has adopted the Swift 6 language mode
43
+ (`swift-tools-version: 6.0` in `Package.swift`, or the Xcode build
44
+ setting) — this decides how strictly `Sendable`/actor-isolation
45
+ errors are enforced at compile time versus only warned about.
46
+
47
+ ### Step 2: Design before writing
48
+
49
+ - For each new piece of state, decide ownership before writing the
50
+ view: does this view create and own it (`@State`), does a parent own
51
+ it and this view only needs to mutate it (`@Binding`), or is it
52
+ ambient to a subtree (`@Environment`)? Match the deployment target's
53
+ supported pattern (`@Observable`/`@State` on iOS 17+,
54
+ `ObservableObject`/`@StateObject` on an older target).
55
+ - For anything asynchronous (a network call, a database read), decide
56
+ where it runs and how it is isolated: `@MainActor` for anything
57
+ touching view state directly, `async`/`await` through a protocol-typed
58
+ dependency, and how the call's `Task` is scoped (tied to a view's
59
+ `.task` modifier when it should cancel with the view, or owned by a
60
+ longer-lived object when it should outlive one screen).
61
+ - Sketch the protocol boundary for any new external dependency
62
+ (network client, persistence) even if only one concrete
63
+ implementation exists yet — it is what `swift-testing` mocks against
64
+ later.
65
+
66
+ ### Step 3: Implement
67
+
68
+ 1. Model new state per `rules/patterns.mdc`'s state-ownership rules —
69
+ `@State` for view-owned, `@Binding` for child-mutates-parent,
70
+ `@Environment` for ambient/shared, `@Bindable` when a child needs
71
+ bindings into an `@Observable` model it does not own.
72
+ 2. Mark any `@Observable` class (or other UI-touching mutable type)
73
+ `@MainActor`; thread `async`/`await` through the call chain instead
74
+ of nesting completion handlers.
75
+ 3. Use `guard let`/`guard` for early exits on optionals; never force-
76
+ unwrap (`!`) or force-try (`try!`) a network response, decoded value,
77
+ or anything else that is not a guaranteed-safe programmer invariant.
78
+ 4. Keep a closure STORED past its creating call (a completion handler
79
+ held as a property, a Combine `sink` kept in a `Set<AnyCancellable>`)
80
+ from retaining `self` strongly when that would create a genuine
81
+ retain cycle — capture `[weak self]` and unwrap. A `Task {}` closure
82
+ is different: it runs once and releases its captures when it
83
+ finishes, so it does not create a persistent cycle the way a stored
84
+ closure does; still prefer `[weak self]` there when the task can
85
+ outlive something short-lived (the view/screen it was launched from)
86
+ and would otherwise keep it alive for no reason while it runs.
87
+ 5. Format with the project's configured formatter as you go.
88
+
89
+ ### Step 4: Verify
90
+
91
+ ```bash
92
+ xcodebuild build -scheme <Scheme> -destination 'platform=iOS Simulator,name=<Simulator>'
93
+ # or, for a Swift package:
94
+ swift build
95
+ ```
96
+
97
+ Run the project's configured linter (`swiftlint`) if present. A
98
+ build/strict-concurrency failure at this step is a signal to fix the
99
+ implementation, not to reach for `swift-build-fix`'s scope unless the
100
+ failure is purely a build/module/dependency problem unrelated to the
101
+ feature logic.
102
+
103
+ ### Step 5: Report
104
+
105
+ ```
106
+ Implemented: Features/Order/OrderDetailView.swift, Features/Order/OrderDetailModel.swift
107
+ - New @Observable OrderDetailModel (@MainActor), owned via @State in OrderDetailView
108
+ - Async fetch through OrderClient protocol, awaited from a .task modifier
109
+ - xcodebuild build succeeds, swiftlint clean
110
+ ```
111
+
112
+ ## Rules
113
+
114
+ - Never force-unwrap (`!`) or force-try (`try!`) a value that can
115
+ genuinely be nil or throw at runtime (network data, decoded JSON, user
116
+ input) — `guard let`/`if let`/`try`/`try?` instead.
117
+ - Never introduce `ObservableObject`/`@Published`/`@StateObject` in new
118
+ code on a project whose deployment target already supports
119
+ `@Observable` (iOS 17+) — match the modern pattern unless the project
120
+ has an explicit reason not to have migrated yet.
121
+ - Never mark a type `@unchecked Sendable` to silence a strict-
122
+ concurrency diagnostic without actually auditing and documenting why
123
+ its mutable state is safe.
124
+ - Never store `context`-like ambient dependencies as an implicit
125
+ singleton reach-through when `@Environment` or explicit injection
126
+ already expresses the same dependency clearly.
127
+
128
+ ## Red Flags
129
+
130
+ | Rationalization | Why it is wrong |
131
+ |---|---|
132
+ | "I'll force-unwrap this decoded response, the API always returns this field" | "Always" is a claim about a system you do not control; a malformed or versioned-differently response crashes the app instead of failing gracefully |
133
+ | "I'll mark this class `@unchecked Sendable` so the concurrency checker stops complaining" | Trades a compile-time data-race guarantee for an unchecked promise — audit the actual mutable state and isolate it properly, or make the type genuinely immutable |
134
+ | "This view creates the view model, so `@StateObject` is fine even though we target iOS 17+" | `@Observable` + `@State` is the current default for exactly this ownership shape on iOS 17+; reaching for the legacy pattern in new code adds an inconsistency with no benefit |
135
+ | "The completion handler captures `self` strongly, but it always fires quickly" | A retain cycle does not care how quickly the closure fires — if the closure is stored or can outlive the call, an unweakened `self` capture leaks |
136
+
137
+ ## Verification
138
+
139
+ Do not report the work done until all of the following hold:
140
+
141
+ - The build succeeds (`xcodebuild build`/`swift build`) with no new
142
+ strict-concurrency warnings introduced by this change.
143
+ - Every new piece of view state has a deliberate ownership choice
144
+ (`@State`/`@Binding`/`@Environment`/`@Observable`) matching
145
+ `rules/patterns.mdc`, not a default reached for out of habit.
146
+ - No new force-unwrap (`!`) or force-try (`try!`) was added on a value
147
+ that can genuinely be nil or throw.
148
+ - Any new closure that outlives its creating call captures `self`
149
+ weakly where a retain cycle is possible.