Skip to content

Commit 82ea49d

Browse files
authored
Merge pull request #78 from gaelic-ghost/startup/ergonomic-startup-errors
release: prepare v1.3.1
2 parents a7db7e6 + 6a927d2 commit 82ea49d

10 files changed

Lines changed: 353 additions & 39 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Listen to the SwiftASB Codex apps promo clip:
2323

2424
### Status
2525

26-
SwiftASB is actively maintained and supported by Gale. Our current API is v1, and `v1.3.0` is the current and latest release.
26+
SwiftASB is actively maintained and supported by Gale. Our current API is v1, and `v1.3.1` is the current and latest release.
2727

2828
### What This Project Is
2929

@@ -38,7 +38,7 @@ I built SwiftASB because I saw so many others building and forking existing Apps
3838
Add SwiftASB to your `Package.swift` dependencies:
3939

4040
```swift
41-
.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.3.0"),
41+
.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.3.1"),
4242
```
4343

4444
Then add the library product to your target dependencies:
@@ -64,6 +64,12 @@ For copy-pasteable startup code, open the DocC getting-started guide:
6464

6565
- `Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md`
6666

67+
Most clients should start with `CodexAppServer.start(_:)`. The one-call startup
68+
API launches the local Codex app-server, verifies the selected Codex CLI against
69+
SwiftASB's reviewed compatibility window, initializes the session, and throws
70+
typed `CodexAppServerStartupError` values for missing, incompatible, or
71+
unparseable CLI installs.
72+
6773
## Usage
6874

6975
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.

ROADMAP.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
| 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. |
7878
| 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. |
7979
| 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. |
80-
| 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. |
80+
| 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. |
8181
| 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. |
8282
| `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. |
8383
| 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 @@
105105
The next meaningful package step is no longer proving the v1 interactive
106106
lifecycle, SPI visibility, basic history hydration, first-pass reconciliation,
107107
or command-approval completion. Those slices now exist and shipped in the
108-
`v1.3.0` baseline.
108+
`v1.3.1` baseline.
109109

110110
The next meaningful work is to widen the reviewed app-server schema and protocol
111111
coverage before adding more public query descriptors. Descriptors should compile
@@ -216,7 +216,7 @@ That means the current priority order is:
216216

217217
## V1 Readiness Checklist
218218

219-
This checklist records the work that made `SwiftASB` ready for the `v1.3.0`
219+
This checklist records the work that made `SwiftASB` ready for the `v1.3.1`
220220
tag. The goal was not to make every possible app-server feature public before
221221
v1. The goal was to make the supported lifecycle honest, durable, well
222222
documented, and intentionally shaped.
@@ -416,8 +416,8 @@ workflow earns them in a later feature release.
416416

417417
### Documentation And Examples
418418

419-
- [x] Update stale release references after the `v1.3.0` release.
420-
Decision: README now names `v1.3.0` as the current released baseline and no
419+
- [x] Update stale release references after the `v1.3.1` release.
420+
Decision: README now names `v1.3.1` as the current released baseline and no
421421
longer describes the package as early development.
422422
- [x] Finish DocC symbol comments for the supported lifecycle, not just the
423423
conceptual articles.
@@ -602,10 +602,10 @@ workflow earns them in a later feature release.
602602
the `release/v1.0.0` branch on 2026-05-02 and on the
603603
`release/v1.0.1-prep` branch on 2026-05-02.
604604
- [x] Decide whether another targeted `v0.9.x` patch release is needed before
605-
`v1.3.0`, or whether the remaining work should go straight into the v1
605+
`v1.3.1`, or whether the remaining work should go straight into the v1
606606
release branch.
607607
Decision: no additional `v0.9.x` patch is needed. The remaining work should go
608-
straight into the `v1.3.0` release branch.
608+
straight into the `v1.3.1` release branch.
609609
- [x] Prepare v1 release notes with explicit sections for public surface,
610610
intentionally internal surfaces, compatibility window, migration notes,
611611
validation performed, and known post-v1 work.
@@ -659,7 +659,7 @@ workflow earns them in a later feature release.
659659
#### Migration Notes
660660

661661
- Existing `v0.9.x` consumers should update the SwiftPM dependency to
662-
`from: "1.3.0"` once the tag is published.
662+
`from: "1.3.1"` once the tag is published.
663663
- The v1 API surface has removed stale pre-v1 compatibility shims and phantom
664664
fields that no longer exist in the reviewed `v0.128.0` schema.
665665
- Same-thread overlapping turns are rejected client-side with
@@ -684,7 +684,7 @@ workflow earns them in a later feature release.
684684

685685
- Keep an eye on future Swift Package Index builds after compatibility-window
686686
or DocC changes; the `v1.1.1` listing and documentation link are live, and
687-
`v1.3.0` should be rechecked after the patch tag is indexed.
687+
`v1.3.1` should be rechecked after the patch tag is indexed.
688688
- Add broader live server-request coverage for permissions and MCP elicitation
689689
if those become stronger public runtime guarantees.
690690
- Continue tuning recent companion cache calibration, richer file previews,
@@ -1259,7 +1259,7 @@ Completed
12591259
- [x] Add version-compatibility policy notes for the local Codex binary.
12601260
- [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.
12611261
- [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.
1262-
Decision: completed for the `v1.3.0` boundary through the public API audit,
1262+
Decision: completed for the `v1.3.1` boundary through the public API audit,
12631263
symbol inventory, source-comment pass, and focused public file organization.
12641264
- [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.
12651265
- [x] Validate the DocC catalog through Xcode `docbuild` and document the maintainer command.

Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,60 @@
11
import Foundation
22

33
extension CodexAppServer {
4+
/// Compatibility policy applied by the ergonomic startup call.
5+
public enum StartupCompatibilityPolicy: Sendable, Equatable {
6+
/// Require the selected Codex CLI version to be inside SwiftASB's
7+
/// documented reviewed support window before initializing.
8+
case requireReviewedSupportWindow
9+
10+
/// Start and initialize even when the selected Codex CLI version is
11+
/// outside SwiftASB's documented reviewed support window.
12+
case allowOutsideReviewedSupportWindow
13+
}
14+
15+
/// One-call startup request for launching and initializing the app-server.
16+
public struct StartupRequest: Sendable, Equatable {
17+
public var compatibilityPolicy: StartupCompatibilityPolicy
18+
public var initializeRequest: InitializeRequest
19+
20+
/// Creates a startup request.
21+
///
22+
/// By default, SwiftASB requires the selected Codex CLI version to be
23+
/// inside the documented reviewed support window before it sends the
24+
/// initialize handshake.
25+
public init(
26+
compatibilityPolicy: StartupCompatibilityPolicy = .requireReviewedSupportWindow,
27+
initializeRequest: InitializeRequest
28+
) {
29+
self.compatibilityPolicy = compatibilityPolicy
30+
self.initializeRequest = initializeRequest
31+
}
32+
33+
/// Creates a startup request from client metadata.
34+
///
35+
/// Omitting `capabilities` sends an empty capability set during the
36+
/// initialize handshake.
37+
public init(
38+
compatibilityPolicy: StartupCompatibilityPolicy = .requireReviewedSupportWindow,
39+
capabilities: InitializeCapabilities = .init(),
40+
clientInfo: ClientInfo
41+
) {
42+
self.init(
43+
compatibilityPolicy: compatibilityPolicy,
44+
initializeRequest: .init(
45+
capabilities: capabilities,
46+
clientInfo: clientInfo
47+
)
48+
)
49+
}
50+
}
51+
52+
/// Successful one-call startup result.
53+
public struct StartupSession: Sendable, Equatable {
54+
public let cliExecutableDiagnostics: CLIExecutableDiagnostics
55+
public let initializeSession: InitializeSession
56+
}
57+
458
/// Diagnostics for the local Codex executable selected at startup.
559
public struct CLIExecutableDiagnostics: Sendable, Equatable {
660
/// Local install location SwiftASB used to find the Codex executable.

Sources/SwiftASB/Public/CodexAppServer.swift

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,42 @@ public actor CodexAppServer {
175175
/// diagnostics, thread events, and turn events.
176176
public func start() async throws {
177177
do {
178-
try await transport.start()
179-
hasStarted = true
180-
hasCompletedInitializeHandshake = false
181-
isStopping = false
182-
startServerEventLoop()
178+
try await startTransport()
183179
} catch {
184180
throw CodexAppServerError.wrap(error, operation: "start")
185181
}
186182
}
187183

184+
/// Launches the app-server, validates Codex CLI compatibility, and initializes.
185+
///
186+
/// Use this one-call startup path for app clients that want a ready
187+
/// app-server session or a typed startup error. The lower-level `start()`,
188+
/// `cliExecutableDiagnostics()`, and `initialize(_:)` calls remain available
189+
/// for clients that intentionally own each step.
190+
public func start(_ request: StartupRequest) async throws -> StartupSession {
191+
do {
192+
try await startTransport()
193+
} catch {
194+
throw CodexAppServerStartupError.startFailure(from: error)
195+
}
196+
197+
do {
198+
let diagnostics = try await cliExecutableDiagnostics()
199+
try validateStartupCompatibility(
200+
diagnostics,
201+
policy: request.compatibilityPolicy
202+
)
203+
let session = try await initialize(request.initializeRequest)
204+
return .init(
205+
cliExecutableDiagnostics: diagnostics,
206+
initializeSession: session
207+
)
208+
} catch {
209+
await stop()
210+
throw CodexAppServerStartupError.initializeFailure(from: error)
211+
}
212+
}
213+
188214
/// Stops the app-server subprocess and finishes all public streams.
189215
///
190216
/// Streams finish normally when shutdown is initiated through SwiftASB.
@@ -219,6 +245,32 @@ public actor CodexAppServer {
219245
outstandingInteractiveRequests.removeAll()
220246
}
221247

248+
private func startTransport() async throws {
249+
try await transport.start()
250+
hasStarted = true
251+
hasCompletedInitializeHandshake = false
252+
isStopping = false
253+
startServerEventLoop()
254+
}
255+
256+
private func validateStartupCompatibility(
257+
_ diagnostics: CLIExecutableDiagnostics,
258+
policy: StartupCompatibilityPolicy
259+
) throws {
260+
switch (policy, diagnostics.compatibility) {
261+
case (.allowOutsideReviewedSupportWindow, _), (.requireReviewedSupportWindow, .supported):
262+
return
263+
case (.requireReviewedSupportWindow, .outsideDocumentedWindow):
264+
throw CodexAppServerStartupError.incompatibleCodexCLI(
265+
diagnostics: diagnostics
266+
)
267+
case (.requireReviewedSupportWindow, .unknownVersionFormat):
268+
throw CodexAppServerStartupError.unknownCodexCLIVersion(
269+
diagnostics: diagnostics
270+
)
271+
}
272+
}
273+
222274
/// Returns diagnostics for the Codex CLI executable selected at startup.
223275
///
224276
/// The value is available after `start()` succeeds. Use it to show users

Sources/SwiftASB/Public/CodexErrors.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ public enum CodexAppServerError: Error, Sendable, LocalizedError, Equatable {
1818
}
1919
}
2020

21+
/// Error surfaced by the one-call app-server startup API.
22+
public enum CodexAppServerStartupError: Error, Sendable, LocalizedError, Equatable {
23+
case codexCLINotFound(reason: String)
24+
case incompatibleCodexCLI(diagnostics: CodexAppServer.CLIExecutableDiagnostics)
25+
case unknownCodexCLIVersion(diagnostics: CodexAppServer.CLIExecutableDiagnostics)
26+
case launchFailed(reason: String)
27+
case initializeFailed(reason: String)
28+
29+
public var errorDescription: String? {
30+
switch self {
31+
case let .codexCLINotFound(reason):
32+
return "SwiftASB could not find a compatible Codex CLI executable for app-server startup: \(reason)"
33+
case let .incompatibleCodexCLI(diagnostics):
34+
return """
35+
SwiftASB found Codex CLI \(diagnostics.versionString), but startup requires a version inside \
36+
SwiftASB's documented reviewed support window.
37+
"""
38+
case let .unknownCodexCLIVersion(diagnostics):
39+
return """
40+
SwiftASB found Codex CLI \(diagnostics.versionString), but could not parse its version string \
41+
against SwiftASB's documented reviewed support window.
42+
"""
43+
case let .launchFailed(reason):
44+
return "SwiftASB could not launch the Codex app-server during startup: \(reason)"
45+
case let .initializeFailed(reason):
46+
return "SwiftASB launched the Codex app-server but could not complete startup initialization: \(reason)"
47+
}
48+
}
49+
}
50+
2151
internal extension CodexAppServerError {
2252
static func wrap(_ error: Error, operation: String) -> Self {
2353
if let appServerError = error as? CodexAppServerError {
@@ -44,3 +74,42 @@ internal extension CodexAppServerError {
4474
)
4575
}
4676
}
77+
78+
internal extension CodexAppServerStartupError {
79+
static func startFailure(from error: Error) -> Self {
80+
if let startupError = error as? CodexAppServerStartupError {
81+
return startupError
82+
}
83+
84+
if let transportError = error as? CodexTransportError {
85+
switch transportError {
86+
case let .executableDiscoveryFailed(reason):
87+
return .codexCLINotFound(reason: reason)
88+
case .alreadyStarted:
89+
return .launchFailed(reason: transportError.localizedDescription)
90+
case .failedToLaunch:
91+
return .launchFailed(reason: transportError.localizedDescription)
92+
default:
93+
return .launchFailed(reason: transportError.localizedDescription)
94+
}
95+
}
96+
97+
if let appServerError = error as? CodexAppServerError {
98+
return .launchFailed(reason: appServerError.localizedDescription)
99+
}
100+
101+
return .launchFailed(reason: String(describing: error))
102+
}
103+
104+
static func initializeFailure(from error: Error) -> Self {
105+
if let startupError = error as? CodexAppServerStartupError {
106+
return startupError
107+
}
108+
109+
if let appServerError = error as? CodexAppServerError {
110+
return .initializeFailed(reason: appServerError.localizedDescription)
111+
}
112+
113+
return .initializeFailed(reason: String(describing: error))
114+
}
115+
}

0 commit comments

Comments
 (0)