F107: Single core ↔ interfaces facade
Scope
In Scope
- Application facade port (
WorkflowFacade) exposing async-first Run/Resume (returning RunSession) and sync List/Validate/Status/History
- Canonical
pack/workflow resolver consolidating CLI, TUI, HTTP, ACP, and subworkflow name resolution
- Bidirectional
RunSession with bounded replay buffer indexed by Seq, idempotent Close, and Respond() for input collection
- Adapter composing
WorkflowService, ExecutionService, HistoryService, and Resolver; sole subscriber of the F106 Recorder exchange stream (Recorder.Subscribe()) and owner of 4 edge bridges (EventPublisher, OutputWriters, DisplayRenderer, bidirectional UserInputReader)
- Event projection derived from F106
transcript.ExchangeEvent onto neutral facade.Event, reusing the Seq already carried by the transcript
- Provider-agnostic event contract: Claude, Gemini, Codex, Copilot, and OpenAI-compatible agents surface through one
facade.Event schema by reusing F106's ContentBlock normalization — zero provider- or interface-specific branching past the edge adapters
- 3-layer error handling with single
StructuredError → ErrorCode mapping table and pure per-interface translators
- Per-interface migration (CLI, TUI, HTTP, ACP) onto the facade, removing duplicated resolution, polling, and direct
JSONStore access
- Status/History served exclusively via facade port methods
- Shared conformance test suite (golden) across all 4 interfaces
Out of Scope
- Live multi-viewer fan-out of a single session to multiple concurrent consumers
- Exposing workflows as MCP tools (possible via
Run+Resolver but deferred)
- Merging or removing the underlying application services (
WorkflowService, ExecutionService, HistoryService)
- Business logic rewrite — facade composes via delegation only
- Honest isolation validation surfaced in
ValidateWorkflow (F108 Axis A consumes the surface this feature opens)
Deferred
| Item |
Rationale |
Follow-up |
| Live multi-viewer fan-out |
Adds session-level multiplexing complexity orthogonal to facade unification |
future |
| MCP tools surfacing workflows |
Requires MCP-side wiring distinct from interface migration scope |
future |
| Application service merger |
Facade delegates; collapsing services is a separate refactor |
future |
Honest isolation validation in ValidateWorkflow |
Owned by F108 Axis A; F107 only opens the surface |
F108 |
User Stories
US1: Unified workflow execution entry point across all interfaces (P1 - Must Have)
As a maintainer of AWF integrating a new interface (CLI, TUI, HTTP, ACP),
I want a single WorkflowFacade port with Run/Resume returning a bidirectional RunSession,
So that I implement only the presentation edge and inherit identical resolution, execution, events, and error semantics.
Why this priority: Without a single entry point, every interface re-implements pack/workflow resolution, polling vs push divergence, status access, and error mapping — the divergence the research §1.1–§1.4 calls out as the structural debt this feature must eliminate. P1 because no downstream value (conformance, F108 Axis A) is reachable without it.
Acceptance Scenarios:
- Given a workflow
pack/workflow exists in the repository, When any interface invokes WorkflowFacade.Run(ctx, RunRequest), Then the same canonical Resolver parses the identifier, ExecutionService runs the workflow, and a RunSession exposes Events() derived from the F106 transcript.
- Given a
RunSession is active, When the consumer calls Close() multiple times, Then the session terminates exactly once, drains pending events, and subsequent calls are idempotent no-ops.
- Given CLI, TUI, HTTP, and ACP each consume
Events() from the same RunRequest, When the workflow emits a step output, Then all 4 interfaces receive a facade.Event of identical kind/payload before per-interface presentation.
Independent Test: Implement a facadetest fake port producing a scripted sequence of Events; run a CLI integration test that calls Run then Drain, asserting exit code derived from the terminal event and stdout matching the scripted projection.
US2: Bidirectional input collection unifies interactive prompts (P1 - Must Have)
As a workflow author whose step requires user input mid-execution,
I want the facade to emit EventInputRequired and accept RunSession.Respond(InputResponse),
So that CLI interactive collection, ACP parking, conversational multi-turn, and missing-input recovery share one bidirectional protocol.
Why this priority: Research §1.6 identifies input collection as the most fragmented surface — ACPInputReader and TUIInputReader are interface-specific implementations of the same protocol. Without unification, the facade cannot replace them. P1 because the conformance suite must verify identical input semantics across interfaces.
Acceptance Scenarios:
- Given a running workflow reaches a step requiring input, When the executor calls the bridged
UserInputReader, Then an Event{Kind: EventInputRequired, InputRequest} is emitted on Events() and the executor blocks until Respond is called.
- Given a consumer calls
RunSession.Respond(InputResponse), When the response reaches the bridge, Then the blocked UserInputReader returns the response value and the workflow resumes.
- Given the session is closed before
Respond is called, When the executor times out or context cancels, Then the executor receives an error and a terminal Event{workflow_failed} is emitted with ErrorCode mapping the cancellation cause.
Independent Test: Drive the facadetest fake to emit EventInputRequired, call Respond on a separate goroutine, and assert the fake executor observed the response value identically; repeat with a Close() before Respond and assert terminal failure event.
US3: Canonical resolver replaces 3 divergent implementations (P1 - Must Have)
As a subworkflow author or interface integrator,
I want one Resolver that parses pack/workflow, validates the manifest, and loads via PackDiscoverer.LoadWorkflow or repo.Load,
So that identifier resolution behaves identically whether reached from CLI, TUI, HTTP recomposeIdentifier, ACP :↔/, or SplitCallWorkflowName.
Why this priority: Three divergent resolvers (pack_resolver.go, command.go:resolvePackWorkflow, subworkflow_executor.go:SplitCallWorkflowName) produce different errors for the same identifier. P1 because every interface migration in US1 depends on a single resolution semantics.
Acceptance Scenarios:
- Given the identifier
core/build, When any of CLI, TUI, HTTP, ACP, or subworkflow execution calls the resolver, Then the same Workflow is loaded or the same ErrorCode is returned.
- Given a presentation-layer wire identifier (HTTP
pack.workflow, ACP pack:workflow), When the per-interface adapter transforms it to canonical pack/workflow, Then the resolver receives the canonical form and round-trips back through the adapter without loss.
- Given a malformed identifier missing the pack segment, When the resolver runs, Then an
ErrorCode corresponding to invalid identifier is returned and mapped to interface-appropriate exit code / HTTP status / RPC code by the pure translator.
Independent Test: Table-driven test feeding the 4 wire conventions (CLI, HTTP, ACP, MCP) through their adapters into the canonical resolver and asserting round-trip equality plus identical error codes for malformed inputs. For nested call_workflow, assert the resolved subworkflow run correlates to its parent via the transcript's ParentRunID/ChildRunID (one identity model, FR-019) rather than a parallel scheme.
US4: Status, history, and resume served from the facade (P2 - Should Have)
As a CLI user running awf status or awf resume-list,
I want the commands to query WorkflowFacade.Status/History instead of JSONStore directly,
So that status access shares the same authorization, identifier resolution, and error mapping as Run.
Why this priority: Research §1.3 identifies status/history living outside the core as a coupling violation. P2 because the divergence is functional but not user-blocking — CLI works today, just inconsistently with HTTP/ACP.
Acceptance Scenarios:
- Given a workflow has previously executed, When
awf status <workflow-id> runs, Then the CLI calls WorkflowFacade.Status(ctx, id) and renders the returned RunResult/RunStatus without touching JSONStore.
- Given resumable workflows exist, When
awf resume-list runs, Then the CLI calls WorkflowFacade.History(ctx, HistoryFilter{Resumable: true}) and renders []RunRecord identically to HTTP /runs?resumable=true.
- Given
awf resume <id> runs, When the facade processes the request, Then resume flows through the same Run/Resume port (via ExecutionSetup.Build()) as initial execution.
Independent Test: Replace JSONStore in the test with facadetest; assert awf status and awf resume-list produce identical output to running the equivalent HTTP endpoint against the same fake.
US5: Conformance suite proves interface parity (P2 - Should Have)
As a maintainer reviewing a PR that touches any interface,
I want a shared golden conformance suite covering CLI stdout, ACP session/update, SSE frames, and TUI tea.Msg,
So that divergence between interfaces breaks CI rather than slipping into production.
Why this priority: Without a conformance suite the facade gains can erode silently as each interface is maintained independently. P2 because it protects the investment but does not deliver the facade itself.
Acceptance Scenarios:
- Given a scripted
facadetest event sequence, When the conformance suite projects it through all 4 interface adapters, Then the output matches golden files for CLI stdout, ACP updates, SSE frames, and TUI messages.
- Given a developer modifies one interface adapter to drop an event kind, When CI runs, Then the conformance suite fails with a clear diff indicating which interface diverged.
Independent Test: Run the conformance suite against the facadetest fake; deliberately mutate one adapter to skip an event kind and verify the suite fails.
Edge Cases
- What happens when a consumer reads
Events() slowly and the bounded replay buffer overflows? — The session must apply a documented backpressure or drop policy keyed by Seq so HTTP SSE replay-from-Seq remains coherent.
- How does the system handle
Close() called concurrently from multiple goroutines? — Idempotent close must use a sync.Once-style guard; subsequent calls return without error and do not panic on closed channels.
- What is the behavior when
Respond() is called after the session has terminated? — Respond returns a non-fatal error indicating the session is closed; no event is emitted, no panic.
- What happens when the resolver receives a canonical identifier with characters reserved by a presentation layer (e.g.,
: for ACP)? — The CLI/canonical form accepts it; only the per-interface adapter is responsible for wire encoding/decoding; the resolver does not enforce wire constraints.
- How does the facade handle a
StructuredError not present in the mapping table? — A default ErrorCode (ErrInternal) is returned with the original error preserved for logging; the mapping table is exhaustive-checked in tests.
- What happens when a new agent provider emits an unknown
transcript.EventType or ContentBlock.BlockType? — The projection fails closed: a defined fallback EventKind is emitted with a non-fatal projection error logged; the event is never silently dropped and never panics. New provider divergence is absorbed at the F106 normalizer (FR-017), so the facade contract does not widen.
- What happens when
Respond() is called for an input the session never requested, or twice for the same request? — The bridge rejects the unexpected/duplicate response with a non-fatal error; the executor's blocking read is satisfied at most once.
- What happens when two interfaces concurrently call
Run for the same RunRequest? — Each call produces a distinct RunSession with its own ID; the facade does not deduplicate; the registry stores sessions by ID.
Requirements
Functional Requirements
- FR-001: System MUST expose
WorkflowFacade with synchronous List, Validate, Status, History and asynchronous Run, Resume returning RunSession.
- FR-002: System MUST provide a canonical
Resolver that parses pack/workflow, validates the manifest, and loads via PackDiscoverer.LoadWorkflow or repo.Load, replacing pack_resolver.go, command.go:resolvePackWorkflow, and subworkflow_executor.go:SplitCallWorkflowName.
- FR-003: System MUST provide per-interface presentation adapters at the edge (CLI identity, HTTP
recomposeIdentifier, ACP :↔/, MCP _↔/) that convert wire identifiers to canonical form before invoking the resolver.
- FR-004: System MUST implement an
Adapter composing WorkflowService, ExecutionService, HistoryService, and Resolver. The adapter MUST be the sole subscriber of the F106 Recorder exchange stream (Recorder.Subscribe() <-chan transcript.ExchangeEvent) and MUST own exactly 4 edge bridges — EventPublisher, OutputWriters, DisplayRenderer, and bidirectional UserInputReader — each with a stub-acceptable implementation for every interface so no interface is left half-wired.
- FR-005: System MUST project
facade.Event from three sources merged into one ordered stream: (1) the primary source — transcript.ExchangeEvent from Recorder.Subscribe(), reusing its ContentBlock-normalized, provider-agnostic payloads; (2) EventInputRequired, synthesized by the UserInputReader bridge (the transcript does not carry an input.required type); (3) the terminal outcome (workflow_completed/workflow_failed), derived from the synchronous StructuredError/Err() path rather than parsed from a transcript payload. The merge MUST preserve Seq monotonicity for the transcript-sourced events.
- FR-006: System MUST implement
RunSession with ID, Events, Respond, Err, and idempotent Close, backed by a bounded replay buffer keyed by ExchangeEvent.Seq — the transcript's existing sequence number MUST be reused, not regenerated, so HTTP SSE replay-from-Seq stays coherent with the on-disk transcript.
- FR-007: System MUST emit
Event{Kind: EventInputRequired} (synthesized by the UserInputReader bridge, not sourced from the transcript) and accept RunSession.Respond(InputResponse) to unify interactive input collection across CLI, ACP, conversational, and missing-input scenarios. This MUST replace and remove the interface-specific ACPInputReader and TUIInputReader.
- FR-008: System MUST implement a single
StructuredError → ErrorCode mapping table and pure per-interface translators (exitCode, httpStatus, rpcCode). Mapping MUST fail closed: an unmapped StructuredError resolves to ErrInternal (original error preserved for logging), never panics, and is caught by an exhaustive-case test (NFR-005).
- FR-009: System MUST surface
Validate results as ValidationReport, generalizing the 404-vs-422 distinction across interfaces.
- FR-010: Users MUST be able to invoke CLI
run, resume, status, resume-list through the facade with identical resolution and error semantics as HTTP and ACP.
- FR-011: System MUST migrate TUI from 200ms polling to
for e := range sess.Events().
- FR-012: System MUST migrate HTTP to 202 + SSE replay-from-
Seq + /respond + terminal event, removing the parallel eventRegistry and ad-hoc in-memory state.
- FR-013: System MUST migrate ACP to a projector mapping
facade.Event → session/update, reusing F105's toSDKUpdate.
- FR-014: System MUST remove direct
JSONStore access from CLI status and resume-list, routing through ExecutionService.ListResumable() via the facade.
- FR-015: System MUST maintain a session
registry keyed by ID for HTTP/cloud consumers and a Drain(sess) helper for CLI synchronous use.
- FR-016: System MUST define an explicit, exhaustive
transcript.EventType → facade.EventKind mapping table covering all 10 F106 event types (run.started, run.completed, step.started, step.completed, step.call_workflow.started, step.call_workflow.completed, message.user, message.assistant, tool.call, tool.result). An unknown EventType or ContentBlock.BlockType (e.g. emitted by a future provider) MUST fail closed — mapped to a defined fallback kind and surfaced as a non-fatal projection error — and MUST NOT panic or be silently dropped.
- FR-017:
facade.Event projection MUST be provider-agnostic: no branch on agent provider (Claude/Gemini/Codex/Copilot/OpenAI-compatible) is permitted past F106's ContentBlock normalization. Provider divergence MUST be absorbed at the F106 normalizer, not re-introduced in the facade.
- FR-018: System MUST consume the masked event stream: secret masking applied by the F106
Recorder (WithMasker) MUST be preserved end-to-end, and the facade MUST NOT re-expose raw/unmasked payloads on Events(), SSE, or any interface projection.
- FR-019: For nested
call_workflow execution, the facade MUST correlate subworkflow sessions via the transcript's existing ParentRunID/ChildRunID rather than inventing a parallel correlation scheme; the canonical Resolver (FR-002) and subworkflow run correlation MUST share one identity model.
Non-Functional Requirements
- NFR-001: Session and adapter packages MUST achieve >85% test coverage and pass
make test-race with goleak deterministic verification.
- NFR-002: Migration MUST proceed in coexistence — business services remain intact and interfaces migrate one at a time without breaking the others.
- NFR-003: Resolver MUST round-trip all 4 wire conventions (CLI, HTTP, ACP, MCP) through
wire → canonical → wire without loss.
- NFR-004: Event projection latency from
transcript.ExchangeEvent to consumer-visible facade.Event MUST not introduce additional buffering beyond the bounded replay buffer.
- NFR-005: Error mapping MUST be exhaustive — tests MUST fail if a
StructuredError variant is unmapped.
- NFR-006: Agent uniformity — a contract test MUST assert that the same scripted execution produces an identical
facade.Event sequence regardless of agent provider (Claude/Gemini/Codex/Copilot/OpenAI-compatible), proving friction between agents and the core is collapsed to F106's normalization boundary.
- NFR-007: Defensive projection — the adapter and session MUST never panic on malformed, unknown, or out-of-order input (unknown
EventType/BlockType, Respond after close, Seq gaps, slow consumers). Every such case has a defined, tested fail-closed behavior; make test-race + goleak MUST show zero leaks under these fault injections.
- NFR-008: The
WorkflowFacade/RunSession port and the EventKind mapping table MUST be covered by contract tests (recorder_contract_test-style) so any provider or interface added later is forced to satisfy the same contract rather than widening it.
Success Criteria
- SC-001: All 4 interfaces (CLI, TUI, HTTP, ACP) consume
RunSession.Events() exclusively for runtime events; the adapter is the only subscriber of the F106 Recorder stream, and zero direct event-source access (Recorder.Subscribe() or EventPublisher) remains outside the adapter.
- SC-002: The shared conformance suite passes for CLI stdout, ACP
session/update, SSE frames, and TUI tea.Msg against a single scripted facadetest sequence.
- SC-003: Resolver code paths (
pack_resolver.go, command.go:resolvePackWorkflow, subworkflow_executor.go:SplitCallWorkflowName) are removed; one Resolver remains.
- SC-004: Session and adapter packages achieve >85% line coverage and zero
goleak-detected goroutine leaks under make test-race.
- SC-005: TUI 200ms polling loop is removed; HTTP
eventRegistry parallel path is removed; CLI JSONStore direct access from status and resume-list is removed; the interface-specific ACPInputReader and TUIInputReader are removed in favor of the single EventInputRequired/Respond() protocol.
- SC-006: A single mapping table converts
StructuredError to ErrorCode; exhaustive-case test fails on any unmapped variant.
- SC-007: A single exhaustive
EventType → EventKind table covers all 10 F106 event types; the test fails if a new transcript.EventType or ContentBlock.BlockType is unmapped, and an unknown variant is proven to fail closed (fallback kind + projection error, no panic, no silent drop).
- SC-008: The agent-uniformity contract test passes — one scripted execution yields a byte-identical
facade.Event sequence across all supported providers (Claude/Gemini/Codex/Copilot/OpenAI-compatible), with no provider branch present in the adapter.
Key Entities
| Entity |
Description |
Key Attributes |
WorkflowFacade |
Application port consumed by all interfaces |
List, Validate, Status, History, Run, Resume |
RunSession |
Bidirectional session for one workflow execution |
ID, Events(), Respond(), Err(), Close(), replay buffer indexed by Seq |
RunRequest |
Neutral DTO carrying workflow identifier + inputs |
identifier (canonical pack/workflow), inputs map, options |
Event |
Neutral runtime event projected from transcript.ExchangeEvent |
Seq (reused from ExchangeEvent.Seq), Kind (EventKind), payload (provider-agnostic ContentBlock), timestamp |
EventKind mapping |
Exhaustive transcript.EventType → facade.EventKind table |
10 F106 types + EventInputRequired (bridge-synthesized) + terminal; fail-closed on unknown |
InputRequest / InputResponse |
Bidirectional input collection DTOs |
prompt, schema, response value |
RunResult / RunStatus |
Terminal and intermediate execution state |
status, outputs, error |
WorkflowSummary |
List/discovery DTO |
identifier, name, description, version |
ValidationReport |
Validate result generalizing 404/422 |
findings, severity, identifier resolution status |
HistoryFilter / RunRecord |
History query and result DTOs |
filter criteria (resumable, range), record fields |
Resolver |
Canonical pack/workflow parser + loader |
parse, manifest validation, load via PackDiscoverer/repo |
Adapter |
Implementation composing services + resolver |
sole subscriber of Recorder.Subscribe(); owns 4 bridges (EventPublisher, OutputWriters, DisplayRenderer, UserInputReader); provider-agnostic event projector |
ErrorCode |
Enum bridging StructuredError to interface-specific codes |
one mapping table, pure translators per interface |
Assumptions
- F106 canonical transcript is in place and is the authoritative event source. Concretely, the adapter consumes the F106
Recorder port — Record, Subscribe() <-chan transcript.ExchangeEvent, idempotent Close — where ExchangeEvent already carries Seq, RunID, ParentRunID, ChildRunID, a 10-value EventType, and ContentBlock-normalized payloads. Secret masking is applied at record time via WithMasker. The facade reuses these (Seq, ParentRunID/ChildRunID, masking, normalization) rather than reimplementing them.
- The transcript emits no
input.required and no distinct workflow_failed type (only run.completed with a generic payload); therefore input-required and terminal-failure events are NOT transcript-sourced and are produced by the UserInputReader bridge and the synchronous StructuredError/Err() path respectively (FR-005).
- F105 ACP migration to
coder/acp-go-sdk exposes toSDKUpdate, reusable for facade.Event → session/update projection.
- Existing application services (
WorkflowService, ExecutionService, HistoryService) expose the methods the adapter delegates to; no business rewrite is needed.
- The 4 wire conventions (CLI identity, HTTP
recomposeIdentifier, ACP :↔/, MCP _↔/) are stable enough to be encoded as edge adapters without further negotiation.
- Coexistence-style migration is acceptable — each interface migrates independently while the others continue to work against pre-facade code paths.
- The bounded replay buffer size is configurable and the default is sufficient for typical SSE replay-from-
Seq reconnect windows.
Metadata
- Status: backlog
- Version: v0.11.0
- Priority: high
- Estimation: XL
Dependencies
- Blocked by: F106 (event source); benefits from F105 (
toSDKUpdate)
- Unblocks: F108 (Axis A surfaces honest isolation validation in
ValidateWorkflow)
Clarifications
Resolved against the F106 implementation as merged (PR #370):
- Event source: the adapter subscribes to the F106
Recorder stream (Recorder.Subscribe() <-chan transcript.ExchangeEvent), the Seq-indexed pub/sub F106 actually shipped — not the legacy EventPublisher/pluginmodel.DomainEvent bus, which is a distinct port. Earlier "sole consumer of EventPublisher" wording is superseded by FR-004/FR-005/SC-001.
Seq ownership: facade.Event.Seq reuses transcript.ExchangeEvent.Seq; the facade does not mint its own sequence, keeping SSE replay-from-Seq coherent with the on-disk transcript (FR-006).
- Event stream is a 3-way merge: transcript projection + bridge-synthesized
EventInputRequired + synchronous terminal outcome — because F106 emits neither input.required nor a dedicated workflow_failed type (FR-005).
- Type name: the canonical type is
transcript.ExchangeEvent (the PR summary's "Event envelope" is prose); the spec's references are correct.
- Provider uniformity is the goal: F106 already normalizes Claude/Gemini/Codex/Copilot/OpenAI-compatible output into
ContentBlock; F107 forbids re-introducing provider branching past that boundary (FR-017, NFR-006) — this is the mechanism by which agent↔core friction is reduced and uniformized.
Notes
- Source spec:
.agent/specs/2026-06-02-unified-core-facade-design.md
- Research traceability:
research-improvements.md §1 (covers §1.1 → §1.6) — §1.1 resolution → single Resolver; §1.2 entry point → single Run+Resume; §1.3 status/resume → port methods; §1.4 poll vs push → all consume Events(); §1.5 variable validation → uniform Validate/ValidationReport; §1.6 inputs → EventInputRequired+Respond().
- Position: 5/6 in the v0.11.0 sequence (F103 → F104 → F105 → F106 → F107 → F108).
- Test strategy: fake port
facadetest (scriptable session); adapter mapping-table tests (StructuredError → ErrorCode AND EventType → EventKind, both exhaustive + fail-closed on unknown); agent-uniformity contract test (identical facade.Event sequence across Claude/Gemini/Codex/Copilot/OpenAI-compatible — NFR-006); port contract tests (recorder_contract_test-style) so future providers/interfaces satisfy rather than widen the contract; session concurrency with -race + goleak incl. fault injection (unknown event, Respond after close, Seq gaps); resolver 4-convention round-trip + subworkflow ParentRunID/ChildRunID correlation; shared golden conformance suite; per-interface integration fixtures under tests/fixtures/<interface>/.
- Composition, not rewrite: business services intact; facade delegates.
- Gate / DoD:
>85% + test-race on sessions; conformance suite green for all 4 interfaces; business services intact; migration in coexistence, interface by interface.
F107: Single core ↔ interfaces facade
Scope
In Scope
WorkflowFacade) exposing async-firstRun/Resume(returningRunSession) and syncList/Validate/Status/Historypack/workflowresolver consolidating CLI, TUI, HTTP, ACP, and subworkflow name resolutionRunSessionwith bounded replay buffer indexed bySeq, idempotentClose, andRespond()for input collectionWorkflowService,ExecutionService,HistoryService, andResolver; sole subscriber of the F106Recorderexchange stream (Recorder.Subscribe()) and owner of 4 edge bridges (EventPublisher,OutputWriters,DisplayRenderer, bidirectionalUserInputReader)transcript.ExchangeEventonto neutralfacade.Event, reusing theSeqalready carried by the transcriptfacade.Eventschema by reusing F106'sContentBlocknormalization — zero provider- or interface-specific branching past the edge adaptersStructuredError → ErrorCodemapping table and pure per-interface translatorsJSONStoreaccessOut of Scope
Run+Resolverbut deferred)WorkflowService,ExecutionService,HistoryService)ValidateWorkflow(F108 Axis A consumes the surface this feature opens)Deferred
ValidateWorkflowUser Stories
US1: Unified workflow execution entry point across all interfaces (P1 - Must Have)
As a maintainer of AWF integrating a new interface (CLI, TUI, HTTP, ACP),
I want a single
WorkflowFacadeport withRun/Resumereturning a bidirectionalRunSession,So that I implement only the presentation edge and inherit identical resolution, execution, events, and error semantics.
Why this priority: Without a single entry point, every interface re-implements pack/workflow resolution, polling vs push divergence, status access, and error mapping — the divergence the research §1.1–§1.4 calls out as the structural debt this feature must eliminate. P1 because no downstream value (conformance, F108 Axis A) is reachable without it.
Acceptance Scenarios:
pack/workflowexists in the repository, When any interface invokesWorkflowFacade.Run(ctx, RunRequest), Then the same canonicalResolverparses the identifier,ExecutionServiceruns the workflow, and aRunSessionexposesEvents()derived from the F106 transcript.RunSessionis active, When the consumer callsClose()multiple times, Then the session terminates exactly once, drains pending events, and subsequent calls are idempotent no-ops.Events()from the sameRunRequest, When the workflow emits a step output, Then all 4 interfaces receive afacade.Eventof identical kind/payload before per-interface presentation.Independent Test: Implement a
facadetestfake port producing a scripted sequence ofEvents; run a CLI integration test that callsRunthenDrain, asserting exit code derived from the terminal event and stdout matching the scripted projection.US2: Bidirectional input collection unifies interactive prompts (P1 - Must Have)
As a workflow author whose step requires user input mid-execution,
I want the facade to emit
EventInputRequiredand acceptRunSession.Respond(InputResponse),So that CLI interactive collection, ACP parking, conversational multi-turn, and missing-input recovery share one bidirectional protocol.
Why this priority: Research §1.6 identifies input collection as the most fragmented surface —
ACPInputReaderandTUIInputReaderare interface-specific implementations of the same protocol. Without unification, the facade cannot replace them. P1 because the conformance suite must verify identical input semantics across interfaces.Acceptance Scenarios:
UserInputReader, Then anEvent{Kind: EventInputRequired, InputRequest}is emitted onEvents()and the executor blocks untilRespondis called.RunSession.Respond(InputResponse), When the response reaches the bridge, Then the blockedUserInputReaderreturns the response value and the workflow resumes.Respondis called, When the executor times out or context cancels, Then the executor receives an error and a terminalEvent{workflow_failed}is emitted withErrorCodemapping the cancellation cause.Independent Test: Drive the
facadetestfake to emitEventInputRequired, callRespondon a separate goroutine, and assert the fake executor observed the response value identically; repeat with aClose()beforeRespondand assert terminal failure event.US3: Canonical resolver replaces 3 divergent implementations (P1 - Must Have)
As a subworkflow author or interface integrator,
I want one
Resolverthat parsespack/workflow, validates the manifest, and loads viaPackDiscoverer.LoadWorkfloworrepo.Load,So that identifier resolution behaves identically whether reached from CLI, TUI, HTTP
recomposeIdentifier, ACP:↔/, orSplitCallWorkflowName.Why this priority: Three divergent resolvers (
pack_resolver.go,command.go:resolvePackWorkflow,subworkflow_executor.go:SplitCallWorkflowName) produce different errors for the same identifier. P1 because every interface migration in US1 depends on a single resolution semantics.Acceptance Scenarios:
core/build, When any of CLI, TUI, HTTP, ACP, or subworkflow execution calls the resolver, Then the sameWorkflowis loaded or the sameErrorCodeis returned.pack.workflow, ACPpack:workflow), When the per-interface adapter transforms it to canonicalpack/workflow, Then the resolver receives the canonical form and round-trips back through the adapter without loss.ErrorCodecorresponding to invalid identifier is returned and mapped to interface-appropriate exit code / HTTP status / RPC code by the pure translator.Independent Test: Table-driven test feeding the 4 wire conventions (CLI, HTTP, ACP, MCP) through their adapters into the canonical resolver and asserting round-trip equality plus identical error codes for malformed inputs. For nested
call_workflow, assert the resolved subworkflow run correlates to its parent via the transcript'sParentRunID/ChildRunID(one identity model, FR-019) rather than a parallel scheme.US4: Status, history, and resume served from the facade (P2 - Should Have)
As a CLI user running
awf statusorawf resume-list,I want the commands to query
WorkflowFacade.Status/Historyinstead ofJSONStoredirectly,So that status access shares the same authorization, identifier resolution, and error mapping as
Run.Why this priority: Research §1.3 identifies status/history living outside the core as a coupling violation. P2 because the divergence is functional but not user-blocking — CLI works today, just inconsistently with HTTP/ACP.
Acceptance Scenarios:
awf status <workflow-id>runs, Then the CLI callsWorkflowFacade.Status(ctx, id)and renders the returnedRunResult/RunStatuswithout touchingJSONStore.awf resume-listruns, Then the CLI callsWorkflowFacade.History(ctx, HistoryFilter{Resumable: true})and renders[]RunRecordidentically to HTTP/runs?resumable=true.awf resume <id>runs, When the facade processes the request, Then resume flows through the sameRun/Resumeport (viaExecutionSetup.Build()) as initial execution.Independent Test: Replace
JSONStorein the test withfacadetest; assertawf statusandawf resume-listproduce identical output to running the equivalent HTTP endpoint against the same fake.US5: Conformance suite proves interface parity (P2 - Should Have)
As a maintainer reviewing a PR that touches any interface,
I want a shared golden conformance suite covering CLI stdout, ACP
session/update, SSE frames, and TUItea.Msg,So that divergence between interfaces breaks CI rather than slipping into production.
Why this priority: Without a conformance suite the facade gains can erode silently as each interface is maintained independently. P2 because it protects the investment but does not deliver the facade itself.
Acceptance Scenarios:
facadetestevent sequence, When the conformance suite projects it through all 4 interface adapters, Then the output matches golden files for CLI stdout, ACP updates, SSE frames, and TUI messages.Independent Test: Run the conformance suite against the
facadetestfake; deliberately mutate one adapter to skip an event kind and verify the suite fails.Edge Cases
Events()slowly and the bounded replay buffer overflows? — The session must apply a documented backpressure or drop policy keyed bySeqso HTTP SSE replay-from-Seq remains coherent.Close()called concurrently from multiple goroutines? — Idempotent close must use async.Once-style guard; subsequent calls return without error and do not panic on closed channels.Respond()is called after the session has terminated? —Respondreturns a non-fatal error indicating the session is closed; no event is emitted, no panic.:for ACP)? — The CLI/canonical form accepts it; only the per-interface adapter is responsible for wire encoding/decoding; the resolver does not enforce wire constraints.StructuredErrornot present in the mapping table? — A defaultErrorCode(ErrInternal) is returned with the original error preserved for logging; the mapping table is exhaustive-checked in tests.transcript.EventTypeorContentBlock.BlockType? — The projection fails closed: a defined fallbackEventKindis emitted with a non-fatal projection error logged; the event is never silently dropped and never panics. New provider divergence is absorbed at the F106 normalizer (FR-017), so the facade contract does not widen.Respond()is called for an input the session never requested, or twice for the same request? — The bridge rejects the unexpected/duplicate response with a non-fatal error; the executor's blocking read is satisfied at most once.Runfor the sameRunRequest? — Each call produces a distinctRunSessionwith its ownID; the facade does not deduplicate; the registry stores sessions by ID.Requirements
Functional Requirements
WorkflowFacadewith synchronousList,Validate,Status,Historyand asynchronousRun,ResumereturningRunSession.Resolverthat parsespack/workflow, validates the manifest, and loads viaPackDiscoverer.LoadWorkfloworrepo.Load, replacingpack_resolver.go,command.go:resolvePackWorkflow, andsubworkflow_executor.go:SplitCallWorkflowName.recomposeIdentifier, ACP:↔/, MCP_↔/) that convert wire identifiers to canonical form before invoking the resolver.AdaptercomposingWorkflowService,ExecutionService,HistoryService, andResolver. The adapter MUST be the sole subscriber of the F106Recorderexchange stream (Recorder.Subscribe() <-chan transcript.ExchangeEvent) and MUST own exactly 4 edge bridges —EventPublisher,OutputWriters,DisplayRenderer, and bidirectionalUserInputReader— each with a stub-acceptable implementation for every interface so no interface is left half-wired.facade.Eventfrom three sources merged into one ordered stream: (1) the primary source —transcript.ExchangeEventfromRecorder.Subscribe(), reusing itsContentBlock-normalized, provider-agnostic payloads; (2)EventInputRequired, synthesized by theUserInputReaderbridge (the transcript does not carry aninput.requiredtype); (3) the terminal outcome (workflow_completed/workflow_failed), derived from the synchronousStructuredError/Err()path rather than parsed from a transcript payload. The merge MUST preserveSeqmonotonicity for the transcript-sourced events.RunSessionwithID,Events,Respond,Err, and idempotentClose, backed by a bounded replay buffer keyed byExchangeEvent.Seq— the transcript's existing sequence number MUST be reused, not regenerated, so HTTP SSE replay-from-Seqstays coherent with the on-disk transcript.Event{Kind: EventInputRequired}(synthesized by theUserInputReaderbridge, not sourced from the transcript) and acceptRunSession.Respond(InputResponse)to unify interactive input collection across CLI, ACP, conversational, and missing-input scenarios. This MUST replace and remove the interface-specificACPInputReaderandTUIInputReader.StructuredError → ErrorCodemapping table and pure per-interface translators (exitCode,httpStatus,rpcCode). Mapping MUST fail closed: an unmappedStructuredErrorresolves toErrInternal(original error preserved for logging), never panics, and is caught by an exhaustive-case test (NFR-005).Validateresults asValidationReport, generalizing the 404-vs-422 distinction across interfaces.run,resume,status,resume-listthrough the facade with identical resolution and error semantics as HTTP and ACP.for e := range sess.Events().Seq+/respond+ terminal event, removing the paralleleventRegistryand ad-hoc in-memory state.facade.Event → session/update, reusing F105'stoSDKUpdate.JSONStoreaccess from CLIstatusandresume-list, routing throughExecutionService.ListResumable()via the facade.registrykeyed byIDfor HTTP/cloud consumers and aDrain(sess)helper for CLI synchronous use.transcript.EventType → facade.EventKindmapping table covering all 10 F106 event types (run.started,run.completed,step.started,step.completed,step.call_workflow.started,step.call_workflow.completed,message.user,message.assistant,tool.call,tool.result). An unknownEventTypeorContentBlock.BlockType(e.g. emitted by a future provider) MUST fail closed — mapped to a defined fallback kind and surfaced as a non-fatal projection error — and MUST NOT panic or be silently dropped.facade.Eventprojection MUST be provider-agnostic: no branch on agent provider (Claude/Gemini/Codex/Copilot/OpenAI-compatible) is permitted past F106'sContentBlocknormalization. Provider divergence MUST be absorbed at the F106 normalizer, not re-introduced in the facade.Recorder(WithMasker) MUST be preserved end-to-end, and the facade MUST NOT re-expose raw/unmasked payloads onEvents(), SSE, or any interface projection.call_workflowexecution, the facade MUST correlate subworkflow sessions via the transcript's existingParentRunID/ChildRunIDrather than inventing a parallel correlation scheme; the canonicalResolver(FR-002) and subworkflow run correlation MUST share one identity model.Non-Functional Requirements
make test-racewithgoleakdeterministic verification.wire → canonical → wirewithout loss.transcript.ExchangeEventto consumer-visiblefacade.EventMUST not introduce additional buffering beyond the bounded replay buffer.StructuredErrorvariant is unmapped.facade.Eventsequence regardless of agent provider (Claude/Gemini/Codex/Copilot/OpenAI-compatible), proving friction between agents and the core is collapsed to F106's normalization boundary.EventType/BlockType,Respondafter close,Seqgaps, slow consumers). Every such case has a defined, tested fail-closed behavior;make test-race+goleakMUST show zero leaks under these fault injections.WorkflowFacade/RunSessionport and theEventKindmapping table MUST be covered by contract tests (recorder_contract_test-style) so any provider or interface added later is forced to satisfy the same contract rather than widening it.Success Criteria
RunSession.Events()exclusively for runtime events; the adapter is the only subscriber of the F106Recorderstream, and zero direct event-source access (Recorder.Subscribe()orEventPublisher) remains outside the adapter.session/update, SSE frames, and TUItea.Msgagainst a single scriptedfacadetestsequence.pack_resolver.go,command.go:resolvePackWorkflow,subworkflow_executor.go:SplitCallWorkflowName) are removed; oneResolverremains.goleak-detected goroutine leaks undermake test-race.eventRegistryparallel path is removed; CLIJSONStoredirect access fromstatusandresume-listis removed; the interface-specificACPInputReaderandTUIInputReaderare removed in favor of the singleEventInputRequired/Respond()protocol.StructuredErrortoErrorCode; exhaustive-case test fails on any unmapped variant.EventType → EventKindtable covers all 10 F106 event types; the test fails if a newtranscript.EventTypeorContentBlock.BlockTypeis unmapped, and an unknown variant is proven to fail closed (fallback kind + projection error, no panic, no silent drop).facade.Eventsequence across all supported providers (Claude/Gemini/Codex/Copilot/OpenAI-compatible), with no provider branch present in the adapter.Key Entities
WorkflowFacadeList,Validate,Status,History,Run,ResumeRunSessionID,Events(),Respond(),Err(),Close(), replay buffer indexed bySeqRunRequestpack/workflow), inputs map, optionsEventtranscript.ExchangeEventSeq(reused fromExchangeEvent.Seq),Kind(EventKind), payload (provider-agnosticContentBlock), timestampEventKindmappingtranscript.EventType → facade.EventKindtableEventInputRequired(bridge-synthesized) + terminal; fail-closed on unknownInputRequest/InputResponseRunResult/RunStatusWorkflowSummaryValidationReportValidateresult generalizing 404/422HistoryFilter/RunRecordResolverpack/workflowparser + loaderPackDiscoverer/repoAdapterRecorder.Subscribe(); owns 4 bridges (EventPublisher,OutputWriters,DisplayRenderer,UserInputReader); provider-agnostic event projectorErrorCodeStructuredErrorto interface-specific codesAssumptions
Recorderport —Record,Subscribe() <-chan transcript.ExchangeEvent, idempotentClose— whereExchangeEventalready carriesSeq,RunID,ParentRunID,ChildRunID, a 10-valueEventType, andContentBlock-normalized payloads. Secret masking is applied at record time viaWithMasker. The facade reuses these (Seq,ParentRunID/ChildRunID, masking, normalization) rather than reimplementing them.input.requiredand no distinctworkflow_failedtype (onlyrun.completedwith a generic payload); therefore input-required and terminal-failure events are NOT transcript-sourced and are produced by theUserInputReaderbridge and the synchronousStructuredError/Err()path respectively (FR-005).coder/acp-go-sdkexposestoSDKUpdate, reusable forfacade.Event → session/updateprojection.WorkflowService,ExecutionService,HistoryService) expose the methods the adapter delegates to; no business rewrite is needed.recomposeIdentifier, ACP:↔/, MCP_↔/) are stable enough to be encoded as edge adapters without further negotiation.Seqreconnect windows.Metadata
Dependencies
toSDKUpdate)ValidateWorkflow)Clarifications
Resolved against the F106 implementation as merged (PR #370):
Recorderstream (Recorder.Subscribe() <-chan transcript.ExchangeEvent), theSeq-indexed pub/sub F106 actually shipped — not the legacyEventPublisher/pluginmodel.DomainEventbus, which is a distinct port. Earlier "sole consumer ofEventPublisher" wording is superseded by FR-004/FR-005/SC-001.Seqownership:facade.Event.Seqreusestranscript.ExchangeEvent.Seq; the facade does not mint its own sequence, keeping SSE replay-from-Seqcoherent with the on-disk transcript (FR-006).EventInputRequired+ synchronous terminal outcome — because F106 emits neitherinput.requirednor a dedicatedworkflow_failedtype (FR-005).transcript.ExchangeEvent(the PR summary's "Event envelope" is prose); the spec's references are correct.ContentBlock; F107 forbids re-introducing provider branching past that boundary (FR-017, NFR-006) — this is the mechanism by which agent↔core friction is reduced and uniformized.Notes
.agent/specs/2026-06-02-unified-core-facade-design.mdresearch-improvements.md§1 (covers §1.1 → §1.6) — §1.1 resolution → singleResolver; §1.2 entry point → singleRun+Resume; §1.3 status/resume → port methods; §1.4 poll vs push → all consumeEvents(); §1.5 variable validation → uniformValidate/ValidationReport; §1.6 inputs →EventInputRequired+Respond().facadetest(scriptable session); adapter mapping-table tests (StructuredError → ErrorCodeANDEventType → EventKind, both exhaustive + fail-closed on unknown); agent-uniformity contract test (identicalfacade.Eventsequence across Claude/Gemini/Codex/Copilot/OpenAI-compatible — NFR-006); port contract tests (recorder_contract_test-style) so future providers/interfaces satisfy rather than widen the contract; session concurrency with-race+goleakincl. fault injection (unknown event,Respondafter close,Seqgaps); resolver 4-convention round-trip + subworkflowParentRunID/ChildRunIDcorrelation; shared golden conformance suite; per-interface integration fixtures undertests/fixtures/<interface>/.>85%+test-raceon sessions; conformance suite green for all 4 interfaces; business services intact; migration in coexistence, interface by interface.