Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid presenting v1.3.1 as already released before the tag is published.

These lines can briefly break user onboarding if the tag isn’t live yet (install command fails and “latest release” is inaccurate). Consider wording this as pending until publication, or merge this text only once the tag/release is public.

Suggested wording tweak
-SwiftASB is actively maintained and supported by Gale. Our current API is v1, and `v1.3.1` is the current and latest release.
+SwiftASB is actively maintained and supported by Gale. Our current API is v1.
+The next release is `v1.3.1` (update this line to “latest release” once published).
-.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.3.1"),
+.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.3.1"), // after v1.3.1 is published

As per coding guidelines, README.md: "Keep the README product-facing and approachable for package users and their agents".

Also applies to: 41-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 26, The README currently states "v1.3.1" as the current
and latest release; change that sentence (the line containing "SwiftASB is
actively maintained..." and the literal "v1.3.1") to avoid asserting the tag is
published—e.g., say the API is v1 and that "v1.3.1 is the intended/upcoming
release" or "latest release (v1.3.1) pending publication" and only merge the
definitive "latest" phrasing after the tag is actually published.


### What This Project Is

Expand All @@ -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:
Expand All @@ -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.
Expand Down
20 changes: 10 additions & 10 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions Sources/SwiftASB/Public/CodexAppServer+Bootstrap.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
62 changes: 57 additions & 5 deletions Sources/SwiftASB/Public/CodexAppServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions Sources/SwiftASB/Public/CodexErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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))
}
}
Loading