From 5554e19347cdf5fea6b5470089390463ce32a29e Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 30 May 2026 15:41:46 -0400 Subject: [PATCH 1/6] docs: map thread plan and goal companion --- .../thread-plan-goal-companion-plan.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/maintainers/thread-plan-goal-companion-plan.md diff --git a/docs/maintainers/thread-plan-goal-companion-plan.md b/docs/maintainers/thread-plan-goal-companion-plan.md new file mode 100644 index 0000000..4045c0e --- /dev/null +++ b/docs/maintainers/thread-plan-goal-companion-plan.md @@ -0,0 +1,211 @@ +# Thread Plan And Goal Companion Plan + +This note maps the next SwiftASB cleanup pass for Codex plans and goals. The +goal is to make plan and goal state easy for SwiftUI clients to consume without +asking app code to assemble experimental deltas, manually refresh current goals, +or depend on every raw notification as public API. + +## Current Facts + +Official Codex docs describe plan mode as the way to ask Codex for a multi-step +execution plan before implementation work starts. They describe goals as a +persistent thread target, preferably shaped with a plan first. + +The current generated app-server schema exposes: + +- `thread/goal/get` +- `thread/goal/set` +- `thread/goal/clear` +- `thread/goal/updated` +- `thread/goal/cleared` +- `turn/plan/updated` +- `item/plan/delta` + +The promoted wire model currently includes a stable-looking `ThreadGoal` value +with `objective`, `status`, `tokenBudget`, `tokensUsed`, `timeUsedSeconds`, +`createdAt`, and `updatedAt`. Plan updates carry a complete plan snapshot for +one turn. Plan deltas are explicitly marked experimental and warn clients not to +assume concatenated deltas equal the final plan item content. + +The current public SwiftASB API already exposes: + +- `CodexThread.readGoal()` +- `CodexThread.setGoal(_:)` +- `CodexThread.clearGoal()` +- `CodexThreadEvent.goalUpdated` +- `CodexThreadEvent.goalCleared` +- `CodexTurnEvent.planUpdated` +- `CodexTurnEvent.planDelta` +- `CodexThread.Dashboard.goal` +- `CodexTurnHandle.Minimap.latestPlanUpdate` +- `CodexTurnHandle.Minimap.latestPlanDelta` + +That is useful but too raw for routine app UI. Consumers should not need to +reconcile thread-goal reads with live goal notifications, and they should not +need to treat experimental plan deltas as a stable user-facing data source. + +## Proposed Public Shape + +Add one thread-scoped observable companion that owns plan and goal current state. +This is a durable building-block change: it gives SwiftUI clients one object to +store for planning/progress displays while letting SwiftASB simplify lower-level +events over time. + +Recommended name: + +- `CodexThread.Agenda` + +Other viable names: + +- `CodexThread.TaskState` +- `CodexThread.Workspace` +- `CodexThread.Direction` +- `CodexThread.Focus` +- `CodexThread.Planbook` + +`Agenda` is the nicest domain name. It naturally holds both the target and the +ordered work. It is also compact in app code: + +```swift +let agenda = try await thread.makeAgenda() +agenda.goalTitle +agenda.planTitle +agenda.currentPlan.steps +``` + +`TaskState` is the safest literal fallback. It says exactly what the object is: +the thread's current task goal plus the plan state currently known for that +task. It is less charming, but it will age well. + +`Workspace` is viable only if the companion eventually mirrors the whole active +work shape for one thread: current goal, latest accepted plan, proposed plan +text while it streams, and high-level progress. The downside is likely +confusion with `CodexWorkspace`, so it may be too overloaded. + +Preferred recommendation for implementation: + +- Use `CodexThread.Agenda`. +- Add `CodexThread.makeAgenda()` as the construction API. +- Treat the companion as the public way to render current goal and plan state. + +## Companion Responsibilities + +`CodexThread.Agenda` should own: + +- initial goal hydration through `thread/goal/get` +- live goal update and clear notifications +- latest complete turn plan snapshot from `turn/plan/updated` +- proposed plan item text assembled from `item/plan/delta` +- reset or replacement behavior when a later complete plan snapshot arrives +- derived titles for dashboard-style display + +Suggested initial public fields: + +- `threadID: String` +- `goal: CodexThread.Goal?` +- `goalTitle: String` +- `goalStatus: CodexThread.Goal.Status?` +- `currentPlan: Plan?` +- `proposedPlan: ProposedPlan?` +- `planTitle: String` +- `updatedAt: Int?` + +Suggested nested values: + +- `Plan` + - `turnID: String` + - `explanation: String?` + - `steps: [Step]` +- `Plan.Step` + - `id: String` + - `title: String` + - `status: Status` +- `ProposedPlan` + - `turnID: String` + - `items: [Item]` +- `ProposedPlan.Item` + - `id: String` + - `text: String` + +`Plan.Step.id` can be a stable SwiftASB-derived value from the turn id and +step index because upstream plan steps do not expose ids today. Proposed plan +items should keep upstream `itemId`. + +## Dashboard Simplification + +`Dashboard` should become lighter. It should expose only plan and goal summary +text, not the full goal object and not plan delta/update details. + +Recommended dashboard fields: + +- `goalTitle: String` +- `planTitle: String` + +Open question: + +- Keep `Dashboard.goal` deprecated for one minor release, or remove it in the + same public-surface simplification pass if the next release is allowed to + carry a source break. + +The practical effect is that dashboard stays the broad thread status strip, +while agenda becomes the detailed task-progress model. + +## Event Surface Simplification + +The public event stream should move toward events that consumers can safely act +on directly. Raw plan deltas are not that kind of event because upstream marks +them experimental and says the final plan item is authoritative. + +Recommended classification changes: + +- Keep `CodexThreadEvent.goalUpdated` and `.goalCleared` for now only if direct + event observers still need them outside `Agenda`. +- Reclassify `CodexTurnEvent.planDelta` as observable-owned state for `Agenda`, + not a preferred consumer event. +- Consider reclassifying `CodexTurnEvent.planUpdated` as observable-owned too, + once `Agenda.currentPlan` is shipped and documented. + +Near-term conservative path: + +- Ship `Agenda` first. +- Document that `Agenda` is preferred for plans and goals. +- Leave existing events in place for compatibility. +- In the next public API cleanup, deprecate `planDelta` first. + +## Creation And Mutation Boundary + +This pass should not yet create a high-level plan or goal authoring API. The +first implementation should be read/exposure-only plus current low-level goal +set/clear compatibility. + +Later creation APIs can build on `Agenda` once the consumer model is settled: + +- `agenda.setGoal(_ objective: String, tokenBudget: Int?)` +- `agenda.pauseGoal()` +- `agenda.resumeGoal()` +- `agenda.clearGoal()` +- `thread.startPlanningTurn(...)` + +Those should be deliberate convenience APIs, not thin slash-command replicas. + +## Implementation Slices + +1. Add `CodexThread.Agenda` and `makeAgenda()`. +2. Hydrate the current goal on construction and apply thread goal events. +3. Feed turn plan updates and plan deltas into agenda state. +4. Add dashboard `goalTitle` and `planTitle` summaries. +5. Update DocC to recommend agenda for plan/goal UI. +6. Revisit public event deprecations after tests prove agenda covers routine UI. + +## Validation + +Targeted tests should cover: + +- agenda initializes with the current thread goal +- agenda updates and clears goal state from thread events +- agenda derives goal title and status from the current goal +- agenda stores the latest complete plan snapshot from turn plan updates +- agenda accumulates proposed plan text by `itemId` from plan deltas +- agenda replaces or clears proposed state when an authoritative plan update + arrives for the same turn +- dashboard mirrors only `goalTitle` and `planTitle` From 15245c7d214359de90de37eb81d2bcb4172f3247 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 30 May 2026 15:50:06 -0400 Subject: [PATCH 2/6] runtime: add thread agenda companion --- README.md | 4 +- .../SwiftASB/Public/CodexThread+Agenda.swift | 232 ++++++++++++++++++ .../Public/CodexThread+Dashboard.swift | 64 ++++- Sources/SwiftASB/Public/CodexThread.swift | 20 ++ Sources/SwiftASB/SwiftASB.docc/CodexThread.md | 6 +- .../SwiftUIObservableCompanions.md | 10 +- .../ThreadHistoryAndObservables.md | 5 +- .../CodexAppServerCompanionSurfaceTests.swift | 103 ++++++++ .../thread-plan-goal-companion-plan.md | 30 ++- .../v1-public-api-symbol-inventory.md | 7 +- 10 files changed, 455 insertions(+), 26 deletions(-) create mode 100644 Sources/SwiftASB/Public/CodexThread+Agenda.swift diff --git a/README.md b/README.md index 134745d..d202064 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ 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. +Use SwiftASB when an app needs to show what Codex is doing right now, keep plan and goal state visible, keep recent command and file activity visible, answer interactive requests, or build SwiftUI state around a running Codex turn. For app-wide capability and extension UI, `CodexAppServer.makeInventory()` provides observable model capabilities, global MCP summaries, hook diagnostics, apps, skills, plugins, and collaboration modes, with SwiftASB-owned refresh from app-server inventory notifications. For app-wide sidebars and launchers, `CodexAppServer.makeLibrary()` provides observable stored-thread lists, cwd or repository grouping, stable worktree groups, repository/worktree thread filters, refresh actions, library-local selection state, app-server-owned worktree snapshots, selected-worktree Git status, and optional app-wide model, MCP, and hook diagnostics snapshots beside thread lists. Thread handles can also name, archive, unarchive, compact, and roll back stored threads through thread-scoped methods. @@ -80,6 +80,8 @@ Use `CodexAppServer.fs` when a sandboxed client needs filesystem metadata, direc Use `CodexAppServer.ThreadListQD`, `CodexFS.FileDiscoveryQD`, `CodexThread.HistoryWindowQD`, `CodexThread.RecentFilesQD`, and `CodexThread.RecentCommandsQD` when a client needs to preserve repeatable list, file-discovery, history-window, or recent-activity intent without depending on Core Data, SwiftData, direct filesystem reads, or raw app-server paging details. +Use `CodexThread.makeAgenda()` when a SwiftUI surface needs the thread's current goal, latest accepted plan, proposed plan text, and summary titles without manually assembling raw plan deltas or reconciling goal reads with goal events. + Use `CodexThread.startReview(against:placement:)` to start app-server code reviews from a thread. The public API uses hand-owned Swift subjects such as `.uncommittedChanges`, `.baseBranch("main")`, `.commit(sha:title:)`, and `.custom(instructions:)`; `placement: .inline` runs the review turn on the current thread, while `.detached` runs it on a returned review thread. Use `CodexThread.sendShellCommand(_:)` only for explicit user-level shell access. It sends a literal shell command string through app-server `thread/shellCommand`, preserves shell syntax such as pipes and redirects, and is documented upstream as unsandboxed full-user shell execution. SwiftASB keeps its internal `command/exec` helper path separate because that path is argv-shaped app-server command execution for SwiftASB-owned helper intents. `sendShellCommand(_:)` is gated by the disabled-by-default `shellCommandExecution` feature category. diff --git a/Sources/SwiftASB/Public/CodexThread+Agenda.swift b/Sources/SwiftASB/Public/CodexThread+Agenda.swift new file mode 100644 index 0000000..27f7815 --- /dev/null +++ b/Sources/SwiftASB/Public/CodexThread+Agenda.swift @@ -0,0 +1,232 @@ +import Foundation +import Observation + +extension CodexThread { + @MainActor + @Observable + public final class Agenda { + public struct Plan: Sendable, Equatable { + public struct Step: Sendable, Equatable, Identifiable { + public enum Status: String, Sendable, Equatable { + case completed + case inProgress + case pending + } + + public let id: String + public let status: Status + public let title: String + } + + public let turnID: String + public let explanation: String? + public let steps: [Step] + } + + public struct ProposedPlan: Sendable, Equatable { + public struct Item: Sendable, Equatable, Identifiable { + public let id: String + public let text: String + } + + public let turnID: String + public let items: [Item] + } + + public let threadID: String + public private(set) var currentPlan: Plan? + public private(set) var goal: Goal? + public private(set) var goalStatus: Goal.Status? + public private(set) var proposedPlan: ProposedPlan? + public private(set) var updatedAt: Int? + + public var goalTitle: String { + goal?.objective ?? "" + } + + public var planTitle: String { + if let activeStep = currentPlan?.steps.first(where: { $0.status == .inProgress }) { + return activeStep.title + } + if let pendingStep = currentPlan?.steps.first(where: { $0.status == .pending }) { + return pendingStep.title + } + if let firstStep = currentPlan?.steps.first { + return firstStep.title + } + if let explanation = currentPlan?.explanation, !explanation.isEmpty { + return explanation + } + if let proposedText = proposedPlan?.items.first(where: { !$0.text.isEmpty })?.text { + return proposedText + } + return "" + } + + @ObservationIgnored + private var threadEventTask: Task? + + @ObservationIgnored + private var turnEventTask: Task? + + @ObservationIgnored + private var proposedPlanItemsByID: [String: String] + + @ObservationIgnored + private var proposedPlanItemOrder: [String] + + internal init( + threadID: String, + initialGoal: Goal?, + threadEvents: AsyncThrowingStream, + turnEvents: AsyncThrowingStream + ) { + self.threadID = threadID + self.currentPlan = nil + self.goal = initialGoal + self.goalStatus = initialGoal?.status + self.proposedPlan = nil + self.updatedAt = initialGoal?.updatedAt + self.proposedPlanItemsByID = [:] + self.proposedPlanItemOrder = [] + + threadEventTask = Task { [weak self] in + guard let self else { return } + + do { + for try await event in threadEvents { + self.apply(event) + } + } catch is CancellationError { + return + } catch { + return + } + } + + turnEventTask = Task { [weak self] in + guard let self else { return } + + do { + for try await event in turnEvents { + self.apply(event) + } + } catch is CancellationError { + return + } catch { + return + } + } + } + + deinit { + threadEventTask?.cancel() + turnEventTask?.cancel() + } + + private func apply(_ event: CodexThreadEvent) { + switch event { + case let .goalUpdated(update): + goal = update.goal + goalStatus = update.goal.status + updatedAt = update.goal.updatedAt + case .goalCleared: + goal = nil + goalStatus = nil + updatedAt = nil + case .started, + .statusChanged, + .diagnostic, + .approvalRequested, + .elicitationRequested, + .serverRequestResolved, + .archived, + .unarchived, + .closed, + .nameUpdated, + .tokenUsageUpdated: + return + } + } + + private func apply(_ event: CodexTurnEvent) { + switch event { + case let .planUpdated(update): + currentPlan = .init(update) + if proposedPlan?.turnID == update.turnID { + clearProposedPlan() + } + case let .planDelta(delta): + applyPlanDelta(delta) + case .started, + .diffUpdated, + .diagnostic, + .approvalRequested, + .elicitationRequested, + .serverRequestResolved, + .itemStarted, + .itemCompleted, + .agentMessageDelta, + .reasoningSummaryPartAdded, + .reasoningSummaryTextDelta, + .reasoningTextDelta, + .completed: + return + } + } + + private func applyPlanDelta(_ delta: CodexTurnPlanDelta) { + if proposedPlan?.turnID != delta.turnID { + proposedPlanItemsByID.removeAll() + proposedPlanItemOrder.removeAll() + } + + if proposedPlanItemsByID[delta.itemID] == nil { + proposedPlanItemOrder.append(delta.itemID) + } + + proposedPlanItemsByID[delta.itemID, default: ""] += delta.delta + proposedPlan = .init( + turnID: delta.turnID, + items: proposedPlanItemOrder.map { + .init(id: $0, text: proposedPlanItemsByID[$0] ?? "") + } + ) + } + + private func clearProposedPlan() { + proposedPlan = nil + proposedPlanItemsByID.removeAll() + proposedPlanItemOrder.removeAll() + } + } +} + +extension CodexThread.Agenda.Plan { + init(_ update: CodexTurnPlanUpdate) { + self.init( + turnID: update.turnID, + explanation: update.explanation, + steps: update.plan.enumerated().map { index, step in + .init( + id: "\(update.turnID):\(index)", + status: .init(step.status), + title: step.step + ) + } + ) + } +} + +extension CodexThread.Agenda.Plan.Step.Status { + init(_ status: CodexTurnPlanUpdate.Step.Status) { + switch status { + case .completed: + self = .completed + case .inProgress: + self = .inProgress + case .pending: + self = .pending + } + } +} diff --git a/Sources/SwiftASB/Public/CodexThread+Dashboard.swift b/Sources/SwiftASB/Public/CodexThread+Dashboard.swift index 807f1d0..e06410f 100644 --- a/Sources/SwiftASB/Public/CodexThread+Dashboard.swift +++ b/Sources/SwiftASB/Public/CodexThread+Dashboard.swift @@ -91,12 +91,13 @@ extension CodexThread { public private(set) var isArchived: Bool public private(set) var isClosed: Bool public private(set) var isCompactingThreadContext: Bool - public private(set) var goal: Goal? + public private(set) var goalTitle: String public private(set) var latestDiagnostic: CodexDiagnosticEvent? public private(set) var latestTokenUsage: CodexThreadTokenUsageUpdated? public private(set) var mcpCallingStatus: ActivityStatus public private(set) var mcpServers: [CodexAppServer.McpServerSummary] public private(set) var name: String? + public private(set) var planTitle: String public private(set) var preview: String public private(set) var status: CodexAppServer.ThreadStatus public private(set) var toolCallingStatus: ActivityStatus @@ -105,6 +106,9 @@ extension CodexThread { @ObservationIgnored private var eventTask: Task? + @ObservationIgnored + private var turnEventTask: Task? + @ObservationIgnored private var activityTask: Task? @@ -116,6 +120,7 @@ extension CodexThread { initialInfo: CodexAppServer.ThreadInfo, initialMcpServers: [CodexAppServer.McpServerSummary], events: AsyncThrowingStream, + turnEvents: AsyncThrowingStream, initialActivityState: ActivityState, activityUpdates: AsyncStream ) { @@ -123,10 +128,11 @@ extension CodexThread { self.isArchived = false self.isClosed = false self.latestDiagnostic = nil - self.goal = nil + self.goalTitle = "" self.latestTokenUsage = nil self.mcpServers = initialMcpServers self.name = initialInfo.name + self.planTitle = "" self.preview = initialInfo.preview self.status = initialInfo.status self.activityState = initialActivityState @@ -155,6 +161,20 @@ extension CodexThread { } } + turnEventTask = Task { [weak self] in + guard let self else { return } + + do { + for try await event in turnEvents { + self.apply(event) + } + } catch is CancellationError { + return + } catch { + return + } + } + activityTask = Task { [weak self] in guard let self else { return } @@ -166,6 +186,7 @@ extension CodexThread { deinit { eventTask?.cancel() + turnEventTask?.cancel() activityTask?.cancel() } @@ -196,9 +217,31 @@ extension CodexThread { case let .tokenUsageUpdated(update): latestTokenUsage = update case let .goalUpdated(update): - goal = update.goal + goalTitle = update.goal.objective case .goalCleared: - goal = nil + goalTitle = "" + } + } + + private func apply(_ event: CodexTurnEvent) { + switch event { + case let .planUpdated(update): + planTitle = Self.planTitle(from: update) + case .started, + .planDelta, + .diffUpdated, + .diagnostic, + .approvalRequested, + .elicitationRequested, + .serverRequestResolved, + .itemStarted, + .itemCompleted, + .agentMessageDelta, + .reasoningSummaryPartAdded, + .reasoningSummaryTextDelta, + .reasoningTextDelta, + .completed: + return } } @@ -232,6 +275,19 @@ extension CodexThread { } return .idle } + + private static func planTitle(from update: CodexTurnPlanUpdate) -> String { + if let activeStep = update.plan.first(where: { $0.status == .inProgress }) { + return activeStep.step + } + if let pendingStep = update.plan.first(where: { $0.status == .pending }) { + return pendingStep.step + } + if let firstStep = update.plan.first { + return firstStep.step + } + return update.explanation ?? "" + } } } diff --git a/Sources/SwiftASB/Public/CodexThread.swift b/Sources/SwiftASB/Public/CodexThread.swift index 540484e..497b7d5 100644 --- a/Sources/SwiftASB/Public/CodexThread.swift +++ b/Sources/SwiftASB/Public/CodexThread.swift @@ -420,6 +420,7 @@ public struct CodexThread: Sendable { @MainActor public func makeDashboard() async -> Dashboard { let events = await appServer.threadEventStream(threadID: id) + let turnEvents = await appServer.threadTurnEventStream(threadID: id) let initialActivityState = await appServer.threadObservableActivityState(threadID: id) let activityUpdates = await appServer.threadObservableActivityStream(threadID: id) return Dashboard( @@ -427,11 +428,30 @@ public struct CodexThread: Sendable { initialInfo: info, initialMcpServers: mcpServers, events: events, + turnEvents: turnEvents, initialActivityState: initialActivityState, activityUpdates: activityUpdates ) } + /// Creates an observable agenda for this thread. + /// + /// The agenda owns current goal hydration plus live goal and plan updates. + /// Use it for plan and goal UI instead of assembling raw turn-plan deltas + /// or manually reconciling goal reads with thread events. + @MainActor + public func makeAgenda() async throws -> Agenda { + let threadEvents = await appServer.threadEventStream(threadID: id) + let turnEvents = await appServer.threadTurnEventStream(threadID: id) + let initialGoal = try await appServer.readThreadGoal(threadID: id) + return Agenda( + threadID: id, + initialGoal: initialGoal, + threadEvents: threadEvents, + turnEvents: turnEvents + ) + } + /// Asks the app-server to compact this thread's context. /// /// Dashboard and minimap companions mirror compaction activity when the diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md index 5b19c07..4e953e2 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md @@ -25,7 +25,7 @@ Use ``setName(_:)`` for a human-readable title, ``updateMetadata(gitInfo:)`` for Rollback returns a refreshed thread handle. SwiftASB records a local rollback marker and trims visible local history to match the app-server response. It does not preserve the full removed-turn payload archive yet. -Use ``readGoal()``, ``setGoal(_:)``, and ``clearGoal()`` for the app-server goal attached to this thread. +Use ``makeAgenda()`` when a UI wants current goal and plan state for this thread. Use ``readGoal()``, ``setGoal(_:)``, and ``clearGoal()`` when a non-UI caller needs direct goal reads or mutation. Use ``startReview(against:placement:)`` to ask the app-server to review repository state associated with this thread. The `against` subject can be uncommitted changes, a base branch, one commit, or custom instructions. ``ReviewPlacement/inline`` runs the review turn on this thread. ``ReviewPlacement/detached`` runs the review turn on a new review thread returned in ``CodexReviewHandle/reviewThreadID``. @@ -43,7 +43,7 @@ Use the non-UI history helpers when a caller needs completed turn snapshots with ## Observable Companions -Use ``makeDashboard()`` for thread-level current state, ``makeRecentTurns(limit:cachePolicy:)`` for a turn-centric view, ``makeRecentFiles(limit:cachePolicy:)`` or ``makeRecentFiles(_:)`` for a file-change view, and ``makeRecentCommands(limit:cachePolicy:)`` or ``makeRecentCommands(_:)`` for a command-output view. +Use ``makeDashboard()`` for thread-level current status, ``makeAgenda()`` for current goal and plan state, ``makeRecentTurns(limit:cachePolicy:)`` for a turn-centric view, ``makeRecentFiles(limit:cachePolicy:)`` or ``makeRecentFiles(_:)`` for a file-change view, and ``makeRecentCommands(limit:cachePolicy:)`` or ``makeRecentCommands(_:)`` for a command-output view. These companions are separate on purpose. `RecentTurns`, `RecentFiles`, and `RecentCommands` preserve domain-specific behavior that a mixed activity feed would flatten too early. @@ -121,6 +121,8 @@ Recent observable startup can begin as an empty local-only view when the live ap - ``makeDashboard()`` - ``Dashboard`` +- ``makeAgenda()`` +- ``Agenda`` - ``makeRecentTurns(limit:cachePolicy:)`` - ``RecentTurns`` - ``makeRecentFiles(limit:cachePolicy:)`` diff --git a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md index 049f363..c683dd3 100644 --- a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md +++ b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md @@ -1,12 +1,12 @@ # SwiftUI Observable Companions -Use dashboard, minimap, recent-file, and recent-command companions as current-state UI models. +Use dashboard, agenda, minimap, recent-file, and recent-command companions as current-state UI models. ## Overview SwiftASB's observable companions are ready-made `@Observable` state objects for SwiftUI surfaces. They are current-state mirrors over live streams and local history; they are not replayable protocol logs. -Use ``CodexAppServer/makeInventory(configuration:)`` for app-wide capability and extension inventory, ``CodexAppServer/makeLibrary(configuration:)`` for app-wide stored-thread lists, ``CodexThread/makeDashboard()`` for thread-level state, ``CodexTurnHandle/minimap`` for one active turn, and the recent companions for completed turn, file, and command views. +Use ``CodexAppServer/makeInventory(configuration:)`` for app-wide capability and extension inventory, ``CodexAppServer/makeLibrary(configuration:)`` for app-wide stored-thread lists, ``CodexThread/makeDashboard()`` for thread-level status, ``CodexThread/makeAgenda()`` for goal and plan state, ``CodexTurnHandle/minimap`` for one active turn, and the recent companions for completed turn, file, and command views. ```swift import Observation @@ -21,6 +21,7 @@ final class ThreadInspectorModel { var inventory: CodexAppServer.Inventory? var library: CodexAppServer.Library? var dashboard: CodexThread.Dashboard? + var agenda: CodexThread.Agenda? var recentFiles: CodexThread.RecentFiles? var recentCommands: CodexThread.RecentCommands? var currentMinimap: CodexTurnHandle.Minimap? @@ -43,6 +44,7 @@ final class ThreadInspectorModel { ) library?.sortedBy = .selectedNewestFirst dashboard = await thread.makeDashboard() + agenda = try await thread.makeAgenda() recentFiles = try await thread.makeRecentFiles( limit: 20, cachePolicy: .automatic(pageSize: 20) @@ -98,6 +100,8 @@ When `gitObservability` is enabled in ``SwiftASBFeaturePolicy``, selecting a lib Use ``CodexAppServer/Inventory`` when an app-wide UI needs model capabilities, MCP server summaries, hook diagnostics, apps, skills, plugins, and collaboration modes without also needing stored-thread lists. Use ``CodexAppServer/Library/refreshAppSnapshots()`` when model, MCP, and hook snapshots should sit beside the thread library. SwiftASB owns MCP status refresh and keeps summary lists current from startup and app-server status-change notifications. +Use ``CodexThread/Agenda`` when a UI wants to show the thread's current task target, current accepted plan, and proposed plan text while Codex is still shaping it. SwiftASB reads the current goal, listens for goal changes, accepts authoritative plan snapshots, and treats experimental plan deltas as agenda state instead of making app code assemble them. + Recent companions keep caller-owned UI inputs mutable. For example, views can update selected file or command identifiers and visible item identifiers. SwiftASB uses that information to protect visible or selected payloads while slimming older low-value entries when the resident cache exceeds its budget. Start with automatic cache policies unless the UI has known density requirements. Use the named presets for ``CodexThread/RecentTurns`` and automatic policies for file and command companions when the initial page size is enough guidance. @@ -116,6 +120,8 @@ Store the companion object itself in your view model. Do not copy its arrays int - ``CodexAppServer/Inventory`` - ``CodexThread/makeDashboard()`` - ``CodexThread/Dashboard`` +- ``CodexThread/makeAgenda()`` +- ``CodexThread/Agenda`` ### Active Turn State diff --git a/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md b/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md index 0dc2127..f35b112 100644 --- a/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md +++ b/Sources/SwiftASB/SwiftASB.docc/ThreadHistoryAndObservables.md @@ -62,11 +62,11 @@ Use the named cache-policy presets first: ## Dashboard And Minimap -``CodexThread/Dashboard`` summarizes thread-level current state such as active tool, MCP, hook, and compaction activity. ``CodexTurnHandle/Minimap`` summarizes one active turn's command, file-edit, MCP, dynamic-tool, and collab-tool activity. +``CodexThread/Dashboard`` summarizes thread-level current state such as active tool, MCP, hook, compaction activity, and plan or goal title text. ``CodexThread/Agenda`` owns the detailed goal and plan state for a thread, including the current goal, latest accepted plan, and proposed plan text assembled from live deltas. ``CodexTurnHandle/Minimap`` summarizes one active turn's command, file-edit, MCP, dynamic-tool, and collab-tool activity. Use these for "what is happening now" UI. Use history windows or closed turns for completed transcript data. -These companions are not alternate event logs. `Dashboard` starts from the current thread snapshot and aggregate activity state, then mirrors later thread and activity updates. `Minimap`, `RecentTurns`, `RecentFiles`, and `RecentCommands` listen to live feeds after they are created; command-output and file-output deltas that arrive before a recent companion exists are not replayed as delta events, though completed history can still be rehydrated from the local history store. +These companions are not alternate event logs. `Dashboard` starts from the current thread snapshot and aggregate activity state, then mirrors later thread and activity updates. `Agenda` reads the current goal when it starts, then mirrors later goal and plan changes. `Minimap`, `RecentTurns`, `RecentFiles`, and `RecentCommands` listen to live feeds after they are created; command-output and file-output deltas that arrive before a recent companion exists are not replayed as delta events, though completed history can still be rehydrated from the local history store. ## Topics @@ -93,6 +93,7 @@ These companions are not alternate event logs. `Dashboard` starts from the curre - ``CodexAppServer/Library`` - ``CodexThread/Dashboard`` +- ``CodexThread/Agenda`` - ``CodexThread/RecentTurns`` - ``CodexThread/RecentFiles`` - ``CodexThread/RecentCommands`` diff --git a/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift index d6eb8bd..9062ba4 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift @@ -69,6 +69,8 @@ extension CodexAppServerTests { #expect(dashboard.status.type == .active) #expect(dashboard.isArchived == false) #expect(dashboard.isClosed == false) + #expect(dashboard.goalTitle == "") + #expect(dashboard.planTitle == "") #expect(dashboard.latestTokenUsage == nil) await transport.emitHookStarted( @@ -147,6 +149,8 @@ extension CodexAppServerTests { await transport.emitThreadStarted(threadID: thread.id) await transport.emitThreadStatusChanged(threadID: thread.id) await transport.emitThreadNameUpdated(threadID: thread.id) + await transport.emitThreadGoalUpdated(threadID: thread.id) + await transport.emitTurnPlanUpdated(threadID: thread.id, turnID: turnHandle.turn.id) await transport.emitThreadArchived(threadID: thread.id) await transport.emitThreadTokenUsageUpdated(threadID: thread.id, turnID: "turn-123") await transport.emitThreadClosed(threadID: thread.id) @@ -155,6 +159,8 @@ extension CodexAppServerTests { dashboard.name == "Planning Thread" && dashboard.isArchived && dashboard.isClosed + && dashboard.goalTitle == "Promote schemas" + && dashboard.planTitle == "Promote protocol events into CodexTurnEvent" && dashboard.latestTokenUsage?.turnID == "turn-123" } @@ -164,6 +170,8 @@ extension CodexAppServerTests { #expect(dashboard.status.activeFlags == [.waitingOnApproval]) #expect(dashboard.isArchived == true) #expect(dashboard.isClosed == true) + #expect(dashboard.goalTitle == "Promote schemas") + #expect(dashboard.planTitle == "Promote protocol events into CodexTurnEvent") #expect(dashboard.isCompactingThreadContext == false) #expect(dashboard.hookRuns.count == 1) #expect(dashboard.hookRuns[0].status == .completed) @@ -176,6 +184,101 @@ extension CodexAppServerTests { await client.stop() } + @MainActor + @Test("builds an agenda that owns thread goals and plan state") + func buildsThreadAgenda() async throws { + let transport = FakeCodexAppServerTransport() + let client = CodexAppServer(transport: transport) + + try await client.start() + _ = try await client.initialize( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + + let thread = try await client.startThread( + .init( + currentDirectoryPath: "/tmp/project", + model: "gpt-5.4", + modelProvider: "openai" + ) + ) + let turnHandle = try await thread.startTextTurn("Shape agenda state") + + let agenda = try await thread.makeAgenda() + + #expect(agenda.threadID == thread.id) + #expect(agenda.goal?.objective == "Promote schemas") + #expect(agenda.goalStatus == .active) + #expect(agenda.goalTitle == "Promote schemas") + #expect(agenda.updatedAt == 1_713_350_010) + #expect(agenda.currentPlan == nil) + #expect(agenda.proposedPlan == nil) + #expect(agenda.planTitle == "") + + await transport.emitPlanDelta( + threadID: thread.id, + turnID: turnHandle.turn.id, + itemID: "item-plan-1" + ) + await transport.emitPlanDelta( + threadID: thread.id, + turnID: turnHandle.turn.id, + itemID: "item-plan-1" + ) + + await waitForObservableState { + agenda.proposedPlan?.items.first?.text == "Stream partial plan textStream partial plan text" + } + + #expect(agenda.proposedPlan?.turnID == turnHandle.turn.id) + #expect(agenda.proposedPlan?.items.first?.id == "item-plan-1") + #expect(agenda.planTitle == "Stream partial plan textStream partial plan text") + + await transport.emitTurnPlanUpdated( + threadID: thread.id, + turnID: turnHandle.turn.id + ) + + await waitForObservableState { + agenda.currentPlan?.steps.count == 2 + && agenda.proposedPlan == nil + } + + #expect(agenda.currentPlan?.turnID == turnHandle.turn.id) + #expect(agenda.currentPlan?.explanation == "Map richer progress notifications.") + #expect(agenda.currentPlan?.steps[0].id == "\(turnHandle.turn.id):0") + #expect(agenda.currentPlan?.steps[0].status == .inProgress) + #expect(agenda.currentPlan?.steps[0].title == "Promote protocol events into CodexTurnEvent") + #expect(agenda.currentPlan?.steps[1].status == .pending) + #expect(agenda.planTitle == "Promote protocol events into CodexTurnEvent") + + await transport.emitThreadGoalUpdated(threadID: thread.id) + + await waitForObservableState { + agenda.goalStatus == .complete + && agenda.updatedAt == 1_713_350_030 + } + + #expect(agenda.goalTitle == "Promote schemas") + + await transport.emitThreadGoalCleared(threadID: thread.id) + + await waitForObservableState { + agenda.goal == nil + && agenda.goalStatus == nil + && agenda.goalTitle == "" + && agenda.updatedAt == nil + } + + await client.stop() + } + @MainActor @Test("builds a minimap that stays live with turn events") func buildsTurnMinimap() async throws { diff --git a/docs/maintainers/thread-plan-goal-companion-plan.md b/docs/maintainers/thread-plan-goal-companion-plan.md index 4045c0e..e357735 100644 --- a/docs/maintainers/thread-plan-goal-companion-plan.md +++ b/docs/maintainers/thread-plan-goal-companion-plan.md @@ -1,9 +1,13 @@ # Thread Plan And Goal Companion Plan -This note maps the next SwiftASB cleanup pass for Codex plans and goals. The -goal is to make plan and goal state easy for SwiftUI clients to consume without -asking app code to assemble experimental deltas, manually refresh current goals, -or depend on every raw notification as public API. +This note maps the SwiftASB cleanup pass for Codex plans and goals. The goal is +to make plan and goal state easy for SwiftUI clients to consume without asking +app code to assemble experimental deltas, manually refresh current goals, or +depend on every raw notification as public API. + +Status: the first implementation pass shipped `CodexThread.Agenda`, +`CodexThread.makeAgenda()`, and dashboard title summaries on +`feature/plans-goals-api`. ## Current Facts @@ -36,7 +40,7 @@ The current public SwiftASB API already exposes: - `CodexThreadEvent.goalCleared` - `CodexTurnEvent.planUpdated` - `CodexTurnEvent.planDelta` -- `CodexThread.Dashboard.goal` +- `CodexThread.Dashboard.goal` before the first implementation pass - `CodexTurnHandle.Minimap.latestPlanUpdate` - `CodexTurnHandle.Minimap.latestPlanDelta` @@ -141,11 +145,10 @@ Recommended dashboard fields: - `goalTitle: String` - `planTitle: String` -Open question: +Decision: -- Keep `Dashboard.goal` deprecated for one minor release, or remove it in the - same public-surface simplification pass if the next release is allowed to - carry a source break. +- Remove `Dashboard.goal` in the same simplification pass and expose + `Dashboard.goalTitle` plus `Dashboard.planTitle`. The practical effect is that dashboard stays the broad thread status strip, while agenda becomes the detailed task-progress model. @@ -190,11 +193,12 @@ Those should be deliberate convenience APIs, not thin slash-command replicas. ## Implementation Slices -1. Add `CodexThread.Agenda` and `makeAgenda()`. +1. Add `CodexThread.Agenda` and `makeAgenda()`. Shipped. 2. Hydrate the current goal on construction and apply thread goal events. -3. Feed turn plan updates and plan deltas into agenda state. -4. Add dashboard `goalTitle` and `planTitle` summaries. -5. Update DocC to recommend agenda for plan/goal UI. + Shipped. +3. Feed turn plan updates and plan deltas into agenda state. Shipped. +4. Add dashboard `goalTitle` and `planTitle` summaries. Shipped. +5. Update DocC to recommend agenda for plan/goal UI. Shipped. 6. Revisit public event deprecations after tests prove agenda covers routine UI. ## Validation diff --git a/docs/maintainers/v1-public-api-symbol-inventory.md b/docs/maintainers/v1-public-api-symbol-inventory.md index bd7a26d..9b76723 100644 --- a/docs/maintainers/v1-public-api-symbol-inventory.md +++ b/docs/maintainers/v1-public-api-symbol-inventory.md @@ -1,6 +1,6 @@ # V1 Public API Symbol Inventory -Generated from `swift package dump-symbol-graph --minimum-access-level public --skip-synthesized-members` on 2026-05-02 after the v0.128 generated-wire promotion and final pre-v1 public-surface tightening, then updated on 2026-05-05 for the post-v1 app-wide library snapshot, on 2026-05-06 for the public query descriptor, filesystem, config, extension-inventory, thread-goal, recent-activity descriptor, repository-grouping, workspace permission-profile, and file-discovery slices, on 2026-05-08 for the `CodexWorkspace.ProjectInfo` cleanup, `CodexWorkspace.WorktreeSnapshot` promotion, `CodexAppServer.Library` worktree-group helpers, `CodexAppServer.ThreadSource` promotion, and v0.129 hook compact event names, on 2026-05-15 for `CodexThread.sendShellCommand(_:)`, the `shellCommandExecution` feature category, and `CodexThread.startReview(against:placement:)`, on 2026-05-20 for the v0.133 schema compatibility refresh, and on 2026-05-30 for `CodexAppServer.Inventory`, `CodexMCP.statusSnapshot()`, and `CodexMCP.readResource(...)`. This is a maintainer ledger for the v1 public API freeze plus accepted post-v1 app-wide additions; it records public/open declarations visible through the `SwiftASB` library product, excluding synthesized members. +Generated from `swift package dump-symbol-graph --minimum-access-level public --skip-synthesized-members` on 2026-05-02 after the v0.128 generated-wire promotion and final pre-v1 public-surface tightening, then updated on 2026-05-05 for the post-v1 app-wide library snapshot, on 2026-05-06 for the public query descriptor, filesystem, config, extension-inventory, thread-goal, recent-activity descriptor, repository-grouping, workspace permission-profile, and file-discovery slices, on 2026-05-08 for the `CodexWorkspace.ProjectInfo` cleanup, `CodexWorkspace.WorktreeSnapshot` promotion, `CodexAppServer.Library` worktree-group helpers, `CodexAppServer.ThreadSource` promotion, and v0.129 hook compact event names, on 2026-05-15 for `CodexThread.sendShellCommand(_:)`, the `shellCommandExecution` feature category, and `CodexThread.startReview(against:placement:)`, on 2026-05-20 for the v0.133 schema compatibility refresh, and on 2026-05-30 for `CodexAppServer.Inventory`, `CodexMCP.statusSnapshot()`, `CodexMCP.readResource(...)`, and `CodexThread.Agenda`. This is a maintainer ledger for the v1 public API freeze plus accepted post-v1 app-wide additions; it records public/open declarations visible through the `SwiftASB` library product, excluding synthesized members. ## Last Full Snapshot Summary @@ -782,6 +782,8 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `CodexAppServer.Inventory` now owns routine app-wide observable inventory: `Configuration`, `Phase`, `appListPage`, `apps`, `modelCapabilities`, `mcpServers`, `hookListSnapshot`, `skillListSnapshot`, `skillEntries`, `skills`, `pluginListSnapshot`, `pluginMarketplaces`, `collaborationModes`, `collaborationModeEntries`, `refresh()`, and `makeInventory(configuration:)`. - `CodexMCP` now owns detail-oriented MCP helpers beside installs: `statusSnapshot()`, `readResource(_:)`, and `readResource(server:uri:threadID:)`. - `CodexThread` now exposes thread goals: `Goal`, `Goal.Status`, `GoalSetRequest`, `readGoal()`, `setGoal(_:)`, and `clearGoal()`. +- `CodexThread.Agenda` now owns thread plan and goal presentation state: `Plan`, `Plan.Step`, `Plan.Step.Status`, `ProposedPlan`, `ProposedPlan.Item`, `threadID`, `goal`, `goalStatus`, `goalTitle`, `currentPlan`, `proposedPlan`, `planTitle`, `updatedAt`, and `CodexThread.makeAgenda()`. +- `CodexThread.Dashboard` now exposes `goalTitle` and `planTitle` summaries instead of the previous full `goal` value. - The v0.133 compatibility refresh adds `CodexThread.Goal.Status.blocked`, `CodexThread.Goal.Status.usageLimited`, `CodexRemoteControlStatusDiagnostic.installationID`, `CodexRemoteControlStatusDiagnostic.serverName`, and the `subagentStart` / `subagentStop` hook event cases on app-wide hook metadata and thread dashboard hook runs. - `CodexThreadEvent` now includes `.goalUpdated(_:)` and `.goalCleared(_:)` for app-server goal notifications. - `CodexThread.RecentFilesQD` and `CodexThread.RecentCommandsQD` describe repeatable recent-activity companion startup intent. @@ -814,7 +816,8 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `Sources/SwiftASB/Public/CodexFS.swift`: 44 public properties - `Sources/SwiftASB/Public/CodexInteractiveRequests.swift`: 74 public properties - `Sources/SwiftASB/Public/CodexReviewHandle.swift`: 5 public properties -- `Sources/SwiftASB/Public/CodexThread+Dashboard.swift`: 29 public properties +- `Sources/SwiftASB/Public/CodexThread+Agenda.swift`: 18 public properties +- `Sources/SwiftASB/Public/CodexThread+Dashboard.swift`: 30 public properties - `Sources/SwiftASB/Public/CodexThread+RecentCommands.swift`: 25 public properties - `Sources/SwiftASB/Public/CodexThread+RecentFiles.swift`: 25 public properties - `Sources/SwiftASB/Public/CodexThread+RecentTurns.swift`: 54 public properties From e90b43de7306b30dc42bbce30fa1744584ad88a9 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 30 May 2026 16:23:44 -0400 Subject: [PATCH 3/6] runtime: add plan mode and agenda goal actions --- README.md | 2 + ROADMAP.md | 8 +- .../Public/CodexAppServer+TurnLifecycle.swift | 51 +++++++++++++ .../Public/CodexAppServer+WireMapping.swift | 26 ++++++- .../SwiftASB/Public/CodexThread+Agenda.swift | 73 ++++++++++++++++-- Sources/SwiftASB/Public/CodexThread.swift | 44 +++++++++++ Sources/SwiftASB/SwiftASB.docc/CodexThread.md | 9 ++- .../GettingStartedWithSwiftASB.md | 2 +- .../SwiftUIObservableCompanions.md | 18 +++++ .../CodexAppServerProtocolTests.swift | 16 +++- .../CodexAppServerCompanionSurfaceTests.swift | 74 +++++++++++++++++++ .../CodexAppServerTurnLifecycleTests.swift | 44 +++++++++++ .../thread-plan-goal-companion-plan.md | 4 +- .../v1-public-api-symbol-inventory.md | 22 +++--- 14 files changed, 365 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index d202064..9333f11 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ Use `CodexAppServer.ThreadListQD`, `CodexFS.FileDiscoveryQD`, `CodexThread.Histo Use `CodexThread.makeAgenda()` when a SwiftUI surface needs the thread's current goal, latest accepted plan, proposed plan text, and summary titles without manually assembling raw plan deltas or reconciling goal reads with goal events. +Use `CodexThread.startPlanningTurn(...)` when a mode button or segmented control should start the next turn in Codex plan mode without sending slash-command text through the prompt. Advanced callers can use `CodexAppServer.TurnCollaborationMode` directly on a turn-start request. + Use `CodexThread.startReview(against:placement:)` to start app-server code reviews from a thread. The public API uses hand-owned Swift subjects such as `.uncommittedChanges`, `.baseBranch("main")`, `.commit(sha:title:)`, and `.custom(instructions:)`; `placement: .inline` runs the review turn on the current thread, while `.detached` runs it on a returned review thread. Use `CodexThread.sendShellCommand(_:)` only for explicit user-level shell access. It sends a literal shell command string through app-server `thread/shellCommand`, preserves shell syntax such as pipes and redirects, and is documented upstream as unsandboxed full-user shell execution. SwiftASB keeps its internal `command/exec` helper path separate because that path is argv-shaped app-server command execution for SwiftASB-owned helper intents. `sendShellCommand(_:)` is gated by the disabled-by-default `shellCommandExecution` feature category. diff --git a/ROADMAP.md b/ROADMAP.md index 702c83a..0082522 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -62,16 +62,16 @@ | App-server config reads | `Partially shipped` | `CodexAppServer.config` now exposes `CodexConfig` for effective config and requirements reads through the app-server. Effective config stays JSON-shaped for now so SwiftASB does not turn unstable config keys into long-lived public Swift fields too early. | | App-server extension inventory and maintenance | `Partially shipped` | Routine app, skill, plugin, and collaboration-mode inventory now flows through `CodexAppServer.Inventory`; `CodexAppServer.extensions` remains the direct escape hatch for custom pagination, selected plugin-detail reads, and `upgradeMarketplace(_:)` for upgrading already-configured plugin marketplaces through app-server `command/exec` under the `extensionMaintenance` feature category. Plugin installs, removals, sharing changes, and skills config writes remain unpromoted until their permission and review model is clearer. | | SwiftASB feature permission policy | `Fifth slice shipped` | `SwiftASBFeaturePolicy`, `SwiftASBFeatureCategory`, and `SwiftASBHostAccess` now describe feature-category defaults and host access declarations, and `CodexAppServer.Configuration` accepts the app-wide feature policy. SwiftASB also has an internal `command/exec` protocol/executor path for future typed Git/GitHub helper intents, `CodexAppServer.Library` selected-worktree Git status refresh through the default-enabled `gitObservability` category, `CodexAppServer.featureOperationEvents()` for human-readable SwiftASB-owned mutation records, and a typed marketplace-upgrade maintenance intent. Maintainer planning targets quiet read-only Git/config/extension inventory by default, one-time mutation-category enablement, and human-readable mutation events instead of repeated prompts. See [`docs/maintainers/feature-permission-policy-plan.md`](docs/maintainers/feature-permission-policy-plan.md). | -| Thread goals | `Partially shipped` | `CodexThread.readGoal()`, `setGoal(...)`, and `clearGoal()` wrap `thread/goal/get`, `thread/goal/set`, and `thread/goal/clear`, and thread event streams now surface goal updated and cleared notifications. | +| Thread goals | `Partially shipped` | `CodexThread.readGoal()`, `setGoal(...)`, and `clearGoal()` wrap `thread/goal/get`, `thread/goal/set`, and `thread/goal/clear`, thread event streams now surface goal updated and cleared notifications, and `CodexThread.Agenda` provides UI-friendly `setGoal(...)`, `pauseGoal()`, `resumeGoal()`, and `clearGoal()` actions. | | Thread shell commands | `Partially shipped` | `CodexThread.sendShellCommand(_:)` wraps app-server `thread/shellCommand` as a thread-scoped, literal shell-string action. This is deliberately separate from SwiftASB's internal `command/exec` helper path because `thread/shellCommand` preserves shell syntax and is documented upstream as unsandboxed full-user shell access. The public method is gated behind the disabled-by-default high-impact `shellCommandExecution` feature category. | | Code review start flow | `Partially shipped` | `CodexThread.startReview(against:placement:)` wraps app-server `review/start` with hand-owned review subjects and placement names. Inline reviews run on the source thread; detached reviews run on the returned review thread and surface that id through `CodexReviewHandle.reviewThreadID`. | | Paged turn-history flow | `Shipped` | `listThreadTurns(...)` wraps `thread/turns/list`, returns typed paged turn values, and can now seed the local history cache even before that thread has been loaded locally. | | Typed async thread event stream | `Partially shipped` | `CodexThread.events` now streams `thread/started`, `thread/status/changed`, `thread/archived`, `thread/unarchived`, `thread/name/updated`, `thread/tokenUsage/updated`, `thread/goal/updated`, `thread/goal/cleared`, and `thread/closed`, but broader thread lifecycle coverage is still pending. | -| Turn start flow | `Shipped` | `startTurn(...)` returns `CodexTurnHandle`. | +| Turn start flow | `Shipped` | `startTurn(...)` returns `CodexTurnHandle`, and turn requests can opt into app-server collaboration mode through `CodexAppServer.TurnCollaborationMode`. | | Typed async turn event stream | `Partially shipped` | `CodexTurnHandle.events` now streams `turn/started`, `turn/plan/updated`, `turn/diff/updated`, item lifecycle updates, message deltas, reasoning deltas, and `turn/completed`, but broader item and thread events still remain internal. | | Multiple active threads per app-server | `Shipped` | One `CodexAppServer` now supports many concurrently held `CodexThread` handles, and the package tests plus live probes treat cross-thread concurrency as a supported model. | | Multiple simultaneous turns on one thread | `Resolved for now` | Live probing showed that same-thread overlap is not independently routable at the app-server layer today, so `SwiftASB` rejects overlapping same-thread turns client-side with `CodexAppServerError.invalidState`. | -| `CodexThread` convenience wrapper | `Partially shipped` | `CodexThread` exists, owns thread-scoped turn creation, includes a `startTextTurn(...)` happy-path helper, exposes a typed thread event stream, wraps `compactContext()`, and can now vend a live `Dashboard` observable mirror with aggregate tool-calling, MCP-calling, hook-run, and thread-compaction state. | +| `CodexThread` convenience wrapper | `Partially shipped` | `CodexThread` exists, owns thread-scoped turn creation, includes `startTextTurn(...)` and `startPlanningTurn(...)` helpers, exposes a typed thread event stream, wraps `compactContext()`, and can now vend live `Dashboard` and `Agenda` observable mirrors for status plus plan/goal state. | | Thread-scoped recent-turn observable | `Partially shipped` | `CodexThread.makeRecentTurns(limit:)` now vends a bounded recent-turn observable that prewarms from the local history store, supports explicit older/newer whole-turn window expansion, seeds upstream paging cursors even when the visible initial window came from local history, and falls back to `thread/turns/list` when needed. Live probing showed that upstream turn paging is available only after a non-ephemeral thread has materialized at least one user turn, so recent observable startup now degrades to an empty local-only view for the known ephemeral and pre-materialized live runtime responses instead of surfacing raw protocol text. `RecentTurns` now ships named cache-policy presets for chat UIs, full inspectors, and compact history rails; tracks both resident item counts and weighted resident item cost; slims low-value payloads out of older non-visible completed turns before evicting whole turns; rehydrates slimmed turns when they become visible again; and uses scroll-position, visibility, phase, and velocity signals to drive protected residency plus earlier prefetch. Richer weighting heuristics and deeper policy tuning are still open. | | Thread-scoped recent-file observable | `Partially shipped` | `CodexThread.makeRecentFiles(limit:)` and `makeRecentFiles(_:)` now vend a file-centric recent-files observable that hydrates from persisted file-change items, keeps one resident entry per file-change item, enriches live entries from `item/fileChange/outputDelta` and `item/fileChange/patchUpdated`, can load older file entries from the same turn before stepping farther back through older turns, and supports selection-aware shell-versus-payload slimming with automatic payload rehydration for protected files. `CodexThread.RecentFilesQD` gives callers a repeatable descriptor for the initial resident file window and cache policy. Live probing exercises a real create/edit/delete scenario, and recent-file startup now inherits the same empty local-only degradation as recent-turns for the known live history-unavailable responses. The current weighting now accounts for diff structure and line volume, and shell summaries prefer concise edit summaries over raw terminal status when sealed payload is available. The remaining open work is better payload-cost calibration at the margins and richer structured patch presentation beyond the current text preview. | | Thread-scoped recent-command observable | `Partially shipped` | `CodexThread.makeRecentCommands(limit:)` and `makeRecentCommands(_:)` now vend a command-centric recent-commands observable that hydrates from persisted `commandExecution` items, keeps one resident entry per command item, enriches live entries from `item/commandExecution/outputDelta`, can load older command entries from the same turn before stepping farther back through older turns, and supports selection-aware shell-versus-output slimming with automatic output rehydration for protected commands. `CodexThread.RecentCommandsQD` gives callers a repeatable descriptor for the initial resident command window and cache policy. Recent-command startup now inherits the same empty local-only degradation as recent-turns for the known live history-unavailable responses. Current output weighting accounts for output size and line structure, and shell summaries prefer concise command and output summaries over raw transport detail. The remaining open work is better output-cost calibration and sharper shell-summary heuristics. | @@ -1375,6 +1375,8 @@ Completed ## Backlog Candidates - [ ] Add a one-shot `run(...)` convenience API once the lower-level handle model is stable enough to hide honestly. +- [ ] Add optional suggested-goal generation that returns candidate goal strings from a prompt or current agenda state without mutating the thread until a host app or user accepts one. +- [ ] Add an optional auto-plan mode feature policy that can suggest or select plan mode for prompts likely to need planning, while keeping explicit mode controls as the default behavior. - [ ] Add a broader public history cursor or transcript search surface after the local history contract is clearer. - [ ] Add richer MCP progress detail either as public event cases or as deeper observable companion state. - [ ] Add guardian denied-action approval once SwiftASB owns a stable request and response model for that control flow. diff --git a/Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift b/Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift index 70a3ccf..9e478da 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift @@ -5,6 +5,7 @@ extension CodexAppServer { public struct TurnStartRequest: Sendable, Equatable { public var approvalPolicy: ApprovalPolicy? public var approvalsReviewer: ApprovalsReviewer? + public var collaborationMode: TurnCollaborationMode? public var currentDirectoryPath: String? public var effort: ReasoningEffort? public var input: [TurnInput] @@ -26,6 +27,7 @@ extension CodexAppServer { input: [TurnInput], approvalPolicy: ApprovalPolicy? = nil, approvalsReviewer: ApprovalsReviewer? = nil, + collaborationMode: TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: ReasoningEffort? = nil, model: String? = nil, @@ -39,6 +41,7 @@ extension CodexAppServer { self.input = input self.approvalPolicy = approvalPolicy self.approvalsReviewer = approvalsReviewer + self.collaborationMode = collaborationMode self.currentDirectoryPath = currentDirectoryPath self.effort = effort self.model = model @@ -50,6 +53,54 @@ extension CodexAppServer { } } + /// Collaboration preset used for one turn. + /// + /// The app-server currently treats collaboration presets as experimental, + /// but plan mode is useful enough for SwiftASB to expose behind a small + /// Swift-owned value instead of asking callers to emulate slash commands. + public struct TurnCollaborationMode: Sendable, Equatable { + public enum Kind: String, Sendable, Equatable { + case defaultMode = "default" + case plan + } + + public var developerInstructions: String? + public var kind: Kind + public var model: String + public var reasoningEffort: ReasoningEffort? + + /// Creates a collaboration preset for a turn. + /// + /// `model` is required because the app-server collaboration-mode + /// settings require it. Thread helpers default this to the thread's + /// current model when a caller asks for plan mode. + public init( + kind: Kind, + model: String, + reasoningEffort: ReasoningEffort? = nil, + developerInstructions: String? = nil + ) { + self.kind = kind + self.model = model + self.reasoningEffort = reasoningEffort + self.developerInstructions = developerInstructions + } + + /// Starts the turn in Codex plan mode. + public static func plan( + model: String, + reasoningEffort: ReasoningEffort? = nil, + developerInstructions: String? = nil + ) -> Self { + .init( + kind: .plan, + model: model, + reasoningEffort: reasoningEffort, + developerInstructions: developerInstructions + ) + } + } + /// App-server response for a newly started turn. public struct TurnSession: Sendable, Equatable { public let turn: TurnInfo diff --git a/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift b/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift index c3ac830..c3cc8aa 100644 --- a/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift +++ b/Sources/SwiftASB/Public/CodexAppServer+WireMapping.swift @@ -226,7 +226,7 @@ extension CodexAppServer.TurnStartRequest { additionalContext: nil, approvalPolicy: approvalPolicy?.wireValue, approvalsReviewer: approvalsReviewer?.wireValue, - collaborationMode: nil, + collaborationMode: collaborationMode?.wireValue, cwd: currentDirectoryPath, effort: effort?.wireValue, environments: nil, @@ -245,6 +245,30 @@ extension CodexAppServer.TurnStartRequest { } } +extension CodexAppServer.TurnCollaborationMode { + var wireValue: CodexWireCollaborationMode { + CodexWireCollaborationMode( + mode: kind.wireValue, + settings: .init( + developerInstructions: developerInstructions, + model: model, + reasoningEffort: reasoningEffort?.wireValue + ) + ) + } +} + +extension CodexAppServer.TurnCollaborationMode.Kind { + var wireValue: CodexWireModeKind { + switch self { + case .defaultMode: + .modeKindDefault + case .plan: + .plan + } + } +} + extension CodexAppServer.TurnInput { var wireValue: CodexWireUserInput { CodexWireUserInput( diff --git a/Sources/SwiftASB/Public/CodexThread+Agenda.swift b/Sources/SwiftASB/Public/CodexThread+Agenda.swift index 27f7815..a879c51 100644 --- a/Sources/SwiftASB/Public/CodexThread+Agenda.swift +++ b/Sources/SwiftASB/Public/CodexThread+Agenda.swift @@ -75,13 +75,18 @@ extension CodexThread { @ObservationIgnored private var proposedPlanItemOrder: [String] + @ObservationIgnored + private let appServer: CodexAppServer + internal init( threadID: String, initialGoal: Goal?, + appServer: CodexAppServer, threadEvents: AsyncThrowingStream, turnEvents: AsyncThrowingStream ) { self.threadID = threadID + self.appServer = appServer self.currentPlan = nil self.goal = initialGoal self.goalStatus = initialGoal?.status @@ -124,16 +129,60 @@ extension CodexThread { turnEventTask?.cancel() } + /// Creates or updates this thread's app-server goal. + @discardableResult + public func setGoal(_ request: GoalSetRequest) async throws -> Goal { + let updatedGoal = try await appServer.setThreadGoal( + threadID: threadID, + request: request + ) + apply(goal: updatedGoal) + return updatedGoal + } + + /// Sets this thread's goal objective. + @discardableResult + public func setGoal( + _ objective: String, + tokenBudget: Int? = nil + ) async throws -> Goal { + try await setGoal( + .init( + objective: objective, + status: .active, + tokenBudget: tokenBudget + ) + ) + } + + /// Pauses this thread's current app-server goal. + @discardableResult + public func pauseGoal() async throws -> Goal { + try await setGoal(.init(status: .paused)) + } + + /// Resumes this thread's current app-server goal. + @discardableResult + public func resumeGoal() async throws -> Goal { + try await setGoal(.init(status: .active)) + } + + /// Clears this thread's app-server goal. + @discardableResult + public func clearGoal() async throws -> Bool { + let cleared = try await appServer.clearThreadGoal(threadID: threadID) + if cleared { + applyGoalCleared() + } + return cleared + } + private func apply(_ event: CodexThreadEvent) { switch event { case let .goalUpdated(update): - goal = update.goal - goalStatus = update.goal.status - updatedAt = update.goal.updatedAt + apply(goal: update.goal) case .goalCleared: - goal = nil - goalStatus = nil - updatedAt = nil + applyGoalCleared() case .started, .statusChanged, .diagnostic, @@ -149,6 +198,18 @@ extension CodexThread { } } + private func apply(goal: Goal) { + self.goal = goal + goalStatus = goal.status + updatedAt = goal.updatedAt + } + + private func applyGoalCleared() { + goal = nil + goalStatus = nil + updatedAt = nil + } + private func apply(_ event: CodexTurnEvent) { switch event { case let .planUpdated(update): diff --git a/Sources/SwiftASB/Public/CodexThread.swift b/Sources/SwiftASB/Public/CodexThread.swift index 497b7d5..2cf3fa8 100644 --- a/Sources/SwiftASB/Public/CodexThread.swift +++ b/Sources/SwiftASB/Public/CodexThread.swift @@ -186,6 +186,7 @@ public struct CodexThread: Sendable { public struct TurnStartRequest: Sendable, Equatable { public var approvalPolicy: CodexAppServer.ApprovalPolicy? public var approvalsReviewer: CodexAppServer.ApprovalsReviewer? + public var collaborationMode: CodexAppServer.TurnCollaborationMode? public var currentDirectoryPath: String? public var effort: CodexAppServer.ReasoningEffort? public var input: [CodexAppServer.TurnInput] @@ -205,6 +206,7 @@ public struct CodexThread: Sendable { input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, + collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, @@ -217,6 +219,7 @@ public struct CodexThread: Sendable { self.input = input self.approvalPolicy = approvalPolicy self.approvalsReviewer = approvalsReviewer + self.collaborationMode = collaborationMode self.currentDirectoryPath = currentDirectoryPath self.effort = effort self.model = model @@ -351,6 +354,7 @@ public struct CodexThread: Sendable { input: request.input, approvalPolicy: request.approvalPolicy, approvalsReviewer: request.approvalsReviewer, + collaborationMode: request.collaborationMode, currentDirectoryPath: request.currentDirectoryPath, effort: request.effort, model: request.model, @@ -370,6 +374,7 @@ public struct CodexThread: Sendable { _ text: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, + collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, @@ -384,6 +389,7 @@ public struct CodexThread: Sendable { input: [.text(text)], approvalPolicy: approvalPolicy, approvalsReviewer: approvalsReviewer, + collaborationMode: collaborationMode, currentDirectoryPath: currentDirectoryPath, effort: effort, model: model, @@ -396,6 +402,43 @@ public struct CodexThread: Sendable { ) } + /// Starts a plan-mode turn containing one text input item. + /// + /// This is the SwiftASB equivalent of a mode button for Codex planning: it + /// uses the app-server's collaboration-mode field instead of sending slash + /// command text through the prompt. + public func startPlanningTurn( + _ text: String, + approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, + approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, + currentDirectoryPath: String? = nil, + effort: CodexAppServer.ReasoningEffort? = nil, + model: String? = nil, + outputSchema: CodexAppServer.JSONValue? = nil, + permissions: CodexWorkspace.PermissionSelection? = nil, + personality: CodexAppServer.Personality? = nil, + serviceTier: CodexAppServer.ServiceTier? = nil, + summary: CodexAppServer.ReasoningSummary? = nil + ) async throws -> CodexTurnHandle { + try await startTextTurn( + text, + approvalPolicy: approvalPolicy, + approvalsReviewer: approvalsReviewer, + collaborationMode: .plan( + model: model ?? self.model, + reasoningEffort: effort ?? reasoningEffort + ), + currentDirectoryPath: currentDirectoryPath, + effort: effort, + model: model, + outputSchema: outputSchema, + permissions: permissions, + personality: personality, + serviceTier: serviceTier, + summary: summary + ) + } + /// Starts a code review against this thread's repository state. /// /// Inline reviews run on this thread. Detached reviews run on a new review @@ -447,6 +490,7 @@ public struct CodexThread: Sendable { return Agenda( threadID: id, initialGoal: initialGoal, + appServer: appServer, threadEvents: threadEvents, turnEvents: turnEvents ) diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md index 4e953e2..a285dee 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md @@ -25,7 +25,9 @@ Use ``setName(_:)`` for a human-readable title, ``updateMetadata(gitInfo:)`` for Rollback returns a refreshed thread handle. SwiftASB records a local rollback marker and trims visible local history to match the app-server response. It does not preserve the full removed-turn payload archive yet. -Use ``makeAgenda()`` when a UI wants current goal and plan state for this thread. Use ``readGoal()``, ``setGoal(_:)``, and ``clearGoal()`` when a non-UI caller needs direct goal reads or mutation. +Use ``startPlanningTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` when a mode button or segmented control should start the next turn in Codex plan mode without sending slash-command text through the prompt. + +Use ``makeAgenda()`` when a UI wants current goal and plan state for this thread. Agenda also exposes UI-friendly goal actions. Use ``readGoal()``, ``setGoal(_:)``, and ``clearGoal()`` when a non-UI caller needs direct goal reads or mutation. Use ``startReview(against:placement:)`` to ask the app-server to review repository state associated with this thread. The `against` subject can be uncommitted changes, a base branch, one commit, or custom instructions. ``ReviewPlacement/inline`` runs the review turn on this thread. ``ReviewPlacement/detached`` runs the review turn on a new review thread returned in ``CodexReviewHandle/reviewThreadID``. @@ -67,8 +69,6 @@ Recent observable startup can begin as an empty local-only view when the live ap - ``instructionSources`` - ``model`` - ``modelProvider`` -- ``projectInfo`` -- ``info/source`` - ``activePermissionProfile`` - ``permissionProfile`` - ``reasoningEffort`` @@ -80,7 +80,8 @@ Recent observable startup can begin as an empty local-only view when the live ap ### Turns - ``startTurn(_:)`` -- ``startTextTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` +- ``startTextTurn(_:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` +- ``startPlanningTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` - ``startReview(against:placement:)`` - ``TurnStartRequest`` - ``ReviewSubject`` diff --git a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md index a5df734..894c375 100644 --- a/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md +++ b/Sources/SwiftASB/SwiftASB.docc/GettingStartedWithSwiftASB.md @@ -78,5 +78,5 @@ Call ``CodexAppServer/start()`` and then ``CodexAppServer/initialize(_:)`` only - ``CodexAppServer/startThread(_:)`` - ``CodexThread`` -- ``CodexThread/startTextTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` +- ``CodexThread/startTextTurn(_:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` - ``CodexTurnHandle`` diff --git a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md index c683dd3..31e2bdc 100644 --- a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md +++ b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md @@ -85,6 +85,22 @@ final class ThreadInspectorModel { errorMessage = String(describing: error) } } + + func plan(_ prompt: String) async { + do { + let turn = try await thread.startPlanningTurn(prompt) + currentMinimap = turn.minimap + + for try await event in turn.events { + if case .completed = event { + _ = try await turn.complete() + return + } + } + } catch { + errorMessage = String(describing: error) + } + } } ``` @@ -102,6 +118,8 @@ Use ``CodexAppServer/Inventory`` when an app-wide UI needs model capabilities, M Use ``CodexThread/Agenda`` when a UI wants to show the thread's current task target, current accepted plan, and proposed plan text while Codex is still shaping it. SwiftASB reads the current goal, listens for goal changes, accepts authoritative plan snapshots, and treats experimental plan deltas as agenda state instead of making app code assemble them. +Use ``CodexThread/startPlanningTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` for explicit plan-mode UI controls. It sends the app-server collaboration-mode field rather than passing slash-command text as user input. + Recent companions keep caller-owned UI inputs mutable. For example, views can update selected file or command identifiers and visible item identifiers. SwiftASB uses that information to protect visible or selected payloads while slimming older low-value entries when the resident cache exceeds its budget. Start with automatic cache policies unless the UI has known density requirements. Use the named presets for ``CodexThread/RecentTurns`` and automatic policies for file and command companions when the initial page size is enough guidance. diff --git a/Tests/SwiftASBTests/Protocol/CodexAppServerProtocolTests.swift b/Tests/SwiftASBTests/Protocol/CodexAppServerProtocolTests.swift index 087b839..02b5079 100644 --- a/Tests/SwiftASBTests/Protocol/CodexAppServerProtocolTests.swift +++ b/Tests/SwiftASBTests/Protocol/CodexAppServerProtocolTests.swift @@ -786,7 +786,14 @@ struct CodexAppServerProtocolTests { additionalContext: nil, approvalPolicy: .enumeration(.onFailure), approvalsReviewer: .guardianSubagent, - collaborationMode: nil, + collaborationMode: .init( + mode: .plan, + settings: .init( + developerInstructions: nil, + model: "gpt-5.4", + reasoningEffort: .medium + ) + ), cwd: "/tmp/project", effort: .medium, environments: nil, @@ -828,6 +835,13 @@ struct CodexAppServerProtocolTests { #expect(params["summary"] as? String == "concise") #expect(params["threadId"] as? String == "thread-123") + let collaborationMode = try #require(params["collaborationMode"] as? [String: Any]) + #expect(collaborationMode["mode"] as? String == "plan") + let settings = try #require(collaborationMode["settings"] as? [String: Any]) + #expect(settings["model"] as? String == "gpt-5.4") + #expect(settings["reasoning_effort"] as? String == "medium") + #expect(settings["developer_instructions"] == nil) + let input = try #require(params["input"] as? [[String: Any]]) #expect(input.count == 1) #expect(input.first?["type"] as? String == "text") diff --git a/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift index 9062ba4..4c0d802 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerCompanionSurfaceTests.swift @@ -279,6 +279,60 @@ extension CodexAppServerTests { await client.stop() } + @MainActor + @Test("agenda sends user-friendly goal mutations") + func agendaSendsUserFriendlyGoalMutations() async throws { + let transport = FakeCodexAppServerTransport() + let client = CodexAppServer(transport: transport) + + try await client.start() + _ = try await client.initialize( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + + let thread = try await client.startThread( + .init( + currentDirectoryPath: "/tmp/project", + model: "gpt-5.4", + modelProvider: "openai" + ) + ) + let agenda = try await thread.makeAgenda() + + _ = try await agenda.setGoal("Ship plan mode", tokenBudget: 40_000) + _ = try await agenda.pauseGoal() + _ = try await agenda.resumeGoal() + _ = try await agenda.clearGoal() + + let goalSetPayloads = await transport.requestPayloads(for: "thread/goal/set") + #expect(goalSetPayloads.count == 3) + + let setGoalRequest = try companionDecodedJSONObject(from: goalSetPayloads[0]) + #expect(companionValue(at: ["params", "objective"], in: setGoalRequest) as? String == "Ship plan mode") + #expect(companionValue(at: ["params", "status"], in: setGoalRequest) as? String == "active") + #expect(companionValue(at: ["params", "tokenBudget"], in: setGoalRequest) as? Int == 40_000) + + let pauseRequest = try companionDecodedJSONObject(from: goalSetPayloads[1]) + #expect(companionValue(at: ["params", "status"], in: pauseRequest) as? String == "paused") + + let resumeRequest = try companionDecodedJSONObject(from: goalSetPayloads[2]) + #expect(companionValue(at: ["params", "status"], in: resumeRequest) as? String == "active") + + let clearPayload = try #require(await transport.recordedRequestPayload(for: "thread/goal/clear")) + let clearRequest = try companionDecodedJSONObject(from: clearPayload) + #expect(companionValue(at: ["params", "threadId"], in: clearRequest) as? String == thread.id) + #expect(agenda.goal == nil) + #expect(agenda.goalTitle == "") + + await client.stop() + } + @MainActor @Test("builds a minimap that stays live with turn events") func buildsTurnMinimap() async throws { @@ -462,3 +516,23 @@ extension CodexAppServerTests { } } + +private func companionDecodedJSONObject(from data: Data) throws -> [String: Any] { + try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) +} + +private func companionValue( + at path: [String], + in object: [String: Any] +) -> Any? { + var current: Any = object + for key in path { + guard let dictionary = current as? [String: Any], + let next = dictionary[key] + else { + return nil + } + current = next + } + return current +} diff --git a/Tests/SwiftASBTests/Public/CodexAppServerTurnLifecycleTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerTurnLifecycleTests.swift index 96f9fa3..c28b972 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerTurnLifecycleTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerTurnLifecycleTests.swift @@ -225,6 +225,50 @@ extension CodexAppServerTests { await client.stop() } + @Test("starts planning turns through collaboration mode") + func startsPlanningTurnsThroughCollaborationMode() async throws { + let transport = FakeCodexAppServerTransport() + let client = CodexAppServer(transport: transport) + + try await client.start() + _ = try await client.initialize( + .init( + clientInfo: .init( + name: "SwiftASBTests", + title: "SwiftASB Tests", + version: "0.1.0" + ) + ) + ) + + let thread = try await client.startThread( + .init( + currentDirectoryPath: "/tmp/project", + model: "gpt-5.4", + modelProvider: "openai" + ) + ) + + _ = try await thread.startPlanningTurn( + "Map this before editing.", + effort: .high + ) + + let turnStartPayload = try #require(await transport.recordedRequestPayload(for: "turn/start")) + let turnStartRequest = try #require(try JSONSerialization.jsonObject(with: turnStartPayload) as? [String: Any]) + let turnStartParams = try #require(turnStartRequest["params"] as? [String: Any]) + let collaborationMode = try #require(turnStartParams["collaborationMode"] as? [String: Any]) + #expect(collaborationMode["mode"] as? String == "plan") + let settings = try #require(collaborationMode["settings"] as? [String: Any]) + #expect(settings["model"] as? String == "gpt-5.4") + #expect(settings["reasoning_effort"] as? String == "high") + + let input = try #require(turnStartParams["input"] as? [[String: Any]]) + #expect(input.first?["text"] as? String == "Map this before editing.") + + await client.stop() + } + @Test("persists sealed turn history in the internal thread history store") func persistsSealedTurnHistory() async throws { let transport = FakeCodexAppServerTransport() diff --git a/docs/maintainers/thread-plan-goal-companion-plan.md b/docs/maintainers/thread-plan-goal-companion-plan.md index e357735..dcc8a1a 100644 --- a/docs/maintainers/thread-plan-goal-companion-plan.md +++ b/docs/maintainers/thread-plan-goal-companion-plan.md @@ -181,7 +181,7 @@ This pass should not yet create a high-level plan or goal authoring API. The first implementation should be read/exposure-only plus current low-level goal set/clear compatibility. -Later creation APIs can build on `Agenda` once the consumer model is settled: +Shipped creation and mutation APIs: - `agenda.setGoal(_ objective: String, tokenBudget: Int?)` - `agenda.pauseGoal()` @@ -189,7 +189,7 @@ Later creation APIs can build on `Agenda` once the consumer model is settled: - `agenda.clearGoal()` - `thread.startPlanningTurn(...)` -Those should be deliberate convenience APIs, not thin slash-command replicas. +These are deliberate convenience APIs, not thin slash-command replicas. ## Implementation Slices diff --git a/docs/maintainers/v1-public-api-symbol-inventory.md b/docs/maintainers/v1-public-api-symbol-inventory.md index 9b76723..25abf13 100644 --- a/docs/maintainers/v1-public-api-symbol-inventory.md +++ b/docs/maintainers/v1-public-api-symbol-inventory.md @@ -1,6 +1,6 @@ # V1 Public API Symbol Inventory -Generated from `swift package dump-symbol-graph --minimum-access-level public --skip-synthesized-members` on 2026-05-02 after the v0.128 generated-wire promotion and final pre-v1 public-surface tightening, then updated on 2026-05-05 for the post-v1 app-wide library snapshot, on 2026-05-06 for the public query descriptor, filesystem, config, extension-inventory, thread-goal, recent-activity descriptor, repository-grouping, workspace permission-profile, and file-discovery slices, on 2026-05-08 for the `CodexWorkspace.ProjectInfo` cleanup, `CodexWorkspace.WorktreeSnapshot` promotion, `CodexAppServer.Library` worktree-group helpers, `CodexAppServer.ThreadSource` promotion, and v0.129 hook compact event names, on 2026-05-15 for `CodexThread.sendShellCommand(_:)`, the `shellCommandExecution` feature category, and `CodexThread.startReview(against:placement:)`, on 2026-05-20 for the v0.133 schema compatibility refresh, and on 2026-05-30 for `CodexAppServer.Inventory`, `CodexMCP.statusSnapshot()`, `CodexMCP.readResource(...)`, and `CodexThread.Agenda`. This is a maintainer ledger for the v1 public API freeze plus accepted post-v1 app-wide additions; it records public/open declarations visible through the `SwiftASB` library product, excluding synthesized members. +Generated from `swift package dump-symbol-graph --minimum-access-level public --skip-synthesized-members` on 2026-05-02 after the v0.128 generated-wire promotion and final pre-v1 public-surface tightening, then updated on 2026-05-05 for the post-v1 app-wide library snapshot, on 2026-05-06 for the public query descriptor, filesystem, config, extension-inventory, thread-goal, recent-activity descriptor, repository-grouping, workspace permission-profile, and file-discovery slices, on 2026-05-08 for the `CodexWorkspace.ProjectInfo` cleanup, `CodexWorkspace.WorktreeSnapshot` promotion, `CodexAppServer.Library` worktree-group helpers, `CodexAppServer.ThreadSource` promotion, and v0.129 hook compact event names, on 2026-05-15 for `CodexThread.sendShellCommand(_:)`, the `shellCommandExecution` feature category, and `CodexThread.startReview(against:placement:)`, on 2026-05-20 for the v0.133 schema compatibility refresh, and on 2026-05-30 for `CodexAppServer.Inventory`, `CodexMCP.statusSnapshot()`, `CodexMCP.readResource(...)`, `CodexThread.Agenda`, and plan-mode turn helpers. This is a maintainer ledger for the v1 public API freeze plus accepted post-v1 app-wide additions; it records public/open declarations visible through the `SwiftASB` library product, excluding synthesized members. ## Last Full Snapshot Summary @@ -320,7 +320,7 @@ additions are recorded in the ledger sections below. - `CodexAppServer.TurnInput.Kind.init(rawValue:)` - `init?(rawValue: String)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift - `CodexAppServer.TurnInput.init(kind:text:url:path:name:)` - `init(kind: CodexAppServer.TurnInput.Kind, text: String? = nil, url: String? = nil, path: String? = nil, name: String? = nil)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift - `CodexAppServer.TurnInput.text(_:)` - `static func text(_ text: String) -> CodexAppServer.TurnInput` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift -- `CodexAppServer.TurnStartRequest.init(threadID:input:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:personality:serviceTier:summary:)` - `init(threadID: String, input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift +- `CodexAppServer.TurnStartRequest.init(threadID:input:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `init(threadID: String, input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift - `CodexAppServer.TurnStatus.init(rawValue:)` - `init?(rawValue: String)` - Sources/SwiftASB/Public/CodexAppServer+Compatibility.swift - `CodexAppServer.Library.Configuration.init(pageSize:maxPagesPerArchiveState:sortedBy:groupedBy:query:featurePolicy:reconcilesOnCreation:loadsAppSnapshotsOnCreation:hookListCurrentDirectoryPaths:)` - `init(pageSize: Int = 50, maxPagesPerArchiveState: Int = 1, sortedBy: CodexAppServer.Library.SortedBy = .updatedNewestFirst, groupedBy: CodexAppServer.Library.GroupedBy = .cwd, query: CodexAppServer.ThreadListQD = .init(), featurePolicy: SwiftASBFeaturePolicy = .defaults, reconcilesOnCreation: Bool = true, loadsAppSnapshotsOnCreation: Bool = true, hookListCurrentDirectoryPaths: [String]? = nil)` - Sources/SwiftASB/Public/CodexAppServer+Library.swift - `CodexAppServer.Library.refresh()` - `@MainActor func refresh() async` - Sources/SwiftASB/Public/CodexAppServer+Library.swift @@ -397,7 +397,7 @@ additions are recorded in the ledger sections below. - `CodexThread.RecentTurns.loadOlderTurns(limit:)` - `@MainActor func loadOlderTurns(limit: Int? = nil) async throws` - Sources/SwiftASB/Public/CodexThread+RecentTurns.swift - `CodexThread.RecentTurns.updateScrollActivity(phase:verticalVelocityPointsPerSecond:)` - `@MainActor func updateScrollActivity(phase: CodexThread.RecentTurns.ScrollActivityPhase, verticalVelocityPointsPerSecond: Double? = nil)` - Sources/SwiftASB/Public/CodexThread+RecentTurns.swift - `CodexThread.RecentTurns.updateVisibleTurnIDs(_:)` - `@MainActor func updateVisibleTurnIDs(_ ids: [String])` - Sources/SwiftASB/Public/CodexThread+RecentTurns.swift -- `CodexThread.TurnStartRequest.init(input:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:personality:serviceTier:summary:)` - `init(input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexThread.swift +- `CodexThread.TurnStartRequest.init(input:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `init(input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.compactContext()` - `func compactContext() async throws` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.makeDashboard()` - `@MainActor func makeDashboard() async -> CodexThread.Dashboard` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.makeRecentCommands(limit:cachePolicy:)` - `@MainActor func makeRecentCommands(limit: Int = 12, cachePolicy: CodexThread.RecentCommands.CachePolicy? = nil) async throws -> CodexThread.RecentCommands` - Sources/SwiftASB/Public/CodexThread.swift @@ -417,7 +417,8 @@ additions are recorded in the ledger sections below. - `CodexThread.sendShellCommand(_:)` - `func sendShellCommand(_ command: String) async throws` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.setName(_:)` - `func setName(_ name: String) async throws` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.startReview(against:placement:)` - `func startReview(against subject: CodexThread.ReviewSubject, placement: CodexThread.ReviewPlacement = .inline) async throws -> CodexReviewHandle` - Sources/SwiftASB/Public/CodexThread.swift -- `CodexThread.startTextTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `func startTextTurn(_ text: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil) async throws -> CodexTurnHandle` - Sources/SwiftASB/Public/CodexThread.swift +- `CodexThread.startTextTurn(_:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `func startTextTurn(_ text: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil) async throws -> CodexTurnHandle` - Sources/SwiftASB/Public/CodexThread.swift +- `CodexThread.startPlanningTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `func startPlanningTurn(_ text: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil) async throws -> CodexTurnHandle` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.startTurn(_:)` - `func startTurn(_ request: CodexThread.TurnStartRequest) async throws -> CodexTurnHandle` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.updateMetadata(gitInfo:)` - `func updateMetadata(gitInfo: CodexAppServer.ThreadMetadataGitInfoUpdate) async throws -> CodexAppServer.ThreadInfo` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.windowAroundItem(_:limit:)` - `func windowAroundItem(_ itemID: String, limit: Int = 12) async throws -> CodexThread.HistoryWindow` - Sources/SwiftASB/Public/CodexThread.swift @@ -456,7 +457,7 @@ additions are recorded in the ledger sections below. - `CodexAppServer.ThreadStartRequest.init(approvalPolicy:approvalsReviewer:baseInstructions:config:currentDirectoryPath:developerInstructions:ephemeral:model:modelProvider:personality:sandboxMode:serviceName:serviceTier:sessionStartSource:)` - `init(approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, baseInstructions: String? = nil, config: [String : CodexAppServer.JSONValue]? = nil, currentDirectoryPath: String? = nil, developerInstructions: String? = nil, ephemeral: Bool? = nil, model: String? = nil, modelProvider: String? = nil, personality: CodexAppServer.Personality? = nil, sandboxMode: CodexAppServer.SandboxMode? = nil, serviceName: String? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, sessionStartSource: CodexAppServer.SessionStartSource? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.ThreadTurnsListRequest.init(threadID:limit:cursor:sortDirection:)` - `init(threadID: String, limit: Int? = nil, cursor: String? = nil, sortDirection: CodexAppServer.ThreadTurnsSortDirection? = nil)` - Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift - `CodexAppServer.TurnInput.init(kind:text:url:path:name:)` - `init(kind: CodexAppServer.TurnInput.Kind, text: String? = nil, url: String? = nil, path: String? = nil, name: String? = nil)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift -- `CodexAppServer.TurnStartRequest.init(threadID:input:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:personality:serviceTier:summary:)` - `init(threadID: String, input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift +- `CodexAppServer.TurnStartRequest.init(threadID:input:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `init(threadID: String, input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift - `CodexAppServer.init(configuration:)` - `init(configuration: CodexAppServer.Configuration = .init())` - Sources/SwiftASB/Public/CodexAppServer.swift - `CodexAppServer.listLoadedThreads(_:)` - `func listLoadedThreads(_ request: CodexAppServer.LoadedThreadListRequest = .init()) async throws -> CodexAppServer.LoadedThreadListPage` - Sources/SwiftASB/Public/CodexAppServer.swift - `CodexAppServer.mcpServerStatusSnapshot()` - `func mcpServerStatusSnapshot() -> CodexAppServer.McpServerStatusPage` - Sources/SwiftASB/Public/CodexAppServer.swift @@ -480,7 +481,7 @@ additions are recorded in the ledger sections below. - `CodexThread.RecentTurns.loadNewerTurns(limit:)` - `@MainActor func loadNewerTurns(limit: Int? = nil) async throws` - Sources/SwiftASB/Public/CodexThread+RecentTurns.swift - `CodexThread.RecentTurns.loadOlderTurns(limit:)` - `@MainActor func loadOlderTurns(limit: Int? = nil) async throws` - Sources/SwiftASB/Public/CodexThread+RecentTurns.swift - `CodexThread.RecentTurns.updateScrollActivity(phase:verticalVelocityPointsPerSecond:)` - `@MainActor func updateScrollActivity(phase: CodexThread.RecentTurns.ScrollActivityPhase, verticalVelocityPointsPerSecond: Double? = nil)` - Sources/SwiftASB/Public/CodexThread+RecentTurns.swift -- `CodexThread.TurnStartRequest.init(input:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:personality:serviceTier:summary:)` - `init(input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexThread.swift +- `CodexThread.TurnStartRequest.init(input:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `init(input: [CodexAppServer.TurnInput], approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil)` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.makeRecentCommands(limit:cachePolicy:)` - `@MainActor func makeRecentCommands(limit: Int = 12, cachePolicy: CodexThread.RecentCommands.CachePolicy? = nil) async throws -> CodexThread.RecentCommands` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.makeRecentFiles(limit:cachePolicy:)` - `@MainActor func makeRecentFiles(limit: Int = 12, cachePolicy: CodexThread.RecentFiles.CachePolicy? = nil) async throws -> CodexThread.RecentFiles` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.makeRecentTurns(limit:cachePolicy:)` - `@MainActor func makeRecentTurns(limit: Int = 12, cachePolicy: CodexThread.RecentTurns.CachePolicy? = nil) async throws -> CodexThread.RecentTurns` - Sources/SwiftASB/Public/CodexThread.swift @@ -490,7 +491,7 @@ additions are recorded in the ledger sections below. - `CodexThread.readOlderTurnHistoryWindow(olderThan:limit:)` - `func readOlderTurnHistoryWindow(olderThan turnID: String, limit: Int = 12) async throws -> CodexThread.HistoryWindow` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.readRecentTurnHistory(limit:)` - `func readRecentTurnHistory(limit: Int = 12) async throws -> [CodexTurnHandle.ClosedTurn]` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.readRecentTurnHistoryWindow(limit:)` - `func readRecentTurnHistoryWindow(limit: Int = 12) async throws -> CodexThread.HistoryWindow` - Sources/SwiftASB/Public/CodexThread.swift -- `CodexThread.startTextTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:personality:serviceTier:summary:)` - `func startTextTurn(_ text: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil) async throws -> CodexTurnHandle` - Sources/SwiftASB/Public/CodexThread.swift +- `CodexThread.startTextTurn(_:approvalPolicy:approvalsReviewer:collaborationMode:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)` - `func startTextTurn(_ text: String, approvalPolicy: CodexAppServer.ApprovalPolicy? = nil, approvalsReviewer: CodexAppServer.ApprovalsReviewer? = nil, collaborationMode: CodexAppServer.TurnCollaborationMode? = nil, currentDirectoryPath: String? = nil, effort: CodexAppServer.ReasoningEffort? = nil, model: String? = nil, outputSchema: CodexAppServer.JSONValue? = nil, permissions: CodexWorkspace.PermissionSelection? = nil, personality: CodexAppServer.Personality? = nil, serviceTier: CodexAppServer.ServiceTier? = nil, summary: CodexAppServer.ReasoningSummary? = nil) async throws -> CodexTurnHandle` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.windowAroundItem(_:limit:)` - `func windowAroundItem(_ itemID: String, limit: Int = 12) async throws -> CodexThread.HistoryWindow` - Sources/SwiftASB/Public/CodexThread.swift - `CodexThread.windowAroundTurn(_:limit:)` - `func windowAroundTurn(_ turnID: String, limit: Int = 12) async throws -> CodexThread.HistoryWindow` - Sources/SwiftASB/Public/CodexThread.swift @@ -782,7 +783,8 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `CodexAppServer.Inventory` now owns routine app-wide observable inventory: `Configuration`, `Phase`, `appListPage`, `apps`, `modelCapabilities`, `mcpServers`, `hookListSnapshot`, `skillListSnapshot`, `skillEntries`, `skills`, `pluginListSnapshot`, `pluginMarketplaces`, `collaborationModes`, `collaborationModeEntries`, `refresh()`, and `makeInventory(configuration:)`. - `CodexMCP` now owns detail-oriented MCP helpers beside installs: `statusSnapshot()`, `readResource(_:)`, and `readResource(server:uri:threadID:)`. - `CodexThread` now exposes thread goals: `Goal`, `Goal.Status`, `GoalSetRequest`, `readGoal()`, `setGoal(_:)`, and `clearGoal()`. -- `CodexThread.Agenda` now owns thread plan and goal presentation state: `Plan`, `Plan.Step`, `Plan.Step.Status`, `ProposedPlan`, `ProposedPlan.Item`, `threadID`, `goal`, `goalStatus`, `goalTitle`, `currentPlan`, `proposedPlan`, `planTitle`, `updatedAt`, and `CodexThread.makeAgenda()`. +- `CodexThread.Agenda` now owns thread plan and goal presentation state: `Plan`, `Plan.Step`, `Plan.Step.Status`, `ProposedPlan`, `ProposedPlan.Item`, `threadID`, `goal`, `goalStatus`, `goalTitle`, `currentPlan`, `proposedPlan`, `planTitle`, `updatedAt`, `setGoal(...)`, `pauseGoal()`, `resumeGoal()`, `clearGoal()`, and `CodexThread.makeAgenda()`. +- `CodexAppServer.TurnCollaborationMode`, `CodexAppServer.TurnCollaborationMode.Kind`, optional `collaborationMode` fields on turn-start requests, and `CodexThread.startPlanningTurn(...)` now expose app-server plan mode without sending slash-command text through prompts. - `CodexThread.Dashboard` now exposes `goalTitle` and `planTitle` summaries instead of the previous full `goal` value. - The v0.133 compatibility refresh adds `CodexThread.Goal.Status.blocked`, `CodexThread.Goal.Status.usageLimited`, `CodexRemoteControlStatusDiagnostic.installationID`, `CodexRemoteControlStatusDiagnostic.serverName`, and the `subagentStart` / `subagentStop` hook event cases on app-wide hook metadata and thread dashboard hook runs. - `CodexThreadEvent` now includes `.goalUpdated(_:)` and `.goalCleared(_:)` for app-server goal notifications. @@ -809,7 +811,7 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `Sources/SwiftASB/Public/CodexAppServer+Models.swift`: 23 public properties - `Sources/SwiftASB/Public/CodexAppServer+ThreadLifecycle.swift`: 106 public properties - `Sources/SwiftASB/Public/CodexAppServer+ThreadManagement.swift`: 10 public properties -- `Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift`: 24 public properties +- `Sources/SwiftASB/Public/CodexAppServer+TurnLifecycle.swift`: 29 public properties - `Sources/SwiftASB/Public/CodexConfig.swift`: 18 public properties - `Sources/SwiftASB/Public/CodexDiagnostics.swift`: 29 public properties - `Sources/SwiftASB/Public/CodexErrors.swift`: 1 public properties @@ -821,7 +823,7 @@ The 2026-05-06 app-server schema promotion added several hand-owned public names - `Sources/SwiftASB/Public/CodexThread+RecentCommands.swift`: 25 public properties - `Sources/SwiftASB/Public/CodexThread+RecentFiles.swift`: 25 public properties - `Sources/SwiftASB/Public/CodexThread+RecentTurns.swift`: 54 public properties -- `Sources/SwiftASB/Public/CodexThread.swift`: 71 public properties +- `Sources/SwiftASB/Public/CodexThread.swift`: 72 public properties - `Sources/SwiftASB/Public/CodexTurnHandle.swift`: 108 public properties - `Sources/SwiftASB/Public/CodexWorkspace.swift`: 63 public properties - `Sources/SwiftASB/Public/SwiftASBFeatureOperationEvent.swift`: 20 public properties From e08e9abeb13c73dc75fcbc5d25dd1b1c0e88f2ad Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 30 May 2026 17:18:49 -0400 Subject: [PATCH 4/6] docs: clarify plan and goal workflow --- README.md | 2 ++ ROADMAP.md | 1 + Sources/SwiftASB/SwiftASB.docc/CodexThread.md | 2 ++ .../SwiftASB.docc/SwiftUIObservableCompanions.md | 2 ++ docs/maintainers/thread-plan-goal-companion-plan.md | 10 +++++++++- 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9333f11..a5a8ab9 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,8 @@ Use `CodexThread.makeAgenda()` when a SwiftUI surface needs the thread's current Use `CodexThread.startPlanningTurn(...)` when a mode button or segmented control should start the next turn in Codex plan mode without sending slash-command text through the prompt. Advanced callers can use `CodexAppServer.TurnCollaborationMode` directly on a turn-start request. +Plan and goal controls are intentionally separate for now. The recommended workflow is to use plan mode first to shape complex or ambiguous work, then set a persistent goal from the accepted objective when the host app or user is ready to track execution. SwiftASB does not currently auto-create goals from plan prompts or auto-promote completed plans into goals. + Use `CodexThread.startReview(against:placement:)` to start app-server code reviews from a thread. The public API uses hand-owned Swift subjects such as `.uncommittedChanges`, `.baseBranch("main")`, `.commit(sha:title:)`, and `.custom(instructions:)`; `placement: .inline` runs the review turn on the current thread, while `.detached` runs it on a returned review thread. Use `CodexThread.sendShellCommand(_:)` only for explicit user-level shell access. It sends a literal shell command string through app-server `thread/shellCommand`, preserves shell syntax such as pipes and redirects, and is documented upstream as unsandboxed full-user shell execution. SwiftASB keeps its internal `command/exec` helper path separate because that path is argv-shaped app-server command execution for SwiftASB-owned helper intents. `sendShellCommand(_:)` is gated by the disabled-by-default `shellCommandExecution` feature category. diff --git a/ROADMAP.md b/ROADMAP.md index 0082522..0dfdc34 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1376,6 +1376,7 @@ Completed - [ ] Add a one-shot `run(...)` convenience API once the lower-level handle model is stable enough to hide honestly. - [ ] Add optional suggested-goal generation that returns candidate goal strings from a prompt or current agenda state without mutating the thread until a host app or user accepts one. +- [ ] Add an optional accepted-plan-to-goal workflow that can stage a "set this plan as the goal" action after plan mode produces an accepted plan, without creating goals from raw planning prompts. - [ ] Add an optional auto-plan mode feature policy that can suggest or select plan mode for prompts likely to need planning, while keeping explicit mode controls as the default behavior. - [ ] Add a broader public history cursor or transcript search surface after the local history contract is clearer. - [ ] Add richer MCP progress detail either as public event cases or as deeper observable companion state. diff --git a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md index a285dee..aed5ba8 100644 --- a/Sources/SwiftASB/SwiftASB.docc/CodexThread.md +++ b/Sources/SwiftASB/SwiftASB.docc/CodexThread.md @@ -29,6 +29,8 @@ Use ``startPlanningTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath: Use ``makeAgenda()`` when a UI wants current goal and plan state for this thread. Agenda also exposes UI-friendly goal actions. Use ``readGoal()``, ``setGoal(_:)``, and ``clearGoal()`` when a non-UI caller needs direct goal reads or mutation. +Plan and goal actions are separate controls. Use plan mode first to shape complex or ambiguous work, then set a goal from the accepted objective when a user or host app is ready to track execution. SwiftASB does not currently auto-create goals from plan prompts or auto-promote completed plans into goals. + Use ``startReview(against:placement:)`` to ask the app-server to review repository state associated with this thread. The `against` subject can be uncommitted changes, a base branch, one commit, or custom instructions. ``ReviewPlacement/inline`` runs the review turn on this thread. ``ReviewPlacement/detached`` runs the review turn on a new review thread returned in ``CodexReviewHandle/reviewThreadID``. Use ``sendShellCommand(_:)`` only when a host app intentionally wants to send a literal user-level shell command to the thread shell. This is not the same operation as SwiftASB's internal `command/exec` helper path. `command/exec` sends argv-shaped helper commands through the app-server command runner for SwiftASB-owned intents such as Git fact refreshes or extension maintenance. `thread/shellCommand` preserves shell syntax such as pipes, redirects, and quoting, and the upstream schema documents that it runs unsandboxed with the user's full shell access instead of inheriting the thread sandbox policy. diff --git a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md index 31e2bdc..deb229b 100644 --- a/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md +++ b/Sources/SwiftASB/SwiftASB.docc/SwiftUIObservableCompanions.md @@ -120,6 +120,8 @@ Use ``CodexThread/Agenda`` when a UI wants to show the thread's current task tar Use ``CodexThread/startPlanningTurn(_:approvalPolicy:approvalsReviewer:currentDirectoryPath:effort:model:outputSchema:permissions:personality:serviceTier:summary:)`` for explicit plan-mode UI controls. It sends the app-server collaboration-mode field rather than passing slash-command text as user input. +Keep plan and goal actions explicit in host UI. A good default is a Plan control that runs a planning turn, a Goal editor that calls ``CodexThread/Agenda/setGoal(_:tokenBudget:)``, and a separate user-confirmed step that turns an accepted plan into a persistent goal if the app wants that workflow later. + Recent companions keep caller-owned UI inputs mutable. For example, views can update selected file or command identifiers and visible item identifiers. SwiftASB uses that information to protect visible or selected payloads while slimming older low-value entries when the resident cache exceeds its budget. Start with automatic cache policies unless the UI has known density requirements. Use the named presets for ``CodexThread/RecentTurns`` and automatic policies for file and command companions when the initial page size is enough guidance. diff --git a/docs/maintainers/thread-plan-goal-companion-plan.md b/docs/maintainers/thread-plan-goal-companion-plan.md index dcc8a1a..23dde96 100644 --- a/docs/maintainers/thread-plan-goal-companion-plan.md +++ b/docs/maintainers/thread-plan-goal-companion-plan.md @@ -13,7 +13,10 @@ Status: the first implementation pass shipped `CodexThread.Agenda`, Official Codex docs describe plan mode as the way to ask Codex for a multi-step execution plan before implementation work starts. They describe goals as a -persistent thread target, preferably shaped with a plan first. +persistent thread target, preferably shaped with a plan first. SwiftASB should +therefore keep plan and goal controls explicit in the shipped API: planning +creates or updates plan state, and goal actions mutate persisted goal state only +when the host app or user asks for that mutation. The current generated app-server schema exposes: @@ -191,6 +194,11 @@ Shipped creation and mutation APIs: These are deliberate convenience APIs, not thin slash-command replicas. +Combined plan-plus-goal workflows are intentionally deferred. Future APIs may +suggest goal strings from accepted plans, stage a "set this plan as the goal" +action, or optionally recommend plan mode for complex prompts, but the current +surface should not auto-create a goal from a raw planning prompt. + ## Implementation Slices 1. Add `CodexThread.Agenda` and `makeAgenda()`. Shipped. From b72b56c6e95acb423e4c2d10d19e256db3941e32 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 30 May 2026 17:38:52 -0400 Subject: [PATCH 5/6] release: bump versions for v1.6.0 --- 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 a5a8ab9..f9948bd 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.5.0` is the current and latest release. +SwiftASB is actively maintained and supported by Gale. Our current API is v1, and `v1.6.0` 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.5.0"), +.package(url: "https://github.com/gaelic-ghost/SwiftASB", from: "1.6.0"), ``` Then add the library product to your target dependencies: diff --git a/ROADMAP.md b/ROADMAP.md index 0dfdc34..6838d70 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -80,7 +80,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.5.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.6.0` 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. | @@ -108,7 +108,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.5.0` baseline. +`v1.6.0` 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 @@ -226,7 +226,7 @@ After those audit hardening items, the current broader priority order is: ## V1 Readiness Checklist -This checklist records the work that made `SwiftASB` ready for the `v1.5.0` +This checklist records the work that made `SwiftASB` ready for the `v1.6.0` 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. @@ -427,8 +427,8 @@ workflow earns them in a later feature release. ### Documentation And Examples -- [x] Update stale release references after the `v1.5.0` release. - Decision: README now names `v1.5.0` as the current released baseline and no +- [x] Update stale release references after the `v1.6.0` release. + Decision: README now names `v1.6.0` 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. @@ -629,10 +629,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.5.0`, or whether the remaining work should go straight into the v1 + `v1.6.0`, 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.5.0` release branch. + straight into the `v1.6.0` 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. @@ -686,7 +686,7 @@ workflow earns them in a later feature release. #### Migration Notes - Existing `v0.9.x` consumers should update the SwiftPM dependency to - `from: "1.5.0"` once the tag is published. + `from: "1.6.0"` 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 @@ -711,7 +711,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.5.0` should be rechecked after the patch tag is indexed. + `v1.6.0` 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, @@ -1321,7 +1321,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.5.0` boundary through the public API audit, + Decision: completed for the `v1.6.0` 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 6c87ff5..b2e020b 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.5.0`, not to expose every generated +supported app-server lifecycle before `v1.6.0`, not to expose every generated wire family. ## Current Public Source Inventory @@ -432,7 +432,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.5.0` release boundary. Default-bearing public + Decision: complete for the `v1.6.0` 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 @@ -514,7 +514,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.5.0` as the current released baseline. + Decision: README now names `v1.6.0` 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 9afa3ebf5a2deeef51eed2afc0798eabf6c77043 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sat, 30 May 2026 17:40:17 -0400 Subject: [PATCH 6/6] tests: tolerate completed recent file refresh --- Tests/SwiftASBTests/Public/CodexAppServerRecentFilesTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftASBTests/Public/CodexAppServerRecentFilesTests.swift b/Tests/SwiftASBTests/Public/CodexAppServerRecentFilesTests.swift index 47d5594..523aabb 100644 --- a/Tests/SwiftASBTests/Public/CodexAppServerRecentFilesTests.swift +++ b/Tests/SwiftASBTests/Public/CodexAppServerRecentFilesTests.swift @@ -293,7 +293,7 @@ extension CodexAppServerTests { let liveFile = try #require(recentFiles.files.first(where: { $0.id == "\(turn.turn.id):item-file-live" })) let olderFile = try #require(recentFiles.files.first(where: { $0.id == "turn-older:item-file-older" })) - #expect(liveFile.status == .inProgress) + #expect(liveFile.status != .errored) #expect(liveFile.payloadText?.contains("+dependency") == true) #expect(olderFile.isPayloadComplete == false) #expect(olderFile.payloadText == nil)