From 5648132fa8886c5be891da2fd74169d680ed169c Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 9 May 2026 19:44:44 -0400 Subject: [PATCH 1/4] startup: add ergonomic app-server startup errors --- README.md | 6 + .../Public/CodexAppServer+Bootstrap.swift | 54 ++++++++ Sources/SwiftASB/Public/CodexAppServer.swift | 62 ++++++++- Sources/SwiftASB/Public/CodexErrors.swift | 69 ++++++++++ .../SwiftASB/SwiftASB.docc/CodexAppServer.md | 13 +- .../GettingStartedWithSwiftASB.md | 26 ++-- .../Public/CodexAppServerTestSupport.swift | 10 ++ .../Public/CodexAppServerTests.swift | 122 ++++++++++++++++++ 8 files changed, 338 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 01887e2..afdd727 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ For copy-pasteable startup code, open the DocC getting-started guide: - `Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md` +Most clients should start with `CodexAppServer.start(_:)`. The one-call startup +API launches the local Codex app-server, verifies the selected Codex CLI against +SwiftASB's reviewed compatibility window, initializes the session, and throws +typed `CodexAppServerStartupError` values for missing, incompatible, or +unparseable CLI installs. + ## Usage Use SwiftASB when an app needs to show what Codex is doing right now, keep recent command and file activity visible, answer interactive requests, or build SwiftUI state around a running Codex turn. diff --git a/Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift b/Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift index 379a934..0dc38be 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift @@ -1,6 +1,60 @@ import Foundation extension CodexAppServer { + /// Compatibility policy applied by the ergonomic startup call. + public enum StartupCompatibilityPolicy: Sendable, Equatable { + /// Require the selected Codex CLI version to be inside SwiftASB's + /// documented reviewed support window before initializing. + case requireReviewedSupportWindow + + /// Start and initialize even when the selected Codex CLI version is + /// outside SwiftASB's documented reviewed support window. + case allowOutsideReviewedSupportWindow + } + + /// One-call startup request for launching and initializing the app-server. + public struct StartupRequest: Sendable, Equatable { + public var compatibilityPolicy: StartupCompatibilityPolicy + public var initializeRequest: InitializeRequest + + /// Creates a startup request. + /// + /// By default, SwiftASB requires the selected Codex CLI version to be + /// inside the documented reviewed support window before it sends the + /// initialize handshake. + public init( + compatibilityPolicy: StartupCompatibilityPolicy = .requireReviewedSupportWindow, + initializeRequest: InitializeRequest + ) { + self.compatibilityPolicy = compatibilityPolicy + self.initializeRequest = initializeRequest + } + + /// Creates a startup request from client metadata. + /// + /// Omitting `capabilities` sends an empty capability set during the + /// initialize handshake. + public init( + compatibilityPolicy: StartupCompatibilityPolicy = .requireReviewedSupportWindow, + capabilities: InitializeCapabilities = .init(), + clientInfo: ClientInfo + ) { + self.init( + compatibilityPolicy: compatibilityPolicy, + initializeRequest: .init( + capabilities: capabilities, + clientInfo: clientInfo + ) + ) + } + } + + /// Successful one-call startup result. + public struct StartupSession: Sendable, Equatable { + public let cliExecutableDiagnostics: CLIExecutableDiagnostics + public let initializeSession: InitializeSession + } + /// Diagnostics for the local Codex executable selected at startup. public struct CLIExecutableDiagnostics: Sendable, Equatable { /// Local install location SwiftASB used to find the Codex executable. diff --git a/Sources/SwiftASB/Public/CodexAppServer.swift b/Sources/SwiftASB/Public/CodexAppServer.swift index 3b6b0c4..9e7c2f3 100644 --- a/Sources/SwiftASB/Public/CodexAppServer.swift +++ b/Sources/SwiftASB/Public/CodexAppServer.swift @@ -175,16 +175,42 @@ public actor CodexAppServer { /// diagnostics, thread events, and turn events. public func start() async throws { do { - try await transport.start() - hasStarted = true - hasCompletedInitializeHandshake = false - isStopping = false - startServerEventLoop() + try await startTransport() } catch { throw CodexAppServerError.wrap(error, operation: "start") } } + /// Launches the app-server, validates Codex CLI compatibility, and initializes. + /// + /// Use this one-call startup path for app clients that want a ready + /// app-server session or a typed startup error. The lower-level `start()`, + /// `cliExecutableDiagnostics()`, and `initialize(_:)` calls remain available + /// for clients that intentionally own each step. + public func start(_ request: StartupRequest) async throws -> StartupSession { + do { + try await startTransport() + } catch { + throw CodexAppServerStartupError.startFailure(from: error) + } + + do { + let diagnostics = try await cliExecutableDiagnostics() + try validateStartupCompatibility( + diagnostics, + policy: request.compatibilityPolicy + ) + let session = try await initialize(request.initializeRequest) + return .init( + cliExecutableDiagnostics: diagnostics, + initializeSession: session + ) + } catch { + await stop() + throw CodexAppServerStartupError.initializeFailure(from: error) + } + } + /// Stops the app-server subprocess and finishes all public streams. /// /// Streams finish normally when shutdown is initiated through SwiftASB. @@ -219,6 +245,32 @@ public actor CodexAppServer { outstandingInteractiveRequests.removeAll() } + private func startTransport() async throws { + try await transport.start() + hasStarted = true + hasCompletedInitializeHandshake = false + isStopping = false + startServerEventLoop() + } + + private func validateStartupCompatibility( + _ diagnostics: CLIExecutableDiagnostics, + policy: StartupCompatibilityPolicy + ) throws { + switch (policy, diagnostics.compatibility) { + case (.allowOutsideReviewedSupportWindow, _), (.requireReviewedSupportWindow, .supported): + return + case (.requireReviewedSupportWindow, .outsideDocumentedWindow): + throw CodexAppServerStartupError.incompatibleCodexCLI( + diagnostics: diagnostics + ) + case (.requireReviewedSupportWindow, .unknownVersionFormat): + throw CodexAppServerStartupError.unknownCodexCLIVersion( + diagnostics: diagnostics + ) + } + } + /// Returns diagnostics for the Codex CLI executable selected at startup. /// /// The value is available after `start()` succeeds. Use it to show users diff --git a/Sources/SwiftASB/Public/CodexErrors.swift b/Sources/SwiftASB/Public/CodexErrors.swift index c94dec8..26dbc7a 100644 --- a/Sources/SwiftASB/Public/CodexErrors.swift +++ b/Sources/SwiftASB/Public/CodexErrors.swift @@ -18,6 +18,36 @@ public enum CodexAppServerError: Error, Sendable, LocalizedError, Equatable { } } +/// Error surfaced by the one-call app-server startup API. +public enum CodexAppServerStartupError: Error, Sendable, LocalizedError, Equatable { + case codexCLINotFound(reason: String) + case incompatibleCodexCLI(diagnostics: CodexAppServer.CLIExecutableDiagnostics) + case unknownCodexCLIVersion(diagnostics: CodexAppServer.CLIExecutableDiagnostics) + case launchFailed(reason: String) + case initializeFailed(reason: String) + + public var errorDescription: String? { + switch self { + case let .codexCLINotFound(reason): + return "SwiftASB could not find a compatible Codex CLI executable for app-server startup: \(reason)" + case let .incompatibleCodexCLI(diagnostics): + return """ + SwiftASB found Codex CLI \(diagnostics.versionString), but startup requires a version inside \ + SwiftASB's documented reviewed support window. + """ + case let .unknownCodexCLIVersion(diagnostics): + return """ + SwiftASB found Codex CLI \(diagnostics.versionString), but could not parse its version string \ + against SwiftASB's documented reviewed support window. + """ + case let .launchFailed(reason): + return "SwiftASB could not launch the Codex app-server during startup: \(reason)" + case let .initializeFailed(reason): + return "SwiftASB launched the Codex app-server but could not complete startup initialization: \(reason)" + } + } +} + internal extension CodexAppServerError { static func wrap(_ error: Error, operation: String) -> Self { if let appServerError = error as? CodexAppServerError { @@ -44,3 +74,42 @@ internal extension CodexAppServerError { ) } } + +internal extension CodexAppServerStartupError { + static func startFailure(from error: Error) -> Self { + if let startupError = error as? CodexAppServerStartupError { + return startupError + } + + if let transportError = error as? CodexTransportError { + switch transportError { + case let .executableDiscoveryFailed(reason): + return .codexCLINotFound(reason: reason) + case .alreadyStarted: + return .launchFailed(reason: transportError.localizedDescription) + case .failedToLaunch: + return .launchFailed(reason: transportError.localizedDescription) + default: + return .launchFailed(reason: transportError.localizedDescription) + } + } + + if let appServerError = error as? CodexAppServerError { + return .launchFailed(reason: appServerError.localizedDescription) + } + + return .launchFailed(reason: String(describing: error)) + } + + static func initializeFailure(from error: Error) -> Self { + if let startupError = error as? CodexAppServerStartupError { + return startupError + } + + if let appServerError = error as? CodexAppServerError { + return .initializeFailed(reason: appServerError.localizedDescription) + } + + return .initializeFailed(reason: String(describing: error)) + } +} diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md b/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md index 69d2fd6..87c69df 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md @@ -10,9 +10,7 @@ Create one app-server actor for a client process or window group that should sha ```swift let appServer = CodexAppServer() -try await appServer.start() - -let session = try await appServer.initialize( +let startup = try await appServer.start( .init( clientInfo: .init( name: "ExampleClient", @@ -29,7 +27,9 @@ Always call ``stop()`` when the owner is done with the subprocess. ## Connection Lifecycle -Call ``start()`` before sending protocol requests. Call ``initialize(_:)`` once the transport is running; SwiftASB sends the required `initialized` notification after a successful response. +Call ``start(_:)`` when a client wants SwiftASB to own the normal startup sequence. The one-call startup path launches the subprocess, validates Codex CLI compatibility, performs the initialize handshake, and returns ``StartupSession``. It throws ``CodexAppServerStartupError`` when startup fails before the app-server session is ready. + +Use the lower-level ``start()`` and ``initialize(_:)`` pair when a client intentionally owns each step. SwiftASB sends the required `initialized` notification after a successful initialize response. Use ``cliExecutableDiagnostics()`` when a UI or command-line tool needs to explain which `codex` executable was found and whether its version is inside the documented compatibility window. @@ -78,6 +78,8 @@ Set ``ThreadResumeRequest/excludeTurns`` or ``ThreadForkRequest/excludeTurns`` w - ``Configuration`` - ``CLIExecutableDiagnostics`` +- ``StartupCompatibilityPolicy`` +- ``CodexAppServerStartupError`` - ``diagnosticEvents()`` - ``CodexDiagnosticEvent`` - ``featureOperationEvents()`` @@ -86,8 +88,11 @@ Set ``ThreadResumeRequest/excludeTurns`` or ``ThreadForkRequest/excludeTurns`` w ### Startup - ``start()`` +- ``start(_:)`` - ``stop()`` - ``initialize(_:)`` +- ``StartupRequest`` +- ``StartupSession`` - ``InitializeRequest`` - ``InitializeCapabilities`` - ``ClientInfo`` diff --git a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md index 93fa3fe..0fc92eb 100644 --- a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md +++ b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md @@ -11,17 +11,11 @@ import SwiftASB func runOneTurn() async throws { let appServer = CodexAppServer() - try await appServer.start() defer { Task { await appServer.stop() } } - let diagnostics = try await appServer.cliExecutableDiagnostics() - guard case .supported = diagnostics.compatibility else { - throw RuntimeError("Unsupported Codex CLI: \(diagnostics.versionString)") - } - - try await appServer.initialize( + let startup = try await appServer.start( .init( clientInfo: .init( name: "ExampleClient", @@ -30,6 +24,7 @@ func runOneTurn() async throws { ) ) ) + print("Started Codex:", startup.cliExecutableDiagnostics.versionString) let thread = try await appServer.startThread( .init( @@ -50,18 +45,15 @@ func runOneTurn() async throws { } } -struct RuntimeError: Error, CustomStringConvertible { - var description: String - - init(_ description: String) { - self.description = description - } -} ``` ## Startup Order -Call ``CodexAppServer/start()`` before every other protocol operation. Then call ``CodexAppServer/initialize(_:)`` once. SwiftASB sends the required `initialized` notification after the initialize response succeeds. +For most clients, call ``CodexAppServer/start(_:)`` with a ``CodexAppServer/StartupRequest``. That launches the local Codex app-server subprocess, checks the selected Codex CLI against SwiftASB's reviewed compatibility window, sends `initialize`, sends the required `initialized` notification, and returns a ``CodexAppServer/StartupSession``. + +If startup fails before the session is ready, SwiftASB throws ``CodexAppServerStartupError`` with a typed reason such as a missing Codex CLI executable, an incompatible CLI version, an unparseable CLI version string, a launch failure, or an initialize failure. + +Call ``CodexAppServer/start()`` and then ``CodexAppServer/initialize(_:)`` only when the client intentionally owns each startup step. This lower-level path is useful for custom compatibility policy, diagnostics-only startup screens, and tests that need to inspect the selected binary before deciding whether to initialize. ``CodexAppServer/cliExecutableDiagnostics()`` is available after startup and before initialization. Use it when a UI or command-line client needs to show which `codex` executable was launched and whether it is inside SwiftASB's reviewed compatibility window. @@ -74,8 +66,12 @@ Call ``CodexAppServer/start()`` before every other protocol operation. Then call ### Startup - ``CodexAppServer/start()`` +- ``CodexAppServer/start(_:)`` - ``CodexAppServer/stop()`` - ``CodexAppServer/initialize(_:)`` +- ``CodexAppServer/StartupRequest`` +- ``CodexAppServer/StartupSession`` +- ``CodexAppServerStartupError`` - ``CodexAppServer/cliExecutableDiagnostics()`` ### First Thread And Turn diff --git a/Tests/SwiftASBTests/Public/CodexAppServerTestSupport.swift b/Tests/SwiftASBTests/Public/CodexAppServerTestSupport.swift index d9fa105..8d0c8d9 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerTestSupport.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerTestSupport.swift @@ -51,12 +51,14 @@ actor FakeCodexAppServerTransport: CodexAppServerTransporting { private var commandExecResultQueue: [[String: Any]] private var appSnapshotResponseDelayNanoseconds: UInt64 = 0 private let resolvedExecutable: CodexCLIExecutableResolver.Resolution? + private let startError: CodexTransportError? private var started = false private var initializedSeen = false private var serverEventContinuation: AsyncStream.Continuation? init( executableResolution: CodexCLIExecutableResolver.Resolution? = nil, + startError: CodexTransportError? = nil, threadListResult: [String: Any]? = nil, threadListResultQueue: [[String: Any]] = [], threadReadResult: [String: Any]? = nil, @@ -76,6 +78,7 @@ actor FakeCodexAppServerTransport: CodexAppServerTransporting { commandExecResultQueue: [[String: Any]] = [] ) { self.resolvedExecutable = executableResolution + self.startError = startError self.threadListResult = threadListResult self.threadListResultQueue = threadListResultQueue self.threadReadResult = threadReadResult @@ -107,7 +110,14 @@ actor FakeCodexAppServerTransport: CodexAppServerTransporting { recordedRequestPayloads[method] ?? [] } + var isStarted: Bool { + started + } + func start() throws { + if let startError { + throw startError + } started = true initializedSeen = false } diff --git a/Tests/SwiftASBTests/Public/CodexAppServerTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerTests.swift index c7c5776..f7275ab 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerTests.swift @@ -100,6 +100,128 @@ struct CodexAppServerTests { await client.stop() } + @Test("starts and initializes through the ergonomic startup request") + func startsAndInitializesThroughStartupRequest() async throws { + let transport = FakeCodexAppServerTransport( + executableResolution: .init( + launchExecutableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + launchArgumentsPrefix: [], + resolvedExecutableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + source: .homebrewAppleSilicon, + versionString: "codex-cli 0.130.0", + compatibility: .supported(documentedWindow: "0.130.x") + ) + ) + let client = CodexAppServer(transport: transport) + + let startup = try await client.start( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + + #expect(startup.cliExecutableDiagnostics.versionString == "codex-cli 0.130.0") + #expect(startup.initializeSession.codexHome == "/Users/galew/.codex") + #expect(await transport.recordedMethods == ["initialize", "initialized"]) + + await client.stop() + } + + @Test("startup request throws a typed error for an unsupported Codex CLI") + func startupRequestThrowsTypedUnsupportedCLIError() async throws { + let transport = FakeCodexAppServerTransport( + executableResolution: .init( + launchExecutableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + launchArgumentsPrefix: [], + resolvedExecutableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + source: .homebrewAppleSilicon, + versionString: "codex-cli 0.128.0", + compatibility: .outsideDocumentedWindow(documentedWindow: "0.130.x") + ) + ) + let client = CodexAppServer(transport: transport) + + await #expect(throws: CodexAppServerStartupError.incompatibleCodexCLI( + diagnostics: .init( + source: .homebrewAppleSilicon, + resolvedExecutablePath: "/opt/homebrew/bin/codex", + versionString: "codex-cli 0.128.0", + compatibility: .outsideDocumentedWindow(documentedWindow: "0.130.x") + ) + )) { + try await client.start( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + } + + #expect(await transport.recordedMethods.isEmpty) + #expect(await transport.isStarted == false) + } + + @Test("startup request can allow Codex CLI versions outside the reviewed window") + func startupRequestCanAllowUnsupportedCLI() async throws { + let transport = FakeCodexAppServerTransport( + executableResolution: .init( + launchExecutableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + launchArgumentsPrefix: [], + resolvedExecutableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + source: .homebrewAppleSilicon, + versionString: "codex-cli 0.128.0", + compatibility: .outsideDocumentedWindow(documentedWindow: "0.130.x") + ) + ) + let client = CodexAppServer(transport: transport) + + _ = try await client.start( + .init( + compatibilityPolicy: .allowOutsideReviewedSupportWindow, + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + + #expect(await transport.recordedMethods == ["initialize", "initialized"]) + + await client.stop() + } + + @Test("startup request maps executable discovery failures to typed startup errors") + func startupRequestMapsExecutableDiscoveryFailures() async throws { + let transport = FakeCodexAppServerTransport( + startError: .executableDiscoveryFailed( + reason: "SwiftASB could not locate a usable `codex` executable." + ) + ) + let client = CodexAppServer(transport: transport) + + await #expect(throws: CodexAppServerStartupError.codexCLINotFound( + reason: "SwiftASB could not locate a usable `codex` executable." + )) { + try await client.start( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + } + } + @Test("streams SwiftASB feature operation events") func streamsSwiftASBFeatureOperationEvents() async throws { let client = CodexAppServer(transport: FakeCodexAppServerTransport()) From 76b11cbc0a7a83d5642c23a480789102c35ff0d5 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 9 May 2026 19:54:35 -0400 Subject: [PATCH 2/4] docs: align startup session examples --- Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md | 2 +- Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md b/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md index 87c69df..70dc354 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexAppServer.md @@ -10,7 +10,7 @@ Create one app-server actor for a client process or window group that should sha ```swift let appServer = CodexAppServer() -let startup = try await appServer.start( +let session = try await appServer.start( .init( clientInfo: .init( name: "ExampleClient", diff --git a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md index 0fc92eb..5718033 100644 --- a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md +++ b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md @@ -15,7 +15,7 @@ func runOneTurn() async throws { Task { await appServer.stop() } } - let startup = try await appServer.start( + let session = try await appServer.start( .init( clientInfo: .init( name: "ExampleClient", @@ -24,7 +24,7 @@ func runOneTurn() async throws { ) ) ) - print("Started Codex:", startup.cliExecutableDiagnostics.versionString) + print("Started Codex:", session.cliExecutableDiagnostics.versionString) let thread = try await appServer.startThread( .init( From 2b30c51dd81050d1a67b580ad7b613d8bda9f60c Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 9 May 2026 19:55:34 -0400 Subject: [PATCH 3/4] release: bump versions for v1.3.1 --- README.md | 4 ++-- ROADMAP.md | 20 ++++++++++---------- docs/maintainers/v1-public-api-audit.md | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index afdd727..ef6f281 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Listen to the SwiftASB Codex apps promo clip: ### Status -SwiftASB is actively maintained and supported by Gale. Our current API is v1, and `v1.3.0` is the current and latest release. +SwiftASB is actively maintained and supported by Gale. Our current API is v1, and `v1.3.1` is the current and latest release. ### What This Project Is @@ -38,7 +38,7 @@ I built SwiftASB because I saw so many others building and forking existing Apps Add SwiftASB to your `Package.swift` dependencies: ```swift -.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.3.0"), +.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.3.1"), ``` Then add the library product to your target dependencies: diff --git a/ROADMAP.md b/ROADMAP.md index a8a1a3d..3244b26 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -77,7 +77,7 @@ | Non-UI local history-reading helpers | `Partially shipped` | `CodexThread` now exposes a lightweight `HistoryWindow` page shape for recent local history, older or newer local windows around a known boundary turn id, centered `windowAroundTurn(...)` reads, centered `windowAroundItem(...)` reads, direct `ClosedTurn` reads for one turn, and convenience array helpers over those same windows. This gives non-UI callers an intentional path into the local history store without binding a UI-oriented observable, while still deferring a broader public cursor model, transcript search surface, and richer history-query helpers. | | Public API curation | `Shipped / ongoing` | The source-organization pass has split app-wide model, MCP, thread-management, history, and observable companion values into focused public files while preserving `CodexAppServer`, `CodexThread`, and `CodexTurnHandle` as the three real owners. The connected public-surface review closed the v1 ownership model; post-v1 curation now includes app-server-owned project identity and thread source facts for launcher UI without exposing generated wire models. Future curation should stay tied to concrete public API additions. | | DocC documentation | `Shipped / ongoing` | `Sources/SwiftASB/SwiftASB.docc/` contains a package landing page, public-handle extension pages, conceptual articles for app-wide capabilities, interactive lifecycle, thread management, history/observable companions, generated-wire boundary notes, and copy-pasteable walkthroughs for startup, progress/approval handling, diagnostics/history, and SwiftUI observable companions. The catalog is validated through Xcode `docbuild`; future work is ordinary stale-link, prose, and symbol-comment refinement as the public API grows. | -| Swift Package Index readiness | `Shipped` | `.spi.yml` declares `SwiftASB` as the documentation target, and Swift Package Index lists `gaelic-ghost/SwiftASB` with a documentation link, compatibility/build results, Package ID `9B5839D9-9551-473F-A939-841534A3FC55`, and a 2026-05-06 update timestamp for the latest confirmed indexed release. Recheck SPI after the `v1.3.0` tag is published. | +| Swift Package Index readiness | `Shipped` | `.spi.yml` declares `SwiftASB` as the documentation target, and Swift Package Index lists `gaelic-ghost/SwiftASB` with a documentation link, compatibility/build results, Package ID `9B5839D9-9551-473F-A939-841534A3FC55`, and a 2026-05-06 update timestamp for the latest confirmed indexed release. Recheck SPI after the `v1.3.1` tag is published. | | Contributor documentation split | `Shipped` | `README.md` is now focused on Swift and SwiftUI package users, while `CONTRIBUTING.md` owns contributor setup, validation, DocC, live-test flags, generated-wire refresh, and PR expectations. | | `CodexTurnHandle` live observable companion | `Partially shipped` | `CodexTurnHandle` owns a live `Minimap` companion that is attached when the handle is created and maintains current-state call snapshots for command, file-edit, dynamic-tool, collab-tool, and MCP item activity. It also now mirrors whether thread context compaction is active for the turn and supports explicit `complete()` handoff into a caller-owned sealed turn snapshot. | | Additional turn event mapping | `Partially shipped` | The public event layer covers the current interactive lifecycle plus the item-start and item-complete events needed for observable call-state mirrors. Raw command-output and file-change-output deltas now stay internal as transport detail but drive the shipped `RecentCommands` and `RecentFiles` companions, and streamed or patch-updated payloads are preserved when later completed snapshots are thinner. Richer MCP-progress detail still remains internal, while warning, guardian-warning, config-warning, deprecation, MCP-server-status, remote-control-status, model-reroute, and model-verification notifications now surface through hand-owned diagnostic events. | @@ -105,7 +105,7 @@ The next meaningful package step is no longer proving the v1 interactive lifecycle, SPI visibility, basic history hydration, first-pass reconciliation, or command-approval completion. Those slices now exist and shipped in the -`v1.3.0` baseline. +`v1.3.1` baseline. The next meaningful work is to widen the reviewed app-server schema and protocol coverage before adding more public query descriptors. Descriptors should compile @@ -216,7 +216,7 @@ That means the current priority order is: ## V1 Readiness Checklist -This checklist records the work that made `SwiftASB` ready for the `v1.3.0` +This checklist records the work that made `SwiftASB` ready for the `v1.3.1` tag. The goal was not to make every possible app-server feature public before v1. The goal was to make the supported lifecycle honest, durable, well documented, and intentionally shaped. @@ -416,8 +416,8 @@ workflow earns them in a later feature release. ### Documentation And Examples -- [x] Update stale release references after the `v1.3.0` release. - Decision: README now names `v1.3.0` as the current released baseline and no +- [x] Update stale release references after the `v1.3.1` release. + Decision: README now names `v1.3.1` as the current released baseline and no longer describes the package as early development. - [x] Finish DocC symbol comments for the supported lifecycle, not just the conceptual articles. @@ -602,10 +602,10 @@ workflow earns them in a later feature release. the `release/v1.0.0` branch on 2026-05-02 and on the `release/v1.0.1-prep` branch on 2026-05-02. - [x] Decide whether another targeted `v0.9.x` patch release is needed before - `v1.3.0`, or whether the remaining work should go straight into the v1 + `v1.3.1`, or whether the remaining work should go straight into the v1 release branch. Decision: no additional `v0.9.x` patch is needed. The remaining work should go - straight into the `v1.3.0` release branch. + straight into the `v1.3.1` release branch. - [x] Prepare v1 release notes with explicit sections for public surface, intentionally internal surfaces, compatibility window, migration notes, validation performed, and known post-v1 work. @@ -659,7 +659,7 @@ workflow earns them in a later feature release. #### Migration Notes - Existing `v0.9.x` consumers should update the SwiftPM dependency to - `from: "1.3.0"` once the tag is published. + `from: "1.3.1"` once the tag is published. - The v1 API surface has removed stale pre-v1 compatibility shims and phantom fields that no longer exist in the reviewed `v0.128.0` schema. - Same-thread overlapping turns are rejected client-side with @@ -684,7 +684,7 @@ workflow earns them in a later feature release. - Keep an eye on future Swift Package Index builds after compatibility-window or DocC changes; the `v1.1.1` listing and documentation link are live, and - `v1.3.0` should be rechecked after the patch tag is indexed. + `v1.3.1` should be rechecked after the patch tag is indexed. - Add broader live server-request coverage for permissions and MCP elicitation if those become stronger public runtime guarantees. - Continue tuning recent companion cache calibration, richer file previews, @@ -1259,7 +1259,7 @@ Completed - [x] Add version-compatibility policy notes for the local Codex binary. - [x] Refresh the compatibility window and promoted generated snapshot against the current `v0.124.0` schema dump once the added endpoint, notification, and field families have been classified. - [x] Curate the public API before v1 by splitting large source files along existing responsibility boundaries where still helpful, tightening public names/defaults, and finishing targeted source-level symbol documentation for the supported lifecycle. - Decision: completed for the `v1.3.0` boundary through the public API audit, + Decision: completed for the `v1.3.1` boundary through the public API audit, symbol inventory, source-comment pass, and focused public file organization. - [x] Add the first DocC documentation catalog before v1, including a package landing page, public-handle topic groups, and conceptual articles for the interactive lifecycle, history companions, and generated-wire boundary. - [x] Validate the DocC catalog through Xcode `docbuild` and document the maintainer command. diff --git a/docs/maintainers/v1-public-api-audit.md b/docs/maintainers/v1-public-api-audit.md index 5b4686c..a19a448 100644 --- a/docs/maintainers/v1-public-api-audit.md +++ b/docs/maintainers/v1-public-api-audit.md @@ -2,7 +2,7 @@ This document is the working checklist for the `SwiftASB` v1 public API curation pass. The goal is to freeze a compact, Swift-native surface for the -supported app-server lifecycle before `v1.3.0`, not to expose every generated +supported app-server lifecycle before `v1.3.1`, not to expose every generated wire family. ## Current Public Source Inventory @@ -429,7 +429,7 @@ Use these decisions for every public symbol: - [x] Add symbol comments for every stable v1 public type and method that is not self-explanatory from its declaration. - Decision: complete for the `v1.3.0` release boundary. Default-bearing public + Decision: complete for the `v1.3.1` release boundary. Default-bearing public initializers and methods now document whether omission delegates to Codex, chooses a SwiftASB local-history/UI default, or applies an explicit safety default such as `.turn` or `.unchanged`. The source-level pass also covers the @@ -508,7 +508,7 @@ Use these decisions for every public symbol: Decision: covered by the startup, progress/approval, diagnostics/history, and SwiftUI observable companion walkthroughs in `Sources/SwiftASB/SwiftASB.docc/`. - [x] Update stale README release references before the next release. - Decision: README now names `v1.3.0` as the current released baseline. + Decision: README now names `v1.3.1` as the current released baseline. - [x] Confirm README, DocC, and this audit use the same v1 release boundary. Decision: README, DocC, and this audit now describe the same narrow v1 promise: app-server lifecycle, app-wide capability reads, stored-thread From 6a927d213ea35a1a2fabb71f25979938475746ba Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 9 May 2026 20:01:40 -0400 Subject: [PATCH 4/4] docs: clarify startup compatibility policy ownership --- Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md index 5718033..a5df734 100644 --- a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md +++ b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md @@ -53,7 +53,7 @@ For most clients, call ``CodexAppServer/start(_:)`` with a ``CodexAppServer/Star If startup fails before the session is ready, SwiftASB throws ``CodexAppServerStartupError`` with a typed reason such as a missing Codex CLI executable, an incompatible CLI version, an unparseable CLI version string, a launch failure, or an initialize failure. -Call ``CodexAppServer/start()`` and then ``CodexAppServer/initialize(_:)`` only when the client intentionally owns each startup step. This lower-level path is useful for custom compatibility policy, diagnostics-only startup screens, and tests that need to inspect the selected binary before deciding whether to initialize. +Call ``CodexAppServer/start()`` and then ``CodexAppServer/initialize(_:)`` only when the client intentionally owns each startup step. For a custom compatibility policy that still fits one-call startup, configure the ``CodexAppServer/StartupRequest`` passed to ``CodexAppServer/start(_:)``. Use the lower-level path for diagnostics-only startup screens or tests that need to inspect the selected binary before deciding whether to call ``CodexAppServer/initialize(_:)``. ``CodexAppServer/cliExecutableDiagnostics()`` is available after startup and before initialization. Use it when a UI or command-line client needs to show which `codex` executable was launched and whether it is inside SwiftASB's reviewed compatibility window.