From 18e405321ec96188e4a34f09724d0e52a4de47c0 Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Tue, 7 Jul 2026 18:28:11 -0600 Subject: [PATCH 1/2] feat: add meeting notes reference capability --- apps/meeting-notes/app.manifest.json | 117 ++++++++++ .../process/component.manifest.json | 28 +++ .../capabilities/process/contract.json | 168 ++++++++++++++ crates/traverse-cli/src/main.rs | 213 ++++++++++++++++++ .../artifacts/process-agent.wasm | Bin 0 -> 36 bytes .../process-agent/build-fixture.sh | 13 ++ .../meeting-notes/process-agent/manifest.json | 35 +++ .../meeting-notes/process-agent/src/agent.rs | 3 + .../runtime-requests/process.json | 21 ++ scripts/ci/meeting_notes_example_smoke.sh | 70 ++++++ .../meeting-notes/process/workflow.json | 84 +++++++ 11 files changed, 752 insertions(+) create mode 100644 apps/meeting-notes/app.manifest.json create mode 100644 apps/meeting-notes/components/process/component.manifest.json create mode 100644 contracts/examples/meeting-notes/capabilities/process/contract.json create mode 100644 examples/meeting-notes/process-agent/artifacts/process-agent.wasm create mode 100755 examples/meeting-notes/process-agent/build-fixture.sh create mode 100644 examples/meeting-notes/process-agent/manifest.json create mode 100644 examples/meeting-notes/process-agent/src/agent.rs create mode 100644 examples/meeting-notes/runtime-requests/process.json create mode 100755 scripts/ci/meeting_notes_example_smoke.sh create mode 100644 workflows/examples/meeting-notes/process/workflow.json diff --git a/apps/meeting-notes/app.manifest.json b/apps/meeting-notes/app.manifest.json new file mode 100644 index 00000000..9cf8aef7 --- /dev/null +++ b/apps/meeting-notes/app.manifest.json @@ -0,0 +1,117 @@ +{ + "app_id": "meeting-notes", + "version": "1.0.0", + "schema_version": "1.0.0", + "workspace_defaults": { + "workspace_id": "local-default", + "registry_scope": "private" + }, + "components": [ + { + "component_id": "meeting-notes.process-component", + "version": "1.0.0", + "digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "manifest_path": "components/process/component.manifest.json" + } + ], + "workflows": [ + { + "workflow_id": "meeting-notes.process", + "workflow_version": "1.0.0", + "path": "../../workflows/examples/meeting-notes/process/workflow.json" + } + ], + "model_dependencies": [], + "config_schema": { + "type": "object", + "required": [ + "workspace_id" + ], + "properties": { + "workspace_id": { + "type": "string" + }, + "processing_mode": { + "type": "string", + "enum": [ + "deterministic" + ] + } + }, + "additionalProperties": false + }, + "default_config": { + "workspace_id": "local-default", + "processing_mode": "deterministic" + }, + "placement_policy": { + "preferred_targets": [ + "local" + ], + "allow_fallback": false + }, + "public_surfaces": [ + "cli", + "http_json" + ], + "state_machine": { + "initial_state": "idle", + "list_context_fields": [ + "output.action_items", + "output.decisions", + "output.follow_ups", + "output.summary" + ], + "states": [ + { + "id": "idle", + "transitions": [ + { + "on": "submit", + "to": "processing" + } + ] + }, + { + "id": "processing", + "invoke": { + "capability_id": "meeting-notes.process", + "input_from": "command.payload" + }, + "transitions": [ + { + "on": "capability_succeeded", + "to": "results" + }, + { + "on": "capability_failed", + "to": "error" + } + ] + }, + { + "id": "results", + "transitions": [ + { + "on": "reset", + "to": "idle" + } + ] + }, + { + "id": "error", + "transitions": [ + { + "on": "retry", + "to": "processing", + "with_last_payload": true + }, + { + "on": "reset", + "to": "idle" + } + ] + } + ] + } +} diff --git a/apps/meeting-notes/components/process/component.manifest.json b/apps/meeting-notes/components/process/component.manifest.json new file mode 100644 index 00000000..8bda94fb --- /dev/null +++ b/apps/meeting-notes/components/process/component.manifest.json @@ -0,0 +1,28 @@ +{ + "component_id": "meeting-notes.process-component", + "version": "1.0.0", + "schema_version": "1.0.0", + "capability_id": "meeting-notes.process", + "capability_version": "1.0.0", + "contract_path": "../../../../contracts/examples/meeting-notes/capabilities/process/contract.json", + "wasm_binary_path": "../../../../examples/meeting-notes/process-agent/artifacts/process-agent.wasm", + "wasm_digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99", + "runtime_constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + }, + "permitted_targets": [ + "local", + "device" + ], + "dependencies": [], + "connector_requirements": [], + "validation_evidence": [ + { + "evidence_type": "checked_in_fixture", + "status": "passed", + "produced_by": "meeting_notes_example_smoke" + } + ] +} diff --git a/contracts/examples/meeting-notes/capabilities/process/contract.json b/contracts/examples/meeting-notes/capabilities/process/contract.json new file mode 100644 index 00000000..ddf1a986 --- /dev/null +++ b/contracts/examples/meeting-notes/capabilities/process/contract.json @@ -0,0 +1,168 @@ +{ + "kind": "capability_contract", + "schema_version": "1.0.0", + "id": "meeting-notes.process", + "namespace": "meeting-notes", + "name": "process", + "version": "1.0.0", + "lifecycle": "active", + "owner": { + "team": "traverse-core", + "contact": "enrico.piovesan10@gmail.com" + }, + "summary": "Extract deterministic structured meeting notes from one transcript.", + "description": "Reference app capability for meeting notes that extracts action items, decisions, follow-ups, and summary fields without model inference.", + "inputs": { + "schema": { + "type": "object", + "required": [ + "transcript" + ], + "properties": { + "transcript": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "outputs": { + "schema": { + "type": "object", + "required": [ + "action_items", + "decisions", + "follow_ups", + "summary" + ], + "properties": { + "action_items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "task", + "owner", + "due" + ], + "properties": { + "task": { + "type": "string" + }, + "owner": { + "type": [ + "string", + "null" + ] + }, + "due": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "decisions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "text", + "made_by" + ], + "properties": { + "text": { + "type": "string" + }, + "made_by": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "follow_ups": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "preconditions": [ + { + "id": "transcript-provided", + "description": "The caller provides a transcript string. Empty transcripts are accepted and produce empty collections." + } + ], + "postconditions": [ + { + "id": "structured-meeting-notes-produced", + "description": "The capability returns deterministic structured meeting-note fields for the provided transcript." + } + ], + "side_effects": [ + { + "kind": "memory_only", + "description": "Produces deterministic meeting-note metadata from the provided input." + } + ], + "emits": [], + "consumes": [], + "permissions": [ + { + "id": "meeting-notes.process" + } + ], + "execution": { + "binary_format": "wasm", + "entrypoint": { + "kind": "wasi-command", + "command": "run" + }, + "preferred_targets": [ + "local" + ], + "constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + } + }, + "policies": [ + { + "id": "manual-approval-required" + } + ], + "dependencies": [], + "provenance": { + "source": "greenfield", + "author": "enricopiovesan", + "created_at": "2026-07-07T00:00:00Z", + "spec_ref": "044-application-bundle-manifest@1.0.0", + "adr_refs": [ + "0001-rust-wasm-foundation" + ], + "exception_refs": [] + }, + "evidence": [], + "service_type": "stateless", + "permitted_targets": [ + "local", + "cloud", + "edge", + "device" + ], + "artifact_type": "native" +} diff --git a/crates/traverse-cli/src/main.rs b/crates/traverse-cli/src/main.rs index e5f33c1f..02ac9377 100644 --- a/crates/traverse-cli/src/main.rs +++ b/crates/traverse-cli/src/main.rs @@ -3854,6 +3854,20 @@ fn render_agent_execution_summary( lines.push(format!("starter_status: {status}")); } } + "meeting-notes.process" => { + if let Some(summary) = output.get("summary").and_then(Value::as_str) { + lines.push(format!("summary: {summary}")); + } + if let Some(action_items) = output.get("action_items").and_then(Value::as_array) { + lines.push(format!("action_items: {}", action_items.len())); + } + if let Some(decisions) = output.get("decisions").and_then(Value::as_array) { + lines.push(format!("decisions: {}", decisions.len())); + } + if let Some(follow_ups) = output.get("follow_ups").and_then(Value::as_array) { + lines.push(format!("follow_ups: {}", follow_ups.len())); + } + } _ => {} } @@ -3986,6 +4000,7 @@ impl LocalExecutor for ExpeditionExampleExecutor { } "expedition.planning.validate-team-readiness" => execute_validate_team_readiness(input), "traverse-starter.process" => execute_traverse_starter_process(input), + "meeting-notes.process" => execute_meeting_notes_process(input), "expedition.planning.assemble-expedition-plan" => { execute_assemble_expedition_plan(input) } @@ -4008,6 +4023,7 @@ impl LocalExecutor for AgentPackageExampleExecutor { match capability.contract.id.as_str() { "hello.world.say-hello" => execute_hello_world(input), "traverse-starter.process" => execute_traverse_starter_process(input), + "meeting-notes.process" => execute_meeting_notes_process(input), "expedition.planning.interpret-expedition-intent" => { execute_interpret_expedition_intent(input) } @@ -4632,6 +4648,141 @@ fn execute_traverse_starter_process(input: &Value) -> Result Result { + let map = input_object(input)?; + let transcript = required_string(map, "transcript")?; + let trimmed = transcript.trim(); + let mut action_items = Vec::new(); + let mut decisions = Vec::new(); + let mut follow_ups = Vec::new(); + + for line in trimmed + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + let lower = line.to_ascii_lowercase(); + if lower.contains("action:") + || lower.contains("todo:") + || lower.contains("will do") + || lower.contains("to do") + || line.contains('@') + { + action_items.push(serde_json::json!({ + "task": clean_meeting_marker(line), + "owner": meeting_owner(line), + "due": meeting_due(line) + })); + } + if lower.contains("decided:") + || lower.contains("agreed:") + || lower.contains("we will") + || lower.contains("resolution:") + { + decisions.push(serde_json::json!({ + "text": clean_meeting_marker(line), + "made_by": meeting_owner(line) + })); + } + if lower.contains("follow up") + || lower.contains("check in") + || lower.contains("revisit") + || lower.contains("next steps") + { + follow_ups.push(Value::String(clean_meeting_marker(line))); + } + } + + Ok(serde_json::json!({ + "action_items": action_items, + "decisions": decisions, + "follow_ups": follow_ups, + "summary": meeting_summary(trimmed) + })) +} + +fn clean_meeting_marker(line: &str) -> String { + let trimmed = line.trim(); + for marker in ["action:", "todo:", "decided:", "agreed:", "resolution:"] { + if trimmed + .get(..marker.len()) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(marker)) + { + return trimmed[marker.len()..].trim().to_string(); + } + } + trimmed.to_string() +} + +fn meeting_owner(line: &str) -> Value { + if let Some(owner) = line + .split_whitespace() + .find_map(|word| word.strip_prefix('@')) + .map(|word| word.trim_matches(|ch: char| !ch.is_ascii_alphanumeric())) + .filter(|word| !word.is_empty()) + { + return Value::String(owner.to_string()); + } + + let words = line.split_whitespace().collect::>(); + for pair in words.windows(2) { + if pair[0].eq_ignore_ascii_case("by") { + let owner = pair[1].trim_matches(|ch: char| !ch.is_ascii_alphanumeric()); + if !owner.is_empty() { + return Value::String(owner.to_string()); + } + } + } + Value::Null +} + +fn meeting_due(line: &str) -> Value { + let words = line.split_whitespace().collect::>(); + for pair in words.windows(2) { + if pair[0].eq_ignore_ascii_case("by") + || pair[0].eq_ignore_ascii_case("before") + || pair[0].eq_ignore_ascii_case("due") + { + let due = pair[1].trim_matches(|ch: char| ch == '.' || ch == ',' || ch == ';'); + if looks_like_due_token(due) { + return Value::String(due.to_string()); + } + } + } + Value::Null +} + +fn looks_like_due_token(token: &str) -> bool { + if token.is_empty() { + return false; + } + let lower = token.to_ascii_lowercase(); + matches!( + lower.as_str(), + "today" + | "tomorrow" + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday" + ) || token.chars().any(|ch| ch.is_ascii_digit()) +} + +fn meeting_summary(transcript: &str) -> String { + if transcript.is_empty() { + return String::new(); + } + let first_paragraph = transcript + .split("\n\n") + .find(|paragraph| !paragraph.trim().is_empty()) + .unwrap_or(transcript) + .trim(); + first_paragraph.chars().take(280).collect() +} + fn derive_starter_tags(note: &str) -> Vec { let mut tags = Vec::new(); for word in note.split(|ch: char| !ch.is_ascii_alphanumeric()) { @@ -6253,6 +6404,55 @@ mod tests { assert!(output.contains("starter_status: complete")); } + #[test] + fn execute_agent_runs_meeting_notes_process_request() { + let fixture = create_meeting_notes_agent_fixture(); + let request_path = repo_root().join("examples/meeting-notes/runtime-requests/process.json"); + + let output = execute_agent(&fixture.manifest_path, &request_path) + .expect("meeting-notes agent execution should succeed"); + + assert!(output.contains("package_id: meeting-notes.process-agent")); + assert!(output.contains("capability_id: meeting-notes.process")); + assert!(output.contains("status: completed")); + assert!(output.contains("summary: Kickoff notes for Traverse reference app.")); + assert!(output.contains("action_items: 2")); + assert!(output.contains("decisions: 1")); + assert!(output.contains("follow_ups: 1")); + } + + #[test] + fn meeting_notes_process_is_deterministic_and_handles_empty_transcript() { + let input = serde_json::json!({ + "transcript": "Kickoff notes.\nAction: @Mira draft parser by Friday\nDecided: we will ship deterministic extraction\nFollow up next steps with app team" + }); + + let first = crate::execute_meeting_notes_process(&input).expect("first run should succeed"); + let second = + crate::execute_meeting_notes_process(&input).expect("second run should succeed"); + assert_eq!(first, second); + assert_eq!(first["action_items"][0]["owner"], "Mira"); + assert_eq!(first["action_items"][0]["due"], "Friday"); + assert_eq!( + first["decisions"][0]["text"], + "we will ship deterministic extraction" + ); + + let empty = crate::execute_meeting_notes_process(&serde_json::json!({"transcript": ""})) + .expect("empty transcript should succeed"); + assert_eq!(empty["summary"], ""); + assert_eq!(empty["action_items"].as_array().map(Vec::len), Some(0)); + assert_eq!(empty["decisions"].as_array().map(Vec::len), Some(0)); + assert_eq!(empty["follow_ups"].as_array().map(Vec::len), Some(0)); + + let owner_only = crate::execute_meeting_notes_process(&serde_json::json!({ + "transcript": "TODO: by Luca add HTTP validation before demo" + })) + .expect("owner-only by marker should succeed"); + assert_eq!(owner_only["action_items"][0]["owner"], "Luca"); + assert_eq!(owner_only["action_items"][0]["due"], Value::Null); + } + #[test] fn execute_expedition_writes_trace_artifact_when_requested() { let request_path = @@ -6750,6 +6950,19 @@ mod tests { }) } + fn create_meeting_notes_agent_fixture() -> AgentFixture { + create_agent_package_fixture(&AgentPackageFixtureSpec { + package_id: "meeting-notes.process-agent", + capability_id: "meeting-notes.process", + binary_name: "process-agent.wasm", + summary: "Governed WASM agent package for the meeting-notes reference app.", + contract_path: "contracts/examples/meeting-notes/capabilities/process/contract.json", + model_interface: "meeting-notes-deterministic-v1", + model_purpose: "Extract meeting summary, action items, decisions, and follow-ups without model inference.", + workflow_id: "meeting-notes.process", + }) + } + struct AgentPackageFixtureSpec<'a> { package_id: &'a str, capability_id: &'a str, diff --git a/examples/meeting-notes/process-agent/artifacts/process-agent.wasm b/examples/meeting-notes/process-agent/artifacts/process-agent.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ca050857cb7401d40a94e7f18722fd7f75c5d365 GIT binary patch literal 36 rcmZQbEY4+QU|?WmVN76PU}j=uVCQ0Fi!UxoEGl7O;9_B9V&Db "$artifact_path" + +printf 'built %s\n' "$artifact_path" diff --git a/examples/meeting-notes/process-agent/manifest.json b/examples/meeting-notes/process-agent/manifest.json new file mode 100644 index 00000000..3087658e --- /dev/null +++ b/examples/meeting-notes/process-agent/manifest.json @@ -0,0 +1,35 @@ +{ + "kind": "agent_package", + "schema_version": "1.0.0", + "package_id": "meeting-notes.process-agent", + "version": "1.0.0", + "summary": "Governed WASM agent package for the meeting-notes reference app.", + "capability_ref": { + "id": "meeting-notes.process", + "version": "1.0.0", + "contract_path": "../../../contracts/examples/meeting-notes/capabilities/process/contract.json" + }, + "workflow_refs": [ + { + "workflow_id": "meeting-notes.process", + "workflow_version": "1.0.0" + } + ], + "source": { + "path": "./src/agent.rs", + "language": "rust", + "entry": "run" + }, + "binary": { + "path": "./artifacts/process-agent.wasm", + "format": "wasm", + "expected_digest": "fnv1a64:dffc31d6401c84d6", + "abi_version": "1.0.0" + }, + "constraints": { + "host_api_access": "none", + "network_access": "forbidden", + "filesystem_access": "none" + }, + "model_dependencies": [] +} diff --git a/examples/meeting-notes/process-agent/src/agent.rs b/examples/meeting-notes/process-agent/src/agent.rs new file mode 100644 index 00000000..45395465 --- /dev/null +++ b/examples/meeting-notes/process-agent/src/agent.rs @@ -0,0 +1,3 @@ +pub fn run() -> &'static str { + "meeting-notes.process" +} diff --git a/examples/meeting-notes/runtime-requests/process.json b/examples/meeting-notes/runtime-requests/process.json new file mode 100644 index 00000000..ce4497b1 --- /dev/null +++ b/examples/meeting-notes/runtime-requests/process.json @@ -0,0 +1,21 @@ +{ + "kind": "runtime_request", + "schema_version": "1.0.0", + "request_id": "meeting-notes-process-001", + "intent": { + "capability_id": "meeting-notes.process", + "capability_version": "1.0.0" + }, + "input": { + "transcript": "Kickoff notes for Traverse reference app.\nAction: @Mira draft parser by Friday\nTODO: by Luca add HTTP validation before demo\nDecided: we will ship deterministic extraction first\nFollow up next steps with app team" + }, + "lookup": { + "scope": "prefer_private", + "allow_ambiguity": false + }, + "context": { + "requested_target": "local", + "caller": "meeting-notes-example" + }, + "governing_spec": "006-runtime-request-execution" +} diff --git a/scripts/ci/meeting_notes_example_smoke.sh b/scripts/ci/meeting_notes_example_smoke.sh new file mode 100755 index 00000000..6f5decd1 --- /dev/null +++ b/scripts/ci/meeting_notes_example_smoke.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="${TRAVERSE_REPO_ROOT:-$(pwd)}" +cd "$repo_root" + +agent_manifest="examples/meeting-notes/process-agent/manifest.json" +agent_request="examples/meeting-notes/runtime-requests/process.json" +app_manifest="apps/meeting-notes/app.manifest.json" + +bash examples/meeting-notes/process-agent/build-fixture.sh >/tmp/meeting-notes-build.out + +inspect_output="$(cargo run -q -p traverse-cli -- agent inspect "$agent_manifest")" +printf '%s\n' "$inspect_output" + +grep -q "package_id: meeting-notes.process-agent" <<<"$inspect_output" +grep -q "capability_id: meeting-notes.process" <<<"$inspect_output" +grep -q "workflow_refs: meeting-notes.process@1.0.0" <<<"$inspect_output" + +execute_output="$(cargo run -q -p traverse-cli -- agent execute "$agent_manifest" "$agent_request")" +printf '%s\n' "$execute_output" + +grep -q "status: completed" <<<"$execute_output" +grep -q "capability_id: meeting-notes.process" <<<"$execute_output" +grep -q "summary: Kickoff notes for Traverse reference app." <<<"$execute_output" +grep -q "action_items: 2" <<<"$execute_output" +grep -q "decisions: 1" <<<"$execute_output" +grep -q "follow_ups: 1" <<<"$execute_output" + +validate_output="$(cargo run -q -p traverse-cli -- app validate --manifest "$app_manifest" --json)" +printf '%s\n' "$validate_output" + +grep -q '"status": "validated"' <<<"$validate_output" +grep -q '"app_id": "meeting-notes"' <<<"$validate_output" +grep -q '"component_id": "meeting-notes.process-component"' <<<"$validate_output" +grep -q '"capability_id": "meeting-notes.process"' <<<"$validate_output" + +register_output="$(cargo run -q -p traverse-cli -- app register --manifest "$app_manifest" --workspace local-default --json)" +printf '%s\n' "$register_output" + +grep -Eq '"status": "(registered|already_registered)"' <<<"$register_output" +grep -q '"state_scope": "workspace_persisted"' <<<"$register_output" + +rm -f .traverse/server.json +cargo run -q -p traverse-cli -- serve --port 0 --allow-unauthenticated >/tmp/meeting-notes-serve.out 2>&1 & +server_pid=$! +trap 'kill "$server_pid" 2>/dev/null || true' EXIT + +for _ in 1 2 3 4 5 6 7 8 9 10; do + if test -s .traverse/server.json; then + break + fi + sleep 1 +done + +test -s .traverse/server.json +base_url="$(grep -o '"base_url": "[^"]*"' .traverse/server.json | cut -d '"' -f4)" +test -n "$base_url" + +http_output="$(curl -sS -X POST "$base_url/v1/workspaces/local-default/execute" \ + -H 'Content-Type: application/json' \ + --data-binary "@$agent_request")" +printf '%s\n' "$http_output" + +grep -q '"status":"succeeded"' <<<"$http_output" +grep -q '"summary":"Kickoff notes for Traverse reference app.' <<<"$http_output" +grep -q '"action_items":\[' <<<"$http_output" +grep -q '"decisions":\[' <<<"$http_output" +grep -q '"follow_ups":\[' <<<"$http_output" diff --git a/workflows/examples/meeting-notes/process/workflow.json b/workflows/examples/meeting-notes/process/workflow.json new file mode 100644 index 00000000..86043941 --- /dev/null +++ b/workflows/examples/meeting-notes/process/workflow.json @@ -0,0 +1,84 @@ +{ + "kind": "workflow_definition", + "schema_version": "1.0.0", + "id": "meeting-notes.process", + "name": "process", + "version": "1.0.0", + "lifecycle": "active", + "owner": { + "team": "traverse-core", + "contact": "enrico.piovesan10@gmail.com" + }, + "summary": "Run the meeting-notes transcript processing flow as one governed workflow-backed example.", + "inputs": { + "schema": { + "type": "object", + "required": [ + "transcript" + ], + "properties": { + "transcript": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "outputs": { + "schema": { + "type": "object", + "required": [ + "action_items", + "decisions", + "follow_ups", + "summary" + ], + "properties": { + "action_items": { + "type": "array" + }, + "decisions": { + "type": "array" + }, + "follow_ups": { + "type": "array" + }, + "summary": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "nodes": [ + { + "node_id": "process_transcript", + "capability_id": "meeting-notes.process", + "capability_version": "1.0.0", + "input": { + "from_workflow_input": [ + "transcript" + ] + }, + "output": { + "to_workflow_state": [ + "action_items", + "decisions", + "follow_ups", + "summary" + ] + } + } + ], + "edges": [], + "start_node": "process_transcript", + "terminal_nodes": [ + "process_transcript" + ], + "tags": [ + "meeting-notes", + "reference-app", + "deterministic" + ], + "governing_spec": "007-workflow-registry-traversal" +} From 41dca67d514c8b54356dd8c8e0503e04251a0bdb Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Thu, 9 Jul 2026 23:01:23 -0600 Subject: [PATCH 2/2] fix: satisfy clippy in meeting notes summary --- crates/traverse-cli/src/main.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/traverse-cli/src/main.rs b/crates/traverse-cli/src/main.rs index 02ac9377..b8fc396a 100644 --- a/crates/traverse-cli/src/main.rs +++ b/crates/traverse-cli/src/main.rs @@ -3855,18 +3855,7 @@ fn render_agent_execution_summary( } } "meeting-notes.process" => { - if let Some(summary) = output.get("summary").and_then(Value::as_str) { - lines.push(format!("summary: {summary}")); - } - if let Some(action_items) = output.get("action_items").and_then(Value::as_array) { - lines.push(format!("action_items: {}", action_items.len())); - } - if let Some(decisions) = output.get("decisions").and_then(Value::as_array) { - lines.push(format!("decisions: {}", decisions.len())); - } - if let Some(follow_ups) = output.get("follow_ups").and_then(Value::as_array) { - lines.push(format!("follow_ups: {}", follow_ups.len())); - } + append_meeting_notes_summary(&mut lines, output); } _ => {} } @@ -3874,6 +3863,21 @@ fn render_agent_execution_summary( lines.join("\n") } +fn append_meeting_notes_summary(lines: &mut Vec, output: &Value) { + if let Some(summary) = output.get("summary").and_then(Value::as_str) { + lines.push(format!("summary: {summary}")); + } + for (field, label) in [ + ("action_items", "action_items"), + ("decisions", "decisions"), + ("follow_ups", "follow_ups"), + ] { + if let Some(values) = output.get(field).and_then(Value::as_array) { + lines.push(format!("{label}: {}", values.len())); + } + } +} + fn render_trace_summary(trace_path: &Path, trace: &RuntimeTrace) -> String { let final_transition = trace.state_transitions.last(); let mut lines = vec![