Skip to content

Commit 48b5c96

Browse files
Ship traverse-starter.pipeline showcase across platform clients (#144)
Retarget the app manifest to the multi-capability pipeline workflow and parse/render namespaced validate/process/summarize output on every shell. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 24d2b89 commit 48b5c96

37 files changed

Lines changed: 645 additions & 166 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ gh project item-list 2 --owner traverse-framework --format json --limit 300 \
1414
- **Phase 3 embedded runtime** ([#109](https://github.com/traverse-framework/reference-apps/issues/109)[#118](https://github.com/traverse-framework/reference-apps/issues/118)) — see `docs/embedded-runtime-plan.md`; blocked on a **consumable platform embedder SDK** (Traverse [#553](https://github.com/traverse-framework/Traverse/issues/553) closed via [#578](https://github.com/traverse-framework/Traverse/pull/578) with manifest `execution_mode` only — no web/Swift/etc. embedder package yet). Traverse [#615](https://github.com/traverse-framework/Traverse/pull/615) added wasm32 core build only — not a platform embedder package.
1515
- **doc-approval multi-capability showcase** ([#111](https://github.com/traverse-framework/reference-apps/issues/111), [#112](https://github.com/traverse-framework/reference-apps/issues/112)) — blocked on Traverse [#538](https://github.com/traverse-framework/Traverse/issues/538) / [#555](https://github.com/traverse-framework/Traverse/issues/555) (`extract` / `recommend` agents)
1616

17-
Ready: [#110](https://github.com/traverse-framework/reference-apps/issues/110) traverse-starter.pipeline; [#73](https://github.com/traverse-framework/reference-apps/issues/73) doc-approval-core-rs ([#58](https://github.com/traverse-framework/reference-apps/issues/58)/[#59](https://github.com/traverse-framework/reference-apps/issues/59)/[#72](https://github.com/traverse-framework/reference-apps/issues/72) Done).
17+
Ready: (none — shared packages shipped; [#110](https://github.com/traverse-framework/reference-apps/issues/110) pipeline showcase in progress).
1818

1919
Update this section when a PR changes platform status (see PR template checklist).
2020

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,6 @@ Active blockers on [Project 2](https://github.com/orgs/traverse-framework/projec
173173

174174
- **Phase 3 embedded runtime** ([#109](https://github.com/traverse-framework/reference-apps/issues/109)[#118](https://github.com/traverse-framework/reference-apps/issues/118)) — all platform clients must bundle the WASM runtime host; blocked on a **consumable platform embedder SDK** (Traverse [#553](https://github.com/traverse-framework/Traverse/issues/553) closed via [#578](https://github.com/traverse-framework/Traverse/pull/578) with manifest `execution_mode` only; [#615](https://github.com/traverse-framework/Traverse/pull/615) is wasm32 core build only). HTTP sidecar is dev-only.
175175
- **doc-approval multi-capability showcase** ([#111](https://github.com/traverse-framework/reference-apps/issues/111), [#112](https://github.com/traverse-framework/reference-apps/issues/112)) — blocked on Traverse [#538](https://github.com/traverse-framework/Traverse/issues/538) / [#555](https://github.com/traverse-framework/Traverse/issues/555) (`extract` / `recommend` agents).
176-
- **traverse-starter.pipeline showcase** ([#110](https://github.com/traverse-framework/reference-apps/issues/110)) — Ready (Traverse [#620](https://github.com/traverse-framework/Traverse/issues/620) closed; pipeline on Traverse `main`).
176+
- **traverse-starter.pipeline showcase** ([#110](https://github.com/traverse-framework/reference-apps/issues/110)) — In Progress (Traverse [#620](https://github.com/traverse-framework/Traverse/issues/620)/[#554](https://github.com/traverse-framework/Traverse/issues/554) closed).
177177

178-
Ready on Project 2: [#110](https://github.com/traverse-framework/reference-apps/issues/110) pipeline showcase ([#58](https://github.com/traverse-framework/reference-apps/issues/58)/[#59](https://github.com/traverse-framework/reference-apps/issues/59)/[#72](https://github.com/traverse-framework/reference-apps/issues/72) Done; [#73](https://github.com/traverse-framework/reference-apps/issues/73) in progress).
178+
Ready on Project 2: shared HTTP/SSE packages Done ([#58](https://github.com/traverse-framework/reference-apps/issues/58)[#73](https://github.com/traverse-framework/reference-apps/issues/73)).

apps/traverse-starter/TraverseCore/Sources/TraverseCore/TraverseOutput.swift

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import Foundation
22

3-
public struct TraverseStarterOutput: Equatable, Sendable, Codable {
3+
public struct ValidateOutput: Equatable, Sendable, Codable {
4+
public let valid: Bool
5+
public let issues: [String]
6+
7+
public init(valid: Bool, issues: [String]) {
8+
self.valid = valid
9+
self.issues = issues
10+
}
11+
}
12+
13+
public struct ProcessOutput: Equatable, Sendable, Codable {
414
public let title: String
515
public let tags: [String]
616
public let noteType: String
@@ -22,6 +32,41 @@ public struct TraverseStarterOutput: Equatable, Sendable, Codable {
2232
}
2333
}
2434

35+
public struct SummarizeOutput: Equatable, Sendable, Codable {
36+
public let summary: String
37+
public let wordCount: Int
38+
39+
public init(summary: String, wordCount: Int) {
40+
self.summary = summary
41+
self.wordCount = wordCount
42+
}
43+
}
44+
45+
/// Combined pipeline final output (validate → process → summarize).
46+
public struct TraverseStarterOutput: Equatable, Sendable, Codable {
47+
public let validate: ValidateOutput
48+
public let process: ProcessOutput
49+
public let summarize: SummarizeOutput
50+
51+
public init(validate: ValidateOutput, process: ProcessOutput, summarize: SummarizeOutput) {
52+
self.validate = validate
53+
self.process = process
54+
self.summarize = summarize
55+
}
56+
57+
public static let empty = TraverseStarterOutput(
58+
validate: ValidateOutput(valid: false, issues: []),
59+
process: ProcessOutput(
60+
title: "",
61+
tags: [],
62+
noteType: "",
63+
suggestedNextAction: "",
64+
status: ""
65+
),
66+
summarize: SummarizeOutput(summary: "", wordCount: 0)
67+
)
68+
}
69+
2570
public struct TraceEvent: Equatable, Sendable, Codable {
2671
public let event_type: String
2772
public let timestamp: String
@@ -136,20 +181,12 @@ public enum JSONValue: Equatable, Sendable, Codable {
136181
public enum TraverseOutputParser {
137182
public static func parse(_ raw: Any?) -> TraverseStarterOutput? {
138183
guard let dict = raw as? [String: Any],
139-
let title = dict["title"] as? String,
140-
let tags = dict["tags"] as? [String],
141-
let noteType = dict["noteType"] as? String,
142-
let suggestedNextAction = dict["suggestedNextAction"] as? String,
143-
let status = dict["status"] as? String else {
184+
let validate = parseValidate(dict["validate"]),
185+
let process = parseProcess(dict["process"]),
186+
let summarize = parseSummarize(dict["summarize"]) else {
144187
return nil
145188
}
146-
return TraverseStarterOutput(
147-
title: title,
148-
tags: tags,
149-
noteType: noteType,
150-
suggestedNextAction: suggestedNextAction,
151-
status: status
152-
)
189+
return TraverseStarterOutput(validate: validate, process: process, summarize: summarize)
153190
}
154191

155192
public static func parseEventPayload(_ raw: Any?) -> AppStateEventPayload? {
@@ -173,4 +210,47 @@ public enum TraverseOutputParser {
173210
errorMessage: errorMessage
174211
)
175212
}
213+
214+
private static func parseValidate(_ raw: Any?) -> ValidateOutput? {
215+
guard let dict = raw as? [String: Any],
216+
let valid = dict["valid"] as? Bool,
217+
let issues = dict["issues"] as? [String] else {
218+
return nil
219+
}
220+
return ValidateOutput(valid: valid, issues: issues)
221+
}
222+
223+
private static func parseProcess(_ raw: Any?) -> ProcessOutput? {
224+
guard let dict = raw as? [String: Any],
225+
let title = dict["title"] as? String,
226+
let tags = dict["tags"] as? [String],
227+
let noteType = dict["noteType"] as? String,
228+
let suggestedNextAction = dict["suggestedNextAction"] as? String,
229+
let status = dict["status"] as? String else {
230+
return nil
231+
}
232+
return ProcessOutput(
233+
title: title,
234+
tags: tags,
235+
noteType: noteType,
236+
suggestedNextAction: suggestedNextAction,
237+
status: status
238+
)
239+
}
240+
241+
private static func parseSummarize(_ raw: Any?) -> SummarizeOutput? {
242+
guard let dict = raw as? [String: Any],
243+
let summary = dict["summary"] as? String,
244+
let wordCount = parseWordCount(dict["wordCount"]) else {
245+
return nil
246+
}
247+
return SummarizeOutput(summary: summary, wordCount: wordCount)
248+
}
249+
250+
private static func parseWordCount(_ raw: Any?) -> Int? {
251+
if let value = raw as? Int { return value }
252+
if let value = raw as? Double { return Int(value) }
253+
if let value = raw as? String, let parsed = Int(value) { return parsed }
254+
return nil
255+
}
176256
}

apps/traverse-starter/TraverseCore/Tests/TraverseCoreTests/TraverseCoreTests.swift

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,33 @@ final class TraverseClientTests: XCTestCase {
8181

8282
final class TraverseOutputTests: XCTestCase {
8383
func testParseOutput() {
84+
let raw: [String: Any] = [
85+
"validate": ["valid": true, "issues": [] as [String]],
86+
"process": [
87+
"title": "T",
88+
"tags": ["a"],
89+
"noteType": "n",
90+
"suggestedNextAction": "x",
91+
"status": "done",
92+
],
93+
"summarize": ["summary": "A short summary", "wordCount": 3],
94+
]
95+
let output = TraverseOutputParser.parse(raw)
96+
XCTAssertEqual(output?.process.title, "T")
97+
XCTAssertEqual(output?.process.tags, ["a"])
98+
XCTAssertEqual(output?.validate.valid, true)
99+
XCTAssertEqual(output?.summarize.wordCount, 3)
100+
}
101+
102+
func testParseRejectsFlatLegacyOutput() {
84103
let raw: [String: Any] = [
85104
"title": "T",
86105
"tags": ["a"],
87106
"noteType": "n",
88107
"suggestedNextAction": "x",
89108
"status": "done",
90109
]
91-
let output = TraverseOutputParser.parse(raw)
92-
XCTAssertEqual(output?.title, "T")
93-
XCTAssertEqual(output?.tags, ["a"])
110+
XCTAssertNil(TraverseOutputParser.parse(raw))
94111
}
95112

96113
func testParseEventPayload() {
@@ -99,16 +116,20 @@ final class TraverseOutputTests: XCTestCase {
99116
"session_id": "sess-1",
100117
"execution_id": "exec-1",
101118
"output": [
102-
"title": "T",
103-
"tags": [] as [String],
104-
"noteType": "n",
105-
"suggestedNextAction": "x",
106-
"status": "done",
119+
"validate": ["valid": true, "issues": [] as [String]],
120+
"process": [
121+
"title": "T",
122+
"tags": [] as [String],
123+
"noteType": "n",
124+
"suggestedNextAction": "x",
125+
"status": "done",
126+
],
127+
"summarize": ["summary": "Summary", "wordCount": 1],
107128
],
108129
]
109130
let payload = TraverseOutputParser.parseEventPayload(raw)
110131
XCTAssertEqual(payload?.state, "results")
111-
XCTAssertEqual(payload?.output?.title, "T")
132+
XCTAssertEqual(payload?.output?.process.title, "T")
112133
}
113134
}
114135

@@ -177,16 +198,20 @@ final class AppStateViewModelTests: XCTestCase {
177198
sessionId: "sess-1",
178199
executionId: "exec-1",
179200
output: TraverseStarterOutput(
180-
title: "T",
181-
tags: [],
182-
noteType: "n",
183-
suggestedNextAction: "x",
184-
status: "done"
201+
validate: ValidateOutput(valid: true, issues: []),
202+
process: ProcessOutput(
203+
title: "T",
204+
tags: [],
205+
noteType: "n",
206+
suggestedNextAction: "x",
207+
status: "done"
208+
),
209+
summarize: SummarizeOutput(summary: "Summary", wordCount: 1)
185210
)
186211
)
187212
)
188213
XCTAssertEqual(vm.currentState, "results")
189-
XCTAssertEqual(vm.output?.title, "T")
214+
XCTAssertEqual(vm.output?.process.title, "T")
190215
XCTAssertEqual(vm.executionId, "exec-1")
191216
}
192217

apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ExecutionViewModel.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,7 @@ class ExecutionViewModel(
106106
} catch (_: Exception) {
107107
emptyList()
108108
}
109-
val output = result.output ?: TraverseStarterOutput(
110-
title = "",
111-
tags = emptyList(),
112-
noteType = "",
113-
suggestedNextAction = "",
114-
status = "",
115-
)
109+
val output = result.output ?: TraverseStarterOutput.EMPTY
116110
_uiState.update { it.copy(phase = ExecutionPhase.Succeeded(output, trace)) }
117111
return
118112
}

apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/Models.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,48 @@ package com.traverseframework.starter
33
import kotlinx.serialization.json.JsonElement
44

55
@kotlinx.serialization.Serializable
6-
data class TraverseStarterOutput(
6+
data class ValidateOutput(
7+
val valid: Boolean,
8+
val issues: List<String>,
9+
)
10+
11+
@kotlinx.serialization.Serializable
12+
data class ProcessOutput(
713
val title: String,
814
val tags: List<String>,
915
val noteType: String,
1016
val suggestedNextAction: String,
1117
val status: String,
1218
)
1319

20+
@kotlinx.serialization.Serializable
21+
data class SummarizeOutput(
22+
val summary: String,
23+
val wordCount: Int,
24+
)
25+
26+
/** Combined pipeline final output (validate → process → summarize). */
27+
@kotlinx.serialization.Serializable
28+
data class TraverseStarterOutput(
29+
val validate: ValidateOutput,
30+
val process: ProcessOutput,
31+
val summarize: SummarizeOutput,
32+
) {
33+
companion object {
34+
val EMPTY = TraverseStarterOutput(
35+
validate = ValidateOutput(valid = false, issues = emptyList()),
36+
process = ProcessOutput(
37+
title = "",
38+
tags = emptyList(),
39+
noteType = "",
40+
suggestedNextAction = "",
41+
status = "",
42+
),
43+
summarize = SummarizeOutput(summary = "", wordCount = 0),
44+
)
45+
}
46+
}
47+
1448
@kotlinx.serialization.Serializable
1549
data class TraceEvent(
1650
val event_type: String,
@@ -39,7 +73,7 @@ data class ExecutionPollResult(
3973
)
4074

4175
object AppConstants {
42-
const val CAPABILITY_ID = "traverse-starter.process"
76+
const val CAPABILITY_ID = "traverse-starter.pipeline"
4377
const val DEFAULT_BASE_URL = "http://10.0.2.2:8787"
4478
const val DEFAULT_WORKSPACE = "local-default"
4579
const val NOTE_MAX_LENGTH = 2000

apps/traverse-starter/android-compose/app/src/main/java/com/traverseframework/starter/ui/MainScreen.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,15 @@ private fun OutputCard(
168168

169169
@Composable
170170
private fun OutputFields(output: TraverseStarterOutput) {
171-
Field("Title", output.title)
172-
Field("Tags", output.tags.joinToString(", "))
173-
Field("Note type", output.noteType)
174-
Field("Next action", output.suggestedNextAction)
175-
Field("Status", output.status)
171+
Field("Valid", if (output.validate.valid) "yes" else "no")
172+
Field("Issues", output.validate.issues.joinToString(", ").ifEmpty { "None" })
173+
Field("Title", output.process.title)
174+
Field("Note type", output.process.noteType)
175+
Field("Status", output.process.status)
176+
Field("Next action", output.process.suggestedNextAction)
177+
Field("Tags", output.process.tags.joinToString(", "))
178+
Field("Summary", output.summarize.summary)
179+
Field("Word count", output.summarize.wordCount.toString())
176180
}
177181

178182
@Composable

apps/traverse-starter/android-compose/app/src/test/java/com/traverseframework/starter/TraverseClientTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class TraverseClientTest {
4444
val engine = MockEngine {
4545
respond(
4646
content = """
47-
{"status":"succeeded","output":{"title":"T","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"}}
47+
{"status":"succeeded","output":{"validate":{"valid":true,"issues":[]},"process":{"title":"T","tags":["a"],"noteType":"n","suggestedNextAction":"x","status":"done"},"summarize":{"summary":"Summary","wordCount":1}}}
4848
""".trimIndent(),
4949
status = HttpStatusCode.OK,
5050
headers = headersOf(HttpHeaders.ContentType, "application/json"),
@@ -53,6 +53,8 @@ class TraverseClientTest {
5353
val client = TraverseClient(engine)
5454
val result = client.pollExecution("http://10.0.2.2:8787", "local-default", "exec_abc")
5555
assertEquals("succeeded", result.status)
56-
assertEquals("T", result.output?.title)
56+
assertEquals("T", result.output?.process?.title)
57+
assertEquals(true, result.output?.validate?.valid)
58+
assertEquals(1, result.output?.summarize?.wordCount)
5759
}
5860
}

apps/traverse-starter/cli-rust/src/commands/run.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,7 @@ pub fn execute(base_url: &str, workspace: &str, note: &str, json: bool) -> i32 {
6363
.execution_id
6464
.or(accepted.execution_id.clone())
6565
.unwrap_or_default();
66-
let output = event.output.unwrap_or(TraverseStarterOutput {
67-
title: String::new(),
68-
tags: vec![],
69-
note_type: String::new(),
70-
suggested_next_action: String::new(),
71-
status: String::new(),
72-
});
66+
let output = event.output.unwrap_or_else(TraverseStarterOutput::empty);
7367
let trace = if execution_id.is_empty() {
7468
Vec::new()
7569
} else {

apps/traverse-starter/cli-rust/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct Cli {
1818

1919
#[derive(Subcommand)]
2020
enum Commands {
21-
/// Execute traverse-starter.process with a note
21+
/// Execute traverse-starter.pipeline with a note
2222
Run {
2323
#[arg(long)]
2424
note: String,

0 commit comments

Comments
 (0)