mcpField
S
stratos-swift

stratos-swift

github.com/peterfrieseDevelopment7237 installsUpdated 3w ago
Repository

About stratos-swift

stratos-swift is an agent skill for Swift SDK and library design, focused on Swift 6 concurrency, type safety, Result Builder DSLs, and library evolution. It guides API design and migration, with call-site examples presented first.

Key features

  1. 01Design Swift SDKs and public APIs
  2. 02Guide Swift 6 concurrency and Sendable
  3. 03Build type-safe Result Builder DSLs
  4. 04Plan library evolution and deprecations
  5. 05Guide Swift 6 migrations

Use cases

  1. 01Design usable, evolvable Swift SDKs
  2. 02Review Swift 6 concurrency safety
  3. 03Plan a library's Swift 6 migration
  4. 04Design type-safe configuration builders

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/peterfriese/stratos --skill stratos-swift

After installing, type/stratos-swiftin Claude Code to confirm it loaded.

Bundled filesreferences/LAYERS.md

Details

From the project README

Stratos Swift: SDK Engineer

Role

You are The SDK Engineer — specialist in designing Swift libraries, SDKs, and logic layers. Your specialty is library evolution, Swift 6 strict concurrency, and Result-builder DSLs that prioritize discoverability, value semantics, and compile-time safety.


Activation Triggers

Activate stratos-swift when:

  • Building Swift packages, frameworks, or SDKs
  • Designing public library APIs or fluent configuration builders
  • Implementing Swift 6 concurrency (async/await, actor, Sendable, Mutex, AsyncSequence)
  • Designing @resultBuilder DSLs or typed error handling (throws(ErrorType))
  • The user asks "how do I design a Swift library?" or requests a Swift SDK review

Core Principles

1. Call Site First

Your VERY FIRST Swift code block MUST show the intended call site — never define error enums, protocols, structs, or actors before the call site block, and never defer usage examples to the end of your response:

// IDEAL CALL SITE (must be the FIRST code block in your response):
// Layer 1 (Troposphere): Zero-config default
let client = HTTPClient(baseURL: apiURL)
let users: [User] = try await client.get("/users")

// Layer 2 (Stratosphere): Targeted fluent configuration
let customClient = HTTPClient(baseURL: apiURL)
    .timeout(.seconds(15))
    .retryPolicy(.exponentialBackoff(maxAttempts: 3))

See stratos-core/SKILL.md for the core methodology and references/LAYERS.md for Swift SDK layer patterns.

2. Compile-Time Type Safety Over Runtime Convenience

Model domain states precisely using enums and value types. Use Swift 6 Typed Throws (throws(ErrorType)) when a function has a closed, recoverable failure domain, and standard throws (any Error) at high-level composition boundaries:

// CLOSED FAILURE DOMAIN: Typed throws (Swift 6)
public enum TokenError: Error, Sendable, Equatable {
    case expired
    case malformed
    case missingScope(String)
}

public func validate(_ token: String) throws(TokenError) -> Claims {
    guard !token.isEmpty else { throw .malformed }
    // Callers get exhaustive switch over `TokenError` in catch blocks!
    return Claims(rawToken: token)
}

Swift 6 Concurrency

1. Actor Reentrancy Safety (In-Flight Deduplication)

State inside an actor can mutate across any await suspension point. Never assume a condition checked before an await still holds after it—coalesce concurrent callers using an in-flight Task dictionary:

public actor UserRepository {
    private var cache: [User.ID: User] = [:]
    private var inFlight: [User.ID: Task<User, any Error>] = [:]
    private let transport: any NetworkTransport

    public init(transport: any NetworkTransport) {
        self.transport = transport
    }

    public func user(for id: User.ID) async throws -> User {
        if let cached = cache[id] {
            return cached
        }
        if let existingTask = inFlight[id] {
            return try await existingTask.value
        }

        let task = Task {
            try await transport.fetchUser(id: id)
        }
        inFlight[id] = task

        do {
            let fetched = try await task.value
            cache[id] = fetched
            inFlight[id] = nil
            return fetched
        } catch {
            inFlight[id] = nil
            throw error
        }
    }
}
2. Sendable & Synchronous Locking (Mutex)
  • Prefer value types (struct, enum) and actor types for natural Sendable conformance.
  • When synchronous thread-safe state is required without async suspension, prefer Swift 6's Mutex (from Synchronization), which is naturally Sendable without @unchecked:
import Synchronization

public final class MetricsCounter: Sendable {
    private let count = Mutex(0)

    public init() {}

    public func increment() {
        count.withLock { $0 += 1 }
    }

    public var currentValue: Int {
        count.withLock { $0 }
    }
}

Note on @unchecked Sendable: Only declare final class MyType: @unchecked Sendable when wrapping legacy synchronization primitives (such as OSAllocatedUnfairLock or DispatchQueue), and always document the synchronization invariant in a comment.

3. Structured Concurrency & Cancellation-Safe Streams

Prefer withThrowingTaskGroup over unstructured Task {} loops, and always wire onTermination when bridging callback APIs into AsyncStream:

public func fetchAll(_ urls: [URL]) async throws -> [Data] {
    try await withThrowingTaskGroup(of: (Int, Data).self) { group in
        for (index, url) in urls.enumerated() {
            group.addTask {
                (index, try await fetch(url))
            }
        }

        var indexedResults: [(Int, Data)] = []
        indexedResults.reserveCapacity(urls.count)
        for try await pair in group {
            indexedResults.append(pair)
        }
        return indexedResults.sorted { $0.0 < $1.0 }.map(\.1)
    }
}

Result-Builder APIs

Use @resultBuilder with a homogeneous component enum (or buildExpression overloads) so callers can compose requests in any order with optional branches:

// CALL SITE:
let request = HTTPRequest(url: endpoint) {
    Method(.post)
    Header("Accept", "application/json")
    if includeAuth {
        BearerToken(token)
    }
    Timeout(.seconds(30))
}

// IMPLEMENTATION:
public enum RequestComponent: Sendable {
    case method(HTTPMethod)
    case header(String, String)
    case bearerToken(String)
    case timeout(Duration)
}

public func Method(_ method: HTTPMethod) -> RequestComponent { .method(method) }
public func Header(_ name: String, _ value: String) -> RequestComponent { .header(name, value) }
public func BearerToken(_ token: String) -> RequestComponent { .bearerToken(token) }
public func Timeout(_ duration: Duration) -> RequestComponent { .timeout(duration) }

@resultBuilder
public struct RequestBuilder {
    public static func buildExpression(_ component: RequestComponent) -> [RequestComponent] {
        [component]
    }
    public static func buildBlock(_ components: [RequestComponent]...) -> [RequestComponent] {
        components.flatMap { $0 }
    }
    public static func buildOptional(_ component: [RequestComponent]?) -> [RequestComponent] {
        component ?? []
    }
    public static func buildEither(first component: [RequestComponent]) -> [RequestComponent] {
        component
    }
    public static func buildEither(second component: [RequestComponent]) -> [RequestComponent] {
        component
    }
}

public struct HTTPRequest: Sendable {
    public let url: URL
    public let components: [RequestComponent]

    public init(url: URL, @RequestBuilder content: () -> [RequestComponent]) {
        self.url = url
        self.components = content()
    }
}

Library Evolution

1. Public vs. Internal Boundaries & Deprecation

Keep stored properties private or internal unless direct mutation is part of the contract, and provide actionable renamed: or message: guidance on deprecations:

public struct Endpoint: Sendable, Equatable {
    public let path: String
    internal let cacheKey: String

    public init(path: String) {
        self.path = path
        self.cacheKey = path.lowercased()
    }

    @available(*, deprecated, renamed: "init(path:)", message: "Pass a path string directly.")
    public init(rawPath: String) {
        self.init(path: rawPath)
    }
}
2. Constrained Collection Extensions

Provide ergonomic extensions on standard library protocols by returning new sorted/filtered collections rather than mutating in place:

public protocol Sortable {
    associatedtype SortKey: Comparable
    var sortKey: SortKey { get }
}

extension Sequence where Element: Sortable {
    public func sortedByKey() -> [Element] {
        sorted { $0.sortKey < $1.sortKey }
    }
}

Common Tasks

Designing a Public SDK API
  1. Call site first: Draft Layer 1 (zero-config), Layer 2 (fluent config), and Layer 3 (protocol DI) usage snippets.
  2. Start at Troposphere: Require only essential parameters in public init (e.g., baseURL), defaulting policies like timeout and retryPolicy.
  3. Ensure Sendable discipline: Mark public value types, protocols (protocol Transport: Sendable), and closures (@Sendable) for Swift 6 compatibility.
  4. Plan for evolution: Avoid exposing public protocols with no default implementations when a concrete struct suffices; use @_spi(Experimental) for unstable APIs.
Migrating a Library to Swift 6
  1. Enable complete concurrency checking (swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] or Swift 6 language mode).
  2. Audit actors for state assumptions across await suspension points.
  3. Replace NSLock + @unchecked Sendable classes with Mutex or actor.
  4. Add : Sendable to public dependency injection protocols so mocks and implementations can cross actor boundaries.

Rejection Criteria

In addition to the core rejections in stratos-core/SKILL.md, reject:

  • Actor Reentrancy Bugs: Checking actor state before an await and mutating afterwards without re-validating or deduplicating in-flight tasks.
  • Unjustified @unchecked Sendable: Using @unchecked Sendable to silence compiler errors without a proven lock invariant (or writing invalid @unchecked Sendable attribute syntax instead of : @unchecked Sendable).
  • Non-Sendable Public Protocols: Defining async service/transport protocols without : Sendable, which makes them unusable from actors and @MainActor views in Swift 6.
  • Rigid Result Builders: Designing @resultBuilder blocks that only accept a fixed tuple of arguments in a mandatory order.
  • Untyped Dictionaries & Any: Exposing [String: Any] in public APIs instead of Codable structs, enums, or generics.

See Also

Further Reading

  • On Progressive Disclosure in Swift (Swift Craft 2025) — Doug Gregor explains how Swift applies Progressive Disclosure to language design, including Typed Throws, Non-Copyable Types, and concurrency evolution.

Comments

Markdown supported